Commits

TK Soh  committed bac32db

import bzr-tortoise source @ rev 222 (Sat 2007-04-21 20:03:54 -0700)

  • Participants

Comments (0)

Files changed (26)

+syntax: glob
+
+*.orig
+*.rej
+*.pyc

File README.tortoisebzr

+
+===========
+TortoiseBzr
+===========
+
+About
+=====
+
+TortoiseBzr is a Windows shell extension (similar to `TortoiseCVS`_) for
+viewing the source control status of a `bzr`_ tree from within Windows
+Explorer.
+
+It is derived from `bzr-gtk`_.
+
+.. _bzr: http://www.bazaar-vcs.org
+.. _TortoiseCVS: http://www.tortoisecvs.org
+.. _bzr-gtk: http://bazaar-vcs.org/bzr-gtk
+
+
+Status
+======
+
+* Icon overlays work
+* Context menu hooks allow commit, checkout, and diff.
+
+
+TODO
+====
+
+* Speed it up (it currently causes a noticable delay when browsing)
+* Share a common 'source control' shell icon overlay extension with 
+  tortoisecvs (to avoid using too many icon overlays).
+* Hook in the `meld`_ diff / merge utility.
+* Enable add, merge, log, revert, etc context menu options
+* Only display relevant context menu items
+* Integrate pygtk and tortoisebzr into the bzr installer
+* Single file logging
+* Implement the ICopyHook shell interface to detect when the user moves
+  or deletes files (and update the bzr status accordingly). Note this
+  is only used by the shell, so it is only called in response to user
+  actions (not programatic actions).
+
+.. _meld: http://meld.sourceforge.net/
+
+
+Screenshots
+===========
+
+.. image:: screenshots/iconoverlay.png
+  :align: center
+
+.. image:: screenshots/contextmenu.png
+  :align: center
+
+.. image:: screenshots/commit.png
+  :align: center
+
+
+Development
+===========
+
+The tortoisebzr development branch is available by running::
+
+  bzr get http://www.hl.id.au/Projects/tortoisebzr/bzr-tortoise
+
+To register the shell extension, run::
+
+  set PYTHONPATH=c:\somepath\bzr.dev
+  python tortoise-bzr.py
+
+To remove the shell extension, run::
+
+  python tortoise-bzr.py --unregister
+

File icons/commit.png

Added
New image

File icons/commit16.png

Added
New image

File icons/diff.png

Added
New image

File icons/diff16.png

Added
New image

File icons/log.png

Added
New image

File icons/log16.png

Added
New image

File icons/olive-gtk.png

Added
New image

File icons/pull.png

Added
New image

File icons/pull16.png

Added
New image

File icons/push.png

Added
New image

File icons/push16.png

Added
New image

File icons/refresh.png

Added
New image

File icons/status/added.ico

Added
New image

File icons/status/changed.ico

Added
New image

File icons/status/conflict.ico

Added
New image

File icons/status/ignored.ico

Added
New image

File icons/status/unchanged.ico

Added
New image

File icons/status/unknown.ico

Added
New image

File tortoise-bzr.py

+# Simple TortoiseSVN-like Bazaar plugin for the Windows Shell
+# Published under the GNU GPL, v2 or later.
+# Copyright (C) 2007 Jelmer Vernooij <jelmer@samba.org>
+# Copyright (C) 2007 Henry Ludemann <misc@hl.id.au>
+
+import os.path
+import _winreg
+
+
+def DllRegisterServer(cls):
+    import _winreg
+
+    # Add bzrlib to the library path
+    try:
+        import bzrlib
+    except ImportError:
+        import sys
+        from win32com.server import register
+        register.UnregisterClasses(cls)
+        sys.exit("Error: Failed to find bzrlib module! Include the path to bzrlib in PYTHONPATH environment variable while registering component.")
+    bzr_path = os.path.dirname(os.path.dirname(bzrlib.__file__))
+    key = "CLSID\\%s\\PythonCOMPath" % cls._reg_clsid_
+    path = _winreg.QueryValue(_winreg.HKEY_CLASSES_ROOT, key)
+    _winreg.SetValue(_winreg.HKEY_CLASSES_ROOT, key, _winreg.REG_SZ, "%s;%s" % (path, bzr_path))
+
+    # Add the appropriate shell extension registry keys
+    for category, keyname in cls.registry_keys: 
+        _winreg.SetValue(category, keyname, _winreg.REG_SZ, cls._reg_clsid_)
+
+    print cls._reg_desc_, "registration complete."
+
+def DllUnregisterServer(cls):
+    import _winreg
+    for category, keyname in cls.registry_keys:
+        try:
+            _winreg.DeleteKey(category, keyname)
+        except WindowsError, details:
+            import errno
+            if details.errno != errno.ENOENT:
+                raise
+    print cls._reg_desc_, "unregistration complete."
+
+if __name__ == '__main__':
+    from win32com.server import register
+    from tortoise.contextmenu import ContextMenuExtension
+    register.UseCommandLine(ContextMenuExtension,
+                   finalize_register = lambda: DllRegisterServer(ContextMenuExtension),
+                   finalize_unregister = lambda: DllUnregisterServer(ContextMenuExtension))
+
+    # Register all of the icon overlay extensions
+    import tortoise.iconoverlay
+    for icon_class in tortoise.iconoverlay.get_overlay_classes():
+        register.UseCommandLine(icon_class,
+                       finalize_register = lambda: DllRegisterServer(icon_class),
+                       finalize_unregister = lambda: DllUnregisterServer(icon_class))

File tortoise/__init__.py

+
+import gettext
+
+# Add '_' to the builtin namespace
+gettext.install('olive-gtk')
+

File tortoise/contextmenu.py

+# Published under the GNU GPL, v2 or later.
+# Copyright (C) 2007 Jelmer Vernooij <jelmer@samba.org>
+# Copyright (C) 2007 Henry Ludemann <misc@hl.id.au>
+
+import os.path
+import pythoncom
+from win32com.shell import shell, shellcon
+import win32con
+import win32gui
+import win32gui_struct
+import _winreg
+
+from dialog import info_dialog
+
+S_OK = 0
+S_FALSE = 1
+
+"""Windows shell extension that adds context menu items to Bazaar branches."""
+class ContextMenuExtension:
+    _reg_progid_ = "Bazaar.ShellExtension.ContextMenu"
+    _reg_desc_ = "Bazaar Shell Extension"
+    _reg_clsid_ = "{EEE9936B-73ED-4D45-80C9-AF918354F885}"
+    _com_interfaces_ = [shell.IID_IShellExtInit, shell.IID_IContextMenu]
+    _public_methods_ = [
+        "Initialize", # From IShellExtInit
+        "QueryContextMenu", "InvokeCommand", "GetCommandString" # IContextMenu
+        ]
+
+    registry_keys = [
+        (_winreg.HKEY_CLASSES_ROOT, r"*\shellex\ContextMenuHandlers\TortoiseBzr"),
+        (_winreg.HKEY_CLASSES_ROOT, r"Directory\Background\shellex\ContextMenuHandlers\TortoiseBzr"),
+        (_winreg.HKEY_CLASSES_ROOT, r"Directory\shellex\ContextMenuHandlers\TortoiseBzr"),
+        (_winreg.HKEY_CLASSES_ROOT, r"Folder\shellex\ContextMenuHandlers\TortoiseBzr"),
+        ]
+
+    def __init__(self):
+        self._filenames = []
+        self._handlers = {}
+
+    def Initialize(self, folder, dataobj, hkey):
+        format_etc = win32con.CF_HDROP, None, 1, -1, pythoncom.TYMED_HGLOBAL
+        sm = dataobj.GetData(format_etc)
+        num_files = shell.DragQueryFile(sm.data_handle, -1)
+        self._filenames = []
+        for i in range(num_files):
+            self._filenames.append(shell.DragQueryFile(sm.data_handle, i))
+
+    def QueryContextMenu(self, hMenu, indexMenu, idCmdFirst, idCmdLast, uFlags):
+        if uFlags & shellcon.CMF_DEFAULTONLY:
+            return 0
+
+        # As we are a context menu handler, we can ignore verbs.
+        self._handlers = {}
+        commands = self._get_commands()
+        if len(commands) > 0:
+            win32gui.InsertMenu(hMenu, indexMenu,
+                                win32con.MF_SEPARATOR|win32con.MF_BYPOSITION,
+                                0, None)
+            indexMenu += 1
+            for id, (text, help_text, command) in enumerate(commands):
+                item, extras = win32gui_struct.PackMENUITEMINFO(text=text,
+                            wID=idCmdFirst + id)
+                win32gui.InsertMenuItem(hMenu, indexMenu + id, 1, item)
+                self._handlers[id] = (help_text, command)
+
+            indexMenu += len(commands)
+            win32gui.InsertMenu(hMenu, indexMenu,
+                                win32con.MF_SEPARATOR|win32con.MF_BYPOSITION,
+                                0, None)
+            indexMenu += 1
+
+        # Return the number of commands we added
+        return len(commands)
+
+    def _get_commands(self):
+        """
+        Get a list of commands valid for the current selection.
+
+        Each command is a tuple containing (display text, handler).
+        """
+        import bzrlib.builtins
+        import bzrlib.errors
+
+        tree = None
+        try:
+            tree, relative_files = bzrlib.builtins.internal_tree_files(self._filenames)
+        except bzrlib.errors.NotBranchError:
+            pass
+        except bzrlib.errors.FileInWrongBranch:
+            # We have no commands that are valid for multiple branches.
+            return []
+
+        result = []
+        if len(self._filenames) == 1 and tree is None and os.path.isdir(self._filenames[0]):
+            result.append((_("Bzr Checkout"), _("Checkout a bazaar branch"), self._checkout))
+        if tree is not None:
+            result.append((_("Commit"), _("Commit changes to the branch"), self._commit))
+        if tree is not None and len(self._filenames) == 1:
+            result.append((_("Diff"), _("View changes made in the local tree"), self._diff))
+        return result
+
+    def InvokeCommand(self, ci):
+        mask, hwnd, verb, params, dir, nShow, hotkey, hicon = ci
+        if verb >> 16:
+            # This is a textual verb invocation... not supported.
+            return S_FALSE
+        if verb not in self._handlers:
+            raise Exception("Unsupported command id %i!" % verb)
+        self._handlers[verb][1](hwnd)
+
+    def GetCommandString(self, cmd, uFlags):
+        if uFlags & shellcon.GCS_VALIDATEA or uFlags & shellcon.GCS_VALIDATEW:
+            if cmd in self._handlers:
+                return S_OK
+            return S_FALSE
+        if uFlags & shellcon.GCS_VERBA or uFlags & shellcon.GCS_VERBW:
+            return S_FALSE
+        if uFlags & shellcon.GCS_HELPTEXTA or uFlags & shellcon.GCS_HELPTEXTW:
+            # The win32com.shell implementation encodes the resultant
+            # string into the correct encoding depending on the flags.
+            return self._handlers[cmd][0]
+        return S_FALSE
+
+    def _checkout(self, parent_window):
+        import checkout
+        dialog = checkout.CheckoutDialog(self._filenames[0])
+        dialog.run()
+        dialog.destroy()
+
+    def _commit(self, parent_window):
+        import bzrlib.builtins
+
+        # Note that we don't catch the exceptions; we shouldn't be in here
+        # if we aren't in a tree.
+        tree, relative_files = bzrlib.builtins.internal_tree_files(self._filenames)
+
+        import commit
+        import gtk
+        # Note that the commit dialog only handles a single item to
+        # commit at the moment...
+        dialog = commit.CommitDialog(tree, tree.basedir, False, relative_files)
+        if not dialog.delta.has_changed():
+            info_dialog(_("Commit"), _("No changes found!"))
+        else:
+            dialog.run()
+            dialog.destroy()
+
+    def _diff(self, parent_window):
+        import bzrlib.builtins
+
+        # Note that we don't catch the exceptions; we shouldn't be in here
+        # if we aren't in a tree.
+        tree, relative_files = bzrlib.builtins.internal_tree_files(self._filenames)
+        assert len(relative_files) == 1
+
+        import diff
+        import gtk
+
+        # Note that the commit dialog only handles a single item to
+        # commit at the moment...
+        window = diff.DiffWindow()
+        window.set_diff("changes", tree, tree.basis_tree())
+        if relative_files[0] != "":
+            window.set_file(relative_files[0])
+        window.connect("destroy", lambda widgit: gtk.main_quit())
+        window.show()
+        gtk.main()
+

File tortoise/iconoverlay.py

+# Published under the GNU GPL, v2 or later.
+# Copyright (C) 2007 Henry Ludemann <misc@hl.id.au>
+
+import os.path
+from win32com.shell import shell, shellcon
+import _winreg
+
+UNCHANGED = "unchanged"
+ADDED = "added"
+MODIFIED = "modified"
+UNKNOWN = "unknown"
+NOT_IN_TREE = "not in tree"
+CONTROL_FILE = "control file"
+
+class IconOverlayExtension(object):
+    """
+    Class to implement icon overlays for source controlled files.
+
+    Displays a different icon based on version control status.
+
+    NOTE: The system allocates only 15 slots in _total_ for all
+        icon overlays; we (will) use 6, tortoisecvs uses 7... not a good
+        recipe for a happy system.
+    """
+    _com_interfaces_ = [shell.IID_IShellIconOverlayIdentifier]
+    _public_methods_ = [
+        "GetOverlayInfo", "GetPriority", "IsMemberOf"
+        ]
+
+    def GetOverlayInfo(self):
+        icon = os.path.join(os.path.dirname(__file__), "..", "icons", "status", self.icon)
+        return (icon, 0, shellcon.ISIOI_ICONFILE)
+
+    def GetPriority(self):
+        return 0
+
+    def _get_state(self, path):
+        """
+        Get the state of a given path in source control.
+        """
+        import bzrlib.workingtree
+        import bzrlib.errors
+        try:
+            tree, relpath = bzrlib.workingtree.WorkingTree.open_containing(path)
+        except bzrlib.errors.NotBranchError:
+            # We aren't in a working tree
+            return NOT_IN_TREE
+
+        # Ideally, we'd use tree.changes_from, but unfortunately this is
+        # recusive for directories, so we calculate changes ourselves.
+        if tree.is_control_filename(relpath):
+            return CONTROL_FILE
+
+        file_id = tree.path2id(relpath)
+        if file_id is None:
+            # This is an unknown file...
+            return UNKNOWN
+
+        # Get information about the path from the old tree
+        old = tree.basis_tree()
+        old.lock_read()
+        try:
+            try:
+                entry = old.inventory[file_id]
+            except bzrlib.errors.NoSuchId:
+                # This is an added entry...
+                return ADDED
+
+            if entry.kind != "file":
+                # TODO: For directories, we want to find out if
+                # the contents of the directories have changed...
+                return UNCHANGED
+
+            old_size = entry.text_size
+            old_sha1 = entry.text_sha1
+        finally:
+            old.unlock()
+
+        # Look in the new tree to see if has changed...
+        tree.lock_read()
+        try:
+            if tree.get_file_size(file_id) != old_size or tree.get_file_sha1(file_id) != old_sha1:
+                # This file has been modified
+                return MODIFIED
+        finally:
+            tree.unlock()
+
+        # This file has unchanged.
+        return UNCHANGED
+
+    def IsMemberOf(self, path, attrib):
+        S_OK = 0
+        S_FALSE = 1
+        if self._get_state(path) == self.state:
+            return S_OK
+        return S_FALSE
+
+def make_icon_overlay(name, icon, state, clsid):
+    """
+    Make an icon overlay COM class.
+
+    Used to create different COM server classes for highlighting the
+    files with different source controlled states (eg: unchanged, 
+    modified, ...).
+    """
+    classname = "%sOverlay" % name
+    prog_id = "Bazaar.ShellExtension.%s" % classname
+    desc = "Bazaar icon overlay shell extension for %s files" % name.lower()
+    reg = [
+        (_winreg.HKEY_LOCAL_MACHINE, r"Software\Microsoft\Windows\CurrentVersion\Explorer\ShellIconOverlayIdentifiers\%s" % name) ]
+    cls = type(
+            classname,
+            (IconOverlayExtension, ),
+            dict(_reg_clsid_=clsid, _reg_progid_=prog_id, _reg_desc_=desc, registry_keys=reg, icon=icon, state=state))
+
+    _overlay_classes.append(cls)
+    # We need to register the class as global, as pythoncom will
+    # create an instance of it.
+    globals()[classname] = cls
+
+_overlay_classes = []
+make_icon_overlay("Unchanged", "unchanged.ico", UNCHANGED, "{00FEE959-5773-424B-88AC-A01BFC8E4555}")
+make_icon_overlay("Added", "added.ico", ADDED, "{8447DB75-5875-4BA8-9F38-A727DAA484A0}")
+make_icon_overlay("Changed", "changed.ico", MODIFIED, "{102C6A24-5F38-4186-B64A-237011809FAB}")
+make_icon_overlay("Unknown", "unknown.ico", UNKNOWN, "{A8AFEA16-5F0C-4BE2-A64B-80C41C50D911}")
+
+def get_overlay_classes():
+    """
+    Get a list of all registerable icon overlay classes
+    """
+    return _overlay_classes

File tortoise/test/__init__.py

Empty file added.

File tortoise/test/testcontextmenu.py

+import os.path
+import pythoncom
+import struct
+import tempfile
+import tortoise.contextmenu
+import unittest
+import win32con
+import win32ui
+
+class _MockDataObject:
+    """
+    Class to pretend to be a IDataObject
+    """
+    def __init__(self, storage):
+        self._storage = storage
+
+    def GetData(self, format):
+        return self._storage
+
+def _packDROPFILES(filenames):
+    text = "\0".join(filenames)
+    offset_to_text = 14
+    return struct.pack("LLLbb%is" % (len(text) + 1), offset_to_text, 0, 0, 0, 0, text)
+
+class TestContextMenuExtension(unittest.TestCase):
+    """
+    Test the context menu shell extension.
+    """
+
+    def _get_menu_entries(self, filenames):
+        """
+        Populate a menu given selected filenames
+        """
+        # Create a storage medium with a drop target files information
+        storage = pythoncom.STGMEDIUM()
+        storage.set(pythoncom.TYMED_HGLOBAL, _packDROPFILES(filenames))
+        data = _MockDataObject(storage)
+
+        contextmenu = tortoise.contextmenu.ContextMenuExtension()
+        contextmenu.Initialize(None, data, None)
+
+        # Populate the menu
+        menu = win32ui.CreateMenu()
+        initial_id = 10
+        final_id = initial_id + contextmenu.QueryContextMenu(menu.GetHandle(), 0, initial_id, 1000, 0)
+        result = []
+        for i in range(menu.GetMenuItemCount()):
+            text = menu.GetMenuString(i, win32con.MF_BYPOSITION)
+            if text:
+                self.assertTrue(menu.GetMenuItemID(i) in xrange(initial_id, final_id))
+            result.append(text)
+
+        return result
+
+    def test_checkout_menu_item_when_not_in_tree(self):
+        entries = self._get_menu_entries([tempfile.gettempdir()])
+        self.assertEqual(["", "Bzr Checkout", ""], entries)
+
+    def test_folder_context_menu_items(self):
+        entries = self._get_menu_entries([os.path.dirname(__file__)])
+        self.assertEqual(["", "Commit", "Diff", ""], entries)