{"thread":{"id":"4145","subject":"[PATCH 1/3] Don't die when there are no branches","startedAt":"2006-05-15T09:53:24Z","lastAt":"2006-05-18T16:00:40Z","messageCount":15,"participants":["Karl Hasselström","Catalin Marinas","Junio C Hamano"],"isPatch":true,"patchVersion":1,"patchTotal":3},"messages":[{"id":"19986","messageId":"20060515095324.GA11386@backpacker.hemma.treskal.com","threadId":"4145","inReplyTo":"20060510060040.GA3034@diana.vm.bytemark.co.uk","subject":"[PATCH 1/3] Don't die when there are no branches","fromName":"Karl Hasselström","fromEmail":"kha@treskal.com","sentAt":"2006-05-15T09:53:24Z","receivedAt":"2006-05-15T09:53:24Z","isPatch":true,"sender":{"key":"kha@treskal.com","avatar":"https://gravatar.com/avatar/f0120c734b5279b345075a28521e1ac66acb20c9913ffe9bf6ae97e53f7f3f13?d=mp&s=160"},"body":"Signed-off-by: Karl Hasselström <kha@treskal.com>\n\n\n---\n\n stgit/commands/branch.py |   11 +++++++----\n 1 files changed, 7 insertions(+), 4 deletions(-)\n\nc32f6b7bfd81bdbdb136ff72a4ad073e162ab97c\ndiff --git a/stgit/commands/branch.py b/stgit/commands/branch.py\nindex c7561a8..2218bbb 100644\n--- a/stgit/commands/branch.py\n+++ b/stgit/commands/branch.py\n@@ -174,11 +174,14 @@ def func(parser, options, args):\n \n         branches = os.listdir(os.path.join(basedir.get(), 'refs', 'heads'))\n         branches.sort()\n-        max_len = max([len(i) for i in branches])\n \n-        print 'Available branches:'\n-        for i in branches:\n-            __print_branch(i, max_len)\n+        if branches:\n+            print 'Available branches:'\n+            max_len = max([len(i) for i in branches])\n+            for i in branches:\n+                __print_branch(i, max_len)\n+        else:\n+            print 'No branches'\n         return\n \n     elif options.protect:\n-- \n1.3.2.g639c\n\n\n-- \nKarl Hasselström, kha@treskal.com\n      www.treskal.com/kalle\n"},{"id":"19987","messageId":"20060515095440.GA11412@backpacker.hemma.treskal.com","threadId":"4145","inReplyTo":"20060510060040.GA3034@diana.vm.bytemark.co.uk","subject":"[PATCH 2/3] Handle branch names with slashes","fromName":"Karl Hasselström","fromEmail":"kha@treskal.com","sentAt":"2006-05-15T09:54:40Z","receivedAt":"2006-05-15T09:54:40Z","isPatch":true,"sender":{"key":"kha@treskal.com","avatar":"https://gravatar.com/avatar/f0120c734b5279b345075a28521e1ac66acb20c9913ffe9bf6ae97e53f7f3f13?d=mp&s=160"},"body":"Teach stgit to handle branch names with slashes in them; that is,\nbranches living in a subdirectory of .git/refs/heads.\n\nI had to change the patch@branch/top command-line syntax to\npatch@branch#top, in order to get sane parsing. The /top variant is\nstill available for repositories that have no slashy branches; it is\ndisabled as soon as there exists at least one subdirectory of\nrefs/heads. Preferably, this compatibility hack can be killed some\ntime in the future.\n\nSigned-off-by: Karl Hasselström <kha@treskal.com>\n\n\n---\n\n stgit/commands/branch.py |    5 ++\n stgit/commands/common.py |  103 ++++++++++++++++++++++++++--------------------\n stgit/commands/diff.py   |   12 +++--\n stgit/commands/files.py  |    4 +-\n stgit/commands/id.py     |    2 -\n stgit/commands/mail.py   |    8 ++--\n stgit/git.py             |   42 +++++++++----------\n stgit/stack.py           |   21 ++++++---\n stgit/utils.py           |   88 +++++++++++++++++++++++++++++++++++++--\n 9 files changed, 193 insertions(+), 92 deletions(-)\n\n466723a40f8d40b84a23fe720447e1938e22c0a5\ndiff --git a/stgit/commands/branch.py b/stgit/commands/branch.py\nindex 2218bbb..d348409 100644\n--- a/stgit/commands/branch.py\n+++ b/stgit/commands/branch.py\n@@ -172,7 +172,10 @@ def func(parser, options, args):\n         if len(args) != 0:\n             parser.error('incorrect number of arguments')\n \n-        branches = os.listdir(os.path.join(basedir.get(), 'refs', 'heads'))\n+        branches = []\n+        basepath = os.path.join(basedir.get(), 'refs', 'heads')\n+        for path, files, dirs in walk_tree(basepath):\n+            branches += [os.path.join(path, f) for f in files]\n         branches.sort()\n \n         if branches:\ndiff --git a/stgit/commands/common.py b/stgit/commands/common.py\nindex c6ca514..9439976 100644\n--- a/stgit/commands/common.py\n+++ b/stgit/commands/common.py\n@@ -18,7 +18,7 @@ along with this program; if not, write t\n Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA\n \"\"\"\n \n-import sys, os, re\n+import sys, os, os.path, re\n from optparse import OptionParser, make_option\n \n from stgit.utils import *\n@@ -34,54 +34,69 @@ class CmdException(Exception):\n \n \n # Utility functions\n+class RevParseException(Exception):\n+    \"\"\"Revision spec parse error.\"\"\"\n+    pass\n+\n+def parse_rev(rev):\n+    \"\"\"Parse a revision specification into its\n+    patchname@branchname#patch_id parts. If no branch name has a slash\n+    in it, also accept / instead of #.\"\"\"\n+    files, dirs = list_files_and_dirs(os.path.join(basedir.get(),\n+                                                   'refs', 'heads'))\n+    if len(dirs) != 0:\n+        # We have branch names with / in them.\n+        branch_chars = '[^@#]'\n+        patch_id_mark = '#'\n+    else:\n+        # No / in branch names.\n+        branch_chars = '[^@#/]'\n+        patch_id_mark = '[/#]'\n+    patch_re = r'(?P<patch>[^@/#]+)'\n+    branch_re = r'@(?P<branch>%s+)' % branch_chars\n+    patch_id_re = r'%s(?P<patch_id>[a-z.]*)' % patch_id_mark\n+\n+    # Try #patch_id.\n+    m = re.match(r'^%s$' % patch_id_re, rev)\n+    if m:\n+        return None, None, m.group('patch_id')\n+\n+    # Try patch[@branch][#patch_id].\n+    m = re.match(r'^%s(%s)?(%s)?$' % (patch_re, branch_re, patch_id_re), rev)\n+    if m:\n+        return m.group('patch'), m.group('branch'), m.group('patch_id')\n+\n+    # No, we can't parse that.\n+    raise RevParseException\n+\n def git_id(rev):\n     \"\"\"Return the GIT id\n     \"\"\"\n     if not rev:\n         return None\n-    \n-    rev_list = rev.split('/')\n-    if len(rev_list) == 2:\n-        patch_id = rev_list[1]\n-        if not patch_id:\n-            patch_id = 'top'\n-    elif len(rev_list) == 1:\n-        patch_id = 'top'\n-    else:\n-        patch_id = None\n-\n-    patch_branch = rev_list[0].split('@')\n-    if len(patch_branch) == 1:\n-        series = crt_series\n-    elif len(patch_branch) == 2:\n-        series = stack.Series(patch_branch[1])\n-    else:\n-        raise CmdException, 'Unknown id: %s' % rev\n-\n-    patch_name = patch_branch[0]\n-    if not patch_name:\n-        patch_name = series.get_current()\n-        if not patch_name:\n-            raise CmdException, 'No patches applied'\n-\n-    # patch\n-    if patch_name in series.get_applied() \\\n-           or patch_name in series.get_unapplied():\n-        if patch_id == 'top':\n-            return series.get_patch(patch_name).get_top()\n-        elif patch_id == 'bottom':\n-            return series.get_patch(patch_name).get_bottom()\n-        # Note we can return None here.\n-        elif patch_id == 'top.old':\n-            return series.get_patch(patch_name).get_old_top()\n-        elif patch_id == 'bottom.old':\n-            return series.get_patch(patch_name).get_old_bottom()\n-\n-    # base\n-    if patch_name == 'base' and len(rev_list) == 1:\n-        return read_string(series.get_base_file())\n-\n-    # anything else failed\n+    try:\n+        patch, branch, patch_id = parse_rev(rev)\n+        if branch == None:\n+            series = crt_series\n+        else:\n+            series = stack.Series(branch)\n+        if patch == None:\n+            patch = series.get_current()\n+            if not patch:\n+                raise CmdException, 'No patches applied'\n+        if patch in series.get_applied() or patch in series.get_unapplied():\n+            if patch_id in ['top', '', None]:\n+                return series.get_patch(patch).get_top()\n+            elif patch_id == 'bottom':\n+                return series.get_patch(patch).get_bottom()\n+            elif patch_id == 'top.old':\n+                return series.get_patch(patch).get_old_top()\n+            elif patch_id == 'bottom.old':\n+                return series.get_patch(patch).get_old_bottom()\n+        if patch == 'base' and patch_id == None:\n+            return read_string(series.get_base_file())\n+    except RevParseException:\n+        pass\n     return git.rev_parse(rev + '^{commit}')\n \n def check_local_changes():\ndiff --git a/stgit/commands/diff.py b/stgit/commands/diff.py\nindex 7dc6c5d..c3ab7d8 100644\n--- a/stgit/commands/diff.py\n+++ b/stgit/commands/diff.py\n@@ -33,12 +33,12 @@ or a tree-ish object and another tree-is\n be given to restrict the diff output. The tree-ish object can be a\n standard git commit, tag or tree. In addition to these, the command\n also supports 'base', representing the bottom of the current stack,\n-and '[patch]/[bottom | top]' for the patch boundaries (defaulting to\n+and '[patch]#[bottom | top]' for the patch boundaries (defaulting to\n the current one):\n \n-rev = '([patch]/[bottom | top]) | <tree-ish> | base'\n+rev = '([patch]#[bottom | top]) | <tree-ish> | base'\n \n-If neither bottom or top are given but a '/' is present, the command\n+If neither bottom or top are given but a '#' is present, the command\n shows the specified patch (defaulting to the current one).\"\"\"\n \n options = [make_option('-r', metavar = 'rev1[:[rev2]]', dest = 'revs',\n@@ -55,10 +55,10 @@ def func(parser, options, args):\n         rev_list = options.revs.split(':')\n         rev_list_len = len(rev_list)\n         if rev_list_len == 1:\n-            if rev_list[0][-1] == '/':\n+            if rev_list[0][-1] in ['/', '#']:\n                 # the whole patch\n-                rev1 = rev_list[0] + 'bottom'\n-                rev2 = rev_list[0] + 'top'\n+                rev1 = rev_list[0][:-1] + '#bottom'\n+                rev2 = rev_list[0][:-1] + '#top'\n             else:\n                 rev1 = rev_list[0]\n                 rev2 = None\ndiff --git a/stgit/commands/files.py b/stgit/commands/files.py\nindex 0694d83..1d5126e 100644\n--- a/stgit/commands/files.py\n+++ b/stgit/commands/files.py\n@@ -53,8 +53,8 @@ def func(parser, options, args):\n     else:\n         parser.error('incorrect number of arguments')\n \n-    rev1 = git_id('%s/bottom' % patch)\n-    rev2 = git_id('%s/top' % patch)\n+    rev1 = git_id('%s#bottom' % patch)\n+    rev2 = git_id('%s#top' % patch)\n \n     if options.stat:\n         print git.diffstat(rev1 = rev1, rev2 = rev2)\ndiff --git a/stgit/commands/id.py b/stgit/commands/id.py\nindex 1cf6ea6..5202aa4 100644\n--- a/stgit/commands/id.py\n+++ b/stgit/commands/id.py\n@@ -28,7 +28,7 @@ usage = \"\"\"%prog [options] [id]\n \n Print the hash value of a GIT id (defaulting to HEAD). In addition to\n the standard GIT id's like heads and tags, this command also accepts\n-'base[@<branch>]' and '[<patch>[@<branch>]][/(bottom | top)]'. If no\n+'base[@<branch>]' and '[<patch>[@<branch>]][#(bottom | top)]'. If no\n 'top' or 'bottom' are passed and <patch> is a valid patch name, 'top'\n will be used by default.\"\"\"\n \ndiff --git a/stgit/commands/mail.py b/stgit/commands/mail.py\nindex 5e01ea1..937efa3 100644\n--- a/stgit/commands/mail.py\n+++ b/stgit/commands/mail.py\n@@ -324,10 +324,10 @@ def __build_message(tmpl, patch, patch_n\n                  'shortdescr':   short_descr,\n                  'longdescr':    long_descr,\n                  'endofheaders': headers_end,\n-                 'diff':         git.diff(rev1 = git_id('%s/bottom' % patch),\n-                                          rev2 = git_id('%s/top' % patch)),\n-                 'diffstat':     git.diffstat(rev1 = git_id('%s/bottom'%patch),\n-                                              rev2 = git_id('%s/top' % patch)),\n+                 'diff':         git.diff(rev1 = git_id('%s#bottom' % patch),\n+                                          rev2 = git_id('%s#top' % patch)),\n+                 'diffstat':     git.diffstat(rev1 = git_id('%s#bottom'%patch),\n+                                              rev2 = git_id('%s#top' % patch)),\n                  'date':         email.Utils.formatdate(localtime = True),\n                  'version':      version_str,\n                  'patchnr':      patch_nr_str,\ndiff --git a/stgit/git.py b/stgit/git.py\nindex 2884f36..716609c 100644\n--- a/stgit/git.py\n+++ b/stgit/git.py\n@@ -225,7 +225,8 @@ def get_head():\n def get_head_file():\n     \"\"\"Returns the name of the file pointed to by the HEAD link\n     \"\"\"\n-    return os.path.basename(_output_one_line('git-symbolic-ref HEAD'))\n+    return strip_prefix('refs/heads/',\n+                        _output_one_line('git-symbolic-ref HEAD'))\n \n def set_head_file(ref):\n     \"\"\"Resets HEAD to point to a new ref\n@@ -233,7 +234,8 @@ def set_head_file(ref):\n     # head cache flushing is needed since we might have a different value\n     # in the new head\n     __clear_head_cache()\n-    if __run('git-symbolic-ref HEAD', [ref]) != 0:\n+    if __run('git-symbolic-ref HEAD',\n+             [os.path.join('refs', 'heads', ref)]) != 0:\n         raise GitException, 'Could not set head to \"%s\"' % ref\n \n def __set_head(val):\n@@ -272,6 +274,7 @@ def rev_parse(git_id):\n def branch_exists(branch):\n     \"\"\"Existence check for the named branch\n     \"\"\"\n+    branch = os.path.join('refs', 'heads', branch)\n     for line in _output_lines('git-rev-parse --symbolic --all 2>&1'):\n         if line.strip() == branch:\n             return True\n@@ -282,12 +285,11 @@ def branch_exists(branch):\n def create_branch(new_branch, tree_id = None):\n     \"\"\"Create a new branch in the git repository\n     \"\"\"\n-    new_head = os.path.join('refs', 'heads', new_branch)\n-    if branch_exists(new_head):\n+    if branch_exists(new_branch):\n         raise GitException, 'Branch \"%s\" already exists' % new_branch\n \n     current_head = get_head()\n-    set_head_file(new_head)\n+    set_head_file(new_branch)\n     __set_head(current_head)\n \n     # a checkout isn't needed if new branch points to the current head\n@@ -297,22 +299,22 @@ def create_branch(new_branch, tree_id = \n     if os.path.isfile(os.path.join(basedir.get(), 'MERGE_HEAD')):\n         os.remove(os.path.join(basedir.get(), 'MERGE_HEAD'))\n \n-def switch_branch(name):\n+def switch_branch(new_branch):\n     \"\"\"Switch to a git branch\n     \"\"\"\n     global __head\n \n-    new_head = os.path.join('refs', 'heads', name)\n-    if not branch_exists(new_head):\n-        raise GitException, 'Branch \"%s\" does not exist' % name\n+    if not branch_exists(new_branch):\n+        raise GitException, 'Branch \"%s\" does not exist' % new_branch\n \n-    tree_id = rev_parse(new_head + '^{commit}')\n+    tree_id = rev_parse(os.path.join('refs', 'heads', new_branch)\n+                        + '^{commit}')\n     if tree_id != get_head():\n         refresh_index()\n         if __run('git-read-tree -u -m', [get_head(), tree_id]) != 0:\n             raise GitException, 'git-read-tree failed (local changes maybe?)'\n         __head = tree_id\n-    set_head_file(new_head)\n+    set_head_file(new_branch)\n \n     if os.path.isfile(os.path.join(basedir.get(), 'MERGE_HEAD')):\n         os.remove(os.path.join(basedir.get(), 'MERGE_HEAD'))\n@@ -320,25 +322,23 @@ def switch_branch(name):\n def delete_branch(name):\n     \"\"\"Delete a git branch\n     \"\"\"\n-    branch_head = os.path.join('refs', 'heads', name)\n-    if not branch_exists(branch_head):\n+    if not branch_exists(name):\n         raise GitException, 'Branch \"%s\" does not exist' % name\n-    os.remove(os.path.join(basedir.get(), branch_head))\n+    remove_file_and_dirs(os.path.join(basedir.get(), 'refs', 'heads'),\n+                         name)\n \n def rename_branch(from_name, to_name):\n     \"\"\"Rename a git branch\n     \"\"\"\n-    from_head = os.path.join('refs', 'heads', from_name)\n-    if not branch_exists(from_head):\n+    if not branch_exists(from_name):\n         raise GitException, 'Branch \"%s\" does not exist' % from_name\n-    to_head = os.path.join('refs', 'heads', to_name)\n-    if branch_exists(to_head):\n+    if branch_exists(to_name):\n         raise GitException, 'Branch \"%s\" already exists' % to_name\n \n     if get_head_file() == from_name:\n-        set_head_file(to_head)\n-    os.rename(os.path.join(basedir.get(), from_head), \\\n-              os.path.join(basedir.get(), to_head))\n+        set_head_file(to_name)\n+    rename(os.path.join(basedir.get(), 'refs', 'heads'),\n+           from_name, to_name)\n \n def add(names):\n     \"\"\"Add the files or recursively add the directory contents\ndiff --git a/stgit/stack.py b/stgit/stack.py\nindex f83161b..49b50e7 100644\n--- a/stgit/stack.py\n+++ b/stgit/stack.py\n@@ -443,8 +443,7 @@ class Series:\n \n         os.makedirs(self.__patch_dir)\n \n-        if not os.path.isdir(bases_dir):\n-            os.makedirs(bases_dir)\n+        create_dirs(bases_dir)\n \n         create_empty_file(self.__applied_file)\n         create_empty_file(self.__unapplied_file)\n@@ -502,11 +501,14 @@ class Series:\n         git.rename_branch(self.__name, to_name)\n \n         if os.path.isdir(self.__series_dir):\n-            os.rename(self.__series_dir, to_stack.__series_dir)\n+            rename(os.path.join(self.__base_dir, 'patches'),\n+                   self.__name, to_stack.__name)\n         if os.path.exists(self.__base_file):\n-            os.rename(self.__base_file, to_stack.__base_file)\n+            rename(os.path.join(self.__base_dir, 'refs', 'bases'),\n+                   self.__name, to_stack.__name)\n         if os.path.exists(self.__refs_dir):\n-            os.rename(self.__refs_dir, to_stack.__refs_dir)\n+            rename(os.path.join(self.__base_dir, 'refs', 'patches'),\n+                   self.__name, to_stack.__name)\n \n         self.__init__(to_name)\n \n@@ -560,16 +562,19 @@ class Series:\n             else:\n                 print 'Patch directory %s is not empty.' % self.__name\n             if not os.listdir(self.__series_dir):\n-                os.rmdir(self.__series_dir)\n+                remove_dirs(os.path.join(self.__base_dir, 'patches'),\n+                            self.__name)\n             else:\n                 print 'Series directory %s is not empty.' % self.__name\n             if not os.listdir(self.__refs_dir):\n-                os.rmdir(self.__refs_dir)\n+                remove_dirs(os.path.join(self.__base_dir, 'refs', 'patches'),\n+                            self.__name)\n             else:\n                 print 'Refs directory %s is not empty.' % self.__refs_dir\n \n         if os.path.exists(self.__base_file):\n-            os.remove(self.__base_file)\n+            remove_file_and_dirs(\n+                os.path.join(self.__base_dir, 'refs', 'bases'), self.__name)\n \n     def refresh_patch(self, files = None, message = None, edit = False,\n                       show_patch = False,\ndiff --git a/stgit/utils.py b/stgit/utils.py\nindex 5749b3b..68b8f58 100644\n--- a/stgit/utils.py\n+++ b/stgit/utils.py\n@@ -1,6 +1,8 @@\n \"\"\"Common utility functions\n \"\"\"\n \n+import errno, os, os.path\n+\n __copyright__ = \"\"\"\n Copyright (C) 2005, Catalin Marinas <catalin.marinas@gmail.com>\n \n@@ -18,6 +20,12 @@ along with this program; if not, write t\n Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA\n \"\"\"\n \n+def mkdir_file(filename, mode):\n+    \"\"\"Opens filename with the given mode, creating the directory it's\n+    in if it doesn't already exist.\"\"\"\n+    create_dirs(os.path.dirname(filename))\n+    return file(filename, mode)\n+\n def read_string(filename, multiline = False):\n     \"\"\"Reads the first line from a file\n     \"\"\"\n@@ -32,7 +40,7 @@ def read_string(filename, multiline = Fa\n def write_string(filename, line, multiline = False):\n     \"\"\"Writes 'line' to file and truncates it\n     \"\"\"\n-    f = file(filename, 'w+')\n+    f = mkdir_file(filename, 'w+')\n     if multiline:\n         f.write(line)\n     else:\n@@ -42,7 +50,7 @@ def write_string(filename, line, multili\n def append_strings(filename, lines):\n     \"\"\"Appends 'lines' sequence to file\n     \"\"\"\n-    f = file(filename, 'a+')\n+    f = mkdir_file(filename, 'a+')\n     for line in lines:\n         print >> f, line\n     f.close()\n@@ -50,14 +58,14 @@ def append_strings(filename, lines):\n def append_string(filename, line):\n     \"\"\"Appends 'line' to file\n     \"\"\"\n-    f = file(filename, 'a+')\n+    f = mkdir_file(filename, 'a+')\n     print >> f, line\n     f.close()\n \n def insert_string(filename, line):\n     \"\"\"Inserts 'line' at the beginning of the file\n     \"\"\"\n-    f = file(filename, 'r+')\n+    f = mkdir_file(filename, 'r+')\n     lines = f.readlines()\n     f.seek(0); f.truncate()\n     print >> f, line\n@@ -67,4 +75,74 @@ def insert_string(filename, line):\n def create_empty_file(name):\n     \"\"\"Creates an empty file\n     \"\"\"\n-    file(name, 'w+').close()\n+    mkdir_file(name, 'w+').close()\n+\n+def list_files_and_dirs(path):\n+    \"\"\"Return the sets of filenames and directory names in a\n+    directory.\"\"\"\n+    files, dirs = [], []\n+    for fd in os.listdir(path):\n+        full_fd = os.path.join(path, fd)\n+        if os.path.isfile(full_fd):\n+            files.append(fd)\n+        elif os.path.isdir(full_fd):\n+            dirs.append(fd)\n+    return files, dirs\n+\n+def walk_tree(basedir):\n+    \"\"\"Starting in the given directory, iterate through all its\n+    subdirectories. For each subdirectory, yield the name of the\n+    subdirectory (relative to the base directory), the list of\n+    filenames in the subdirectory, and the list of directory names in\n+    the subdirectory.\"\"\"\n+    subdirs = ['']\n+    while subdirs:\n+        subdir = subdirs.pop()\n+        files, dirs = list_files_and_dirs(os.path.join(basedir, subdir))\n+        for d in dirs:\n+            subdirs.append(os.path.join(subdir, d))\n+        yield subdir, files, dirs\n+\n+def strip_prefix(prefix, string):\n+    \"\"\"Return string, without the prefix. Blow up if string doesn't\n+    start with prefix.\"\"\"\n+    assert string.startswith(prefix)\n+    return string[len(prefix):]\n+\n+def remove_dirs(basedir, dirs):\n+    \"\"\"Starting at join(basedir, dirs), remove the directory if empty,\n+    and try the same with its parent, until we find a nonempty\n+    directory or reach basedir.\"\"\"\n+    path = dirs\n+    while path:\n+        try:\n+            os.rmdir(os.path.join(basedir, path))\n+        except OSError:\n+            return # can't remove nonempty directory\n+        path = os.path.dirname(path)\n+\n+def remove_file_and_dirs(basedir, file):\n+    \"\"\"Remove join(basedir, file), and then remove the directory it\n+    was in if empty, and try the same with its parent, until we find a\n+    nonempty directory or reach basedir.\"\"\"\n+    os.remove(os.path.join(basedir, file))\n+    remove_dirs(basedir, os.path.dirname(file))\n+\n+def create_dirs(directory):\n+    \"\"\"Create the given directory, if the path doesn't already exist.\"\"\"\n+    if directory:\n+        create_dirs(os.path.dirname(directory))\n+        try:\n+            os.mkdir(directory)\n+        except OSError, e:\n+            if e.errno != errno.EEXIST:\n+                raise e\n+\n+def rename(basedir, file1, file2):\n+    \"\"\"Rename join(basedir, file1) to join(basedir, file2), not\n+    leaving any empty directories behind and creating any directories\n+    necessary.\"\"\"\n+    full_file2 = os.path.join(basedir, file2)\n+    create_dirs(os.path.dirname(full_file2))\n+    os.rename(os.path.join(basedir, file1), full_file2)\n+    remove_dirs(basedir, os.path.dirname(file1))\n-- \n1.3.2.g639c\n\n\n-- \nKarl Hasselström, kha@treskal.com\n      www.treskal.com/kalle\n"},{"id":"19988","messageId":"20060515095520.GA11419@backpacker.hemma.treskal.com","threadId":"4145","inReplyTo":"20060510060040.GA3034@diana.vm.bytemark.co.uk","subject":"[PATCH 3/3] Tests for branch names with slashes","fromName":"Karl Hasselström","fromEmail":"kha@treskal.com","sentAt":"2006-05-15T09:55:20Z","receivedAt":"2006-05-15T09:55:20Z","isPatch":true,"sender":{"key":"kha@treskal.com","avatar":"https://gravatar.com/avatar/f0120c734b5279b345075a28521e1ac66acb20c9913ffe9bf6ae97e53f7f3f13?d=mp&s=160"},"body":"Test a number of operations on a repository that has branch names\ncontaining slashes (that is, branches living in a subdirectory of\n.git/refs/heads).\n\nSigned-off-by: Karl Hasselström <kha@treskal.com>\n\n\n---\n\n t/t0001-subdir-branches.sh |   59 ++++++++++++++++++++++++++++++++++++++++++++\n 1 files changed, 59 insertions(+), 0 deletions(-)\n create mode 100644 t/t0001-subdir-branches.sh\n\n58badd265645874165379bc841453a44b64e1906\ndiff --git a/t/t0001-subdir-branches.sh b/t/t0001-subdir-branches.sh\nnew file mode 100644\nindex 0000000..e3608de\n--- /dev/null\n+++ b/t/t0001-subdir-branches.sh\n@@ -0,0 +1,59 @@\n+#!/bin/sh\n+#\n+# Copyright (c) 2006 Karl HasselstrÃ¶m\n+#\n+\n+test_description='Branch names containing slashes\n+\n+Test a number of operations on a repository that has branch names\n+containing slashes (that is, branches living in a subdirectory of\n+.git/refs/heads).'\n+\n+. ./test-lib.sh\n+\n+test_expect_success 'Create a patch' \\\n+  'stg init &&\n+   echo \"foo\" > foo.txt &&\n+   stg add foo.txt &&\n+   stg new foo -m \"Add foo.txt\" &&\n+   stg refresh'\n+\n+test_expect_success 'Old and new id with non-slashy branch' \\\n+  'stg id foo &&\n+   stg id foo# &&\n+   stg id foo/ &&\n+   stg id foo#top &&\n+   stg id foo/top &&\n+   stg id foo@master &&\n+   stg id foo@master#top &&\n+   stg id foo@master/top'\n+\n+test_expect_success 'Clone branch to slashier name' \\\n+  'stg branch --clone x/y/z'\n+\n+test_expect_success 'Try new form of id with slashy branch' \\\n+  'stg id foo &&\n+   stg id foo# &&\n+   stg id foo#top &&\n+   stg id foo@x/y/z &&\n+   stg id foo@x/y/z#top'\n+\n+test_expect_failure 'Try old id with slashy branch' \\\n+  'stg id foo/ ||\n+   stg id foo/top ||\n+   stg id foo@x/y/z/top'\n+\n+test_expect_success 'Create patch in slashy branch' \\\n+  'echo \"bar\" >> foo.txt &&\n+   stg new bar -m \"Add another line\" &&\n+   stg refresh'\n+\n+test_expect_success 'Rename branches' \\\n+  'stg branch --rename master goo/gaa &&\n+   test ! -e .git/refs/heads/master &&\n+   stg branch --rename goo/gaa x1/x2/x3/x4 &&\n+   test ! -e .git/refs/heads/goo &&\n+   stg branch --rename x1/x2/x3/x4 servant &&\n+   test ! -e .git/refs/heads/x1'\n+\n+test_done\n-- \n1.3.2.g639c\n\n\n-- \nKarl Hasselström, kha@treskal.com\n      www.treskal.com/kalle\n"},{"id":"19990","messageId":"b0943d9e0605150322w684785d5n9b17dccda6b29ac1@mail.gmail.com","threadId":"4145","inReplyTo":"20060515095440.GA11412@backpacker.hemma.treskal.com","subject":"Re: [PATCH 2/3] Handle branch names with slashes","fromName":"Catalin Marinas","fromEmail":"catalin.marinas@gmail.com","sentAt":"2006-05-15T10:22:08Z","receivedAt":"2006-05-15T10:22:08Z","isPatch":true,"sender":{"key":"catalin.marinas@gmail.com","avatar":null},"body":"Karl,\n\nOn 15/05/06, Karl Hasselström <kha@treskal.com> wrote:\n> Teach stgit to handle branch names with slashes in them; that is,\n> branches living in a subdirectory of .git/refs/heads.\n\nThanks for the patches. I'll add them, probably tomorrow evening as\nI'm busy today.\n\n> I had to change the patch@branch/top command-line syntax to\n> patch@branch#top, in order to get sane parsing. The /top variant is\n> still available for repositories that have no slashy branches; it is\n> disabled as soon as there exists at least one subdirectory of\n> refs/heads. Preferably, this compatibility hack can be killed some\n> time in the future.\n\nThere is one problem with killing \"/\" entirely (or maybe we could use\nother character than \"#\"). I tend to write quite often \"stg diff -r\n/bottom\" to see how the whole patch looks like before refreshing. With\n\"#\", the shell ignores \"#bottom\" as being a comment.\n\nOtherwise, I'm OK with changing \"/\" with something else or just\nkeeping both (though I prefer to have a singe way of specifying it).\nIt looks like ^ and ~ are already used by GIT. It leaves us with % and\n!. Do you have any preference? The exclamation mark looks OK to me.\n\n-- \nCatalin\n"},{"id":"19992","messageId":"20060515105810.GA27077@diana.vm.bytemark.co.uk","threadId":"4145","inReplyTo":"b0943d9e0605150322w684785d5n9b17dccda6b29ac1@mail.gmail.com","subject":"Re: [PATCH 2/3] Handle branch names with slashes","fromName":"Karl Hasselström","fromEmail":"kha@treskal.com","sentAt":"2006-05-15T10:58:10Z","receivedAt":"2006-05-15T10:58:10Z","isPatch":true,"sender":{"key":"kha@treskal.com","avatar":"https://gravatar.com/avatar/f0120c734b5279b345075a28521e1ac66acb20c9913ffe9bf6ae97e53f7f3f13?d=mp&s=160"},"body":"On 2006-05-15 11:22:08 +0100, Catalin Marinas wrote:\n\n> There is one problem with killing \"/\" entirely (or maybe we could\n> use other character than \"#\"). I tend to write quite often \"stg diff\n> -r /bottom\" to see how the whole patch looks like before refreshing.\n> With \"#\", the shell ignores \"#bottom\" as being a comment.\n>\n> Otherwise, I'm OK with changing \"/\" with something else or just\n> keeping both (though I prefer to have a singe way of specifying it).\n> It looks like ^ and ~ are already used by GIT. It leaves us with %\n> and !. Do you have any preference? The exclamation mark looks OK to\n> me.\n\nAh, right. Well, I would prefer %, since ! is used for some kind of\nshell history searching, but % is not touched by the shell, I think.\n\n-- \nKarl Hasselström, kha@treskal.com\n      www.treskal.com/kalle\n"},{"id":"20048","messageId":"20060516063541.GA11218@backpacker.hemma.treskal.com","threadId":"4145","inReplyTo":"20060515105810.GA27077@diana.vm.bytemark.co.uk","subject":"[PATCH 1/2] Handle branch names with slashes","fromName":"Karl Hasselström","fromEmail":"kha@treskal.com","sentAt":"2006-05-16T06:35:41Z","receivedAt":"2006-05-16T06:35:41Z","isPatch":true,"sender":{"key":"kha@treskal.com","avatar":"https://gravatar.com/avatar/f0120c734b5279b345075a28521e1ac66acb20c9913ffe9bf6ae97e53f7f3f13?d=mp&s=160"},"body":"Teach stgit to handle branch names with slashes in them; that is,\nbranches living in a subdirectory of .git/refs/heads.\n\nI had to change the patch@branch/top command-line syntax to\npatch@branch%top, in order to get sane parsing. The /top variant is\nstill available for repositories that have no slashy branches; it is\ndisabled as soon as there exists at least one subdirectory of\nrefs/heads. Preferably, this compatibility hack can be killed some\ntime in the future.\n\nSigned-off-by: Karl Hasselström <kha@treskal.com>\n\n\n---\n\nThis is the same patch as before, but with # replaced with %.\n\n stgit/commands/branch.py |    5 ++\n stgit/commands/common.py |  103 ++++++++++++++++++++++++++--------------------\n stgit/commands/diff.py   |   12 +++--\n stgit/commands/files.py  |    4 +-\n stgit/commands/id.py     |    2 -\n stgit/commands/mail.py   |    8 ++--\n stgit/git.py             |   42 +++++++++----------\n stgit/stack.py           |   21 ++++++---\n stgit/utils.py           |   88 +++++++++++++++++++++++++++++++++++++--\n 9 files changed, 193 insertions(+), 92 deletions(-)\n\n76545c189be3a091ab62b112f1a841473600d35c\ndiff --git a/stgit/commands/branch.py b/stgit/commands/branch.py\nindex 2218bbb..d348409 100644\n--- a/stgit/commands/branch.py\n+++ b/stgit/commands/branch.py\n@@ -172,7 +172,10 @@ def func(parser, options, args):\n         if len(args) != 0:\n             parser.error('incorrect number of arguments')\n \n-        branches = os.listdir(os.path.join(basedir.get(), 'refs', 'heads'))\n+        branches = []\n+        basepath = os.path.join(basedir.get(), 'refs', 'heads')\n+        for path, files, dirs in walk_tree(basepath):\n+            branches += [os.path.join(path, f) for f in files]\n         branches.sort()\n \n         if branches:\ndiff --git a/stgit/commands/common.py b/stgit/commands/common.py\nindex c6ca514..a428dd9 100644\n--- a/stgit/commands/common.py\n+++ b/stgit/commands/common.py\n@@ -18,7 +18,7 @@ along with this program; if not, write t\n Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA\n \"\"\"\n \n-import sys, os, re\n+import sys, os, os.path, re\n from optparse import OptionParser, make_option\n \n from stgit.utils import *\n@@ -34,54 +34,69 @@ class CmdException(Exception):\n \n \n # Utility functions\n+class RevParseException(Exception):\n+    \"\"\"Revision spec parse error.\"\"\"\n+    pass\n+\n+def parse_rev(rev):\n+    \"\"\"Parse a revision specification into its\n+    patchname@branchname%patch_id parts. If no branch name has a slash\n+    in it, also accept / instead of %.\"\"\"\n+    files, dirs = list_files_and_dirs(os.path.join(basedir.get(),\n+                                                   'refs', 'heads'))\n+    if len(dirs) != 0:\n+        # We have branch names with / in them.\n+        branch_chars = '[^@%]'\n+        patch_id_mark = '%'\n+    else:\n+        # No / in branch names.\n+        branch_chars = '[^@%/]'\n+        patch_id_mark = '[/%]'\n+    patch_re = r'(?P<patch>[^@/%]+)'\n+    branch_re = r'@(?P<branch>%s+)' % branch_chars\n+    patch_id_re = r'%s(?P<patch_id>[a-z.]*)' % patch_id_mark\n+\n+    # Try %patch_id.\n+    m = re.match(r'^%s$' % patch_id_re, rev)\n+    if m:\n+        return None, None, m.group('patch_id')\n+\n+    # Try patch[@branch][%patch_id].\n+    m = re.match(r'^%s(%s)?(%s)?$' % (patch_re, branch_re, patch_id_re), rev)\n+    if m:\n+        return m.group('patch'), m.group('branch'), m.group('patch_id')\n+\n+    # No, we can't parse that.\n+    raise RevParseException\n+\n def git_id(rev):\n     \"\"\"Return the GIT id\n     \"\"\"\n     if not rev:\n         return None\n-    \n-    rev_list = rev.split('/')\n-    if len(rev_list) == 2:\n-        patch_id = rev_list[1]\n-        if not patch_id:\n-            patch_id = 'top'\n-    elif len(rev_list) == 1:\n-        patch_id = 'top'\n-    else:\n-        patch_id = None\n-\n-    patch_branch = rev_list[0].split('@')\n-    if len(patch_branch) == 1:\n-        series = crt_series\n-    elif len(patch_branch) == 2:\n-        series = stack.Series(patch_branch[1])\n-    else:\n-        raise CmdException, 'Unknown id: %s' % rev\n-\n-    patch_name = patch_branch[0]\n-    if not patch_name:\n-        patch_name = series.get_current()\n-        if not patch_name:\n-            raise CmdException, 'No patches applied'\n-\n-    # patch\n-    if patch_name in series.get_applied() \\\n-           or patch_name in series.get_unapplied():\n-        if patch_id == 'top':\n-            return series.get_patch(patch_name).get_top()\n-        elif patch_id == 'bottom':\n-            return series.get_patch(patch_name).get_bottom()\n-        # Note we can return None here.\n-        elif patch_id == 'top.old':\n-            return series.get_patch(patch_name).get_old_top()\n-        elif patch_id == 'bottom.old':\n-            return series.get_patch(patch_name).get_old_bottom()\n-\n-    # base\n-    if patch_name == 'base' and len(rev_list) == 1:\n-        return read_string(series.get_base_file())\n-\n-    # anything else failed\n+    try:\n+        patch, branch, patch_id = parse_rev(rev)\n+        if branch == None:\n+            series = crt_series\n+        else:\n+            series = stack.Series(branch)\n+        if patch == None:\n+            patch = series.get_current()\n+            if not patch:\n+                raise CmdException, 'No patches applied'\n+        if patch in series.get_applied() or patch in series.get_unapplied():\n+            if patch_id in ['top', '', None]:\n+                return series.get_patch(patch).get_top()\n+            elif patch_id == 'bottom':\n+                return series.get_patch(patch).get_bottom()\n+            elif patch_id == 'top.old':\n+                return series.get_patch(patch).get_old_top()\n+            elif patch_id == 'bottom.old':\n+                return series.get_patch(patch).get_old_bottom()\n+        if patch == 'base' and patch_id == None:\n+            return read_string(series.get_base_file())\n+    except RevParseException:\n+        pass\n     return git.rev_parse(rev + '^{commit}')\n \n def check_local_changes():\ndiff --git a/stgit/commands/diff.py b/stgit/commands/diff.py\nindex 7dc6c5d..e465e7a 100644\n--- a/stgit/commands/diff.py\n+++ b/stgit/commands/diff.py\n@@ -33,12 +33,12 @@ or a tree-ish object and another tree-is\n be given to restrict the diff output. The tree-ish object can be a\n standard git commit, tag or tree. In addition to these, the command\n also supports 'base', representing the bottom of the current stack,\n-and '[patch]/[bottom | top]' for the patch boundaries (defaulting to\n+and '[patch][%[bottom | top]]' for the patch boundaries (defaulting to\n the current one):\n \n-rev = '([patch]/[bottom | top]) | <tree-ish> | base'\n+rev = '([patch][%[bottom | top]]) | <tree-ish> | base'\n \n-If neither bottom or top are given but a '/' is present, the command\n+If neither bottom or top are given but a '%' is present, the command\n shows the specified patch (defaulting to the current one).\"\"\"\n \n options = [make_option('-r', metavar = 'rev1[:[rev2]]', dest = 'revs',\n@@ -55,10 +55,10 @@ def func(parser, options, args):\n         rev_list = options.revs.split(':')\n         rev_list_len = len(rev_list)\n         if rev_list_len == 1:\n-            if rev_list[0][-1] == '/':\n+            if rev_list[0][-1] in ['/', '%']:\n                 # the whole patch\n-                rev1 = rev_list[0] + 'bottom'\n-                rev2 = rev_list[0] + 'top'\n+                rev1 = rev_list[0][:-1] + '%bottom'\n+                rev2 = rev_list[0][:-1] + '%top'\n             else:\n                 rev1 = rev_list[0]\n                 rev2 = None\ndiff --git a/stgit/commands/files.py b/stgit/commands/files.py\nindex 0694d83..a20ce96 100644\n--- a/stgit/commands/files.py\n+++ b/stgit/commands/files.py\n@@ -53,8 +53,8 @@ def func(parser, options, args):\n     else:\n         parser.error('incorrect number of arguments')\n \n-    rev1 = git_id('%s/bottom' % patch)\n-    rev2 = git_id('%s/top' % patch)\n+    rev1 = git_id('%s%%bottom' % patch)\n+    rev2 = git_id('%s%%top' % patch)\n \n     if options.stat:\n         print git.diffstat(rev1 = rev1, rev2 = rev2)\ndiff --git a/stgit/commands/id.py b/stgit/commands/id.py\nindex 1cf6ea6..1a5938b 100644\n--- a/stgit/commands/id.py\n+++ b/stgit/commands/id.py\n@@ -28,7 +28,7 @@ usage = \"\"\"%prog [options] [id]\n \n Print the hash value of a GIT id (defaulting to HEAD). In addition to\n the standard GIT id's like heads and tags, this command also accepts\n-'base[@<branch>]' and '[<patch>[@<branch>]][/(bottom | top)]'. If no\n+'base[@<branch>]' and '[<patch>[@<branch>]][%[bottom | top]]'. If no\n 'top' or 'bottom' are passed and <patch> is a valid patch name, 'top'\n will be used by default.\"\"\"\n \ndiff --git a/stgit/commands/mail.py b/stgit/commands/mail.py\nindex 5e01ea1..0d2c260 100644\n--- a/stgit/commands/mail.py\n+++ b/stgit/commands/mail.py\n@@ -324,10 +324,10 @@ def __build_message(tmpl, patch, patch_n\n                  'shortdescr':   short_descr,\n                  'longdescr':    long_descr,\n                  'endofheaders': headers_end,\n-                 'diff':         git.diff(rev1 = git_id('%s/bottom' % patch),\n-                                          rev2 = git_id('%s/top' % patch)),\n-                 'diffstat':     git.diffstat(rev1 = git_id('%s/bottom'%patch),\n-                                              rev2 = git_id('%s/top' % patch)),\n+                 'diff':         git.diff(rev1 = git_id('%s%%bottom' % patch),\n+                                          rev2 = git_id('%s%%top' % patch)),\n+                 'diffstat':     git.diffstat(rev1 = git_id('%s%%bottom'%patch),\n+                                              rev2 = git_id('%s%%top' % patch)),\n                  'date':         email.Utils.formatdate(localtime = True),\n                  'version':      version_str,\n                  'patchnr':      patch_nr_str,\ndiff --git a/stgit/git.py b/stgit/git.py\nindex 2884f36..716609c 100644\n--- a/stgit/git.py\n+++ b/stgit/git.py\n@@ -225,7 +225,8 @@ def get_head():\n def get_head_file():\n     \"\"\"Returns the name of the file pointed to by the HEAD link\n     \"\"\"\n-    return os.path.basename(_output_one_line('git-symbolic-ref HEAD'))\n+    return strip_prefix('refs/heads/',\n+                        _output_one_line('git-symbolic-ref HEAD'))\n \n def set_head_file(ref):\n     \"\"\"Resets HEAD to point to a new ref\n@@ -233,7 +234,8 @@ def set_head_file(ref):\n     # head cache flushing is needed since we might have a different value\n     # in the new head\n     __clear_head_cache()\n-    if __run('git-symbolic-ref HEAD', [ref]) != 0:\n+    if __run('git-symbolic-ref HEAD',\n+             [os.path.join('refs', 'heads', ref)]) != 0:\n         raise GitException, 'Could not set head to \"%s\"' % ref\n \n def __set_head(val):\n@@ -272,6 +274,7 @@ def rev_parse(git_id):\n def branch_exists(branch):\n     \"\"\"Existence check for the named branch\n     \"\"\"\n+    branch = os.path.join('refs', 'heads', branch)\n     for line in _output_lines('git-rev-parse --symbolic --all 2>&1'):\n         if line.strip() == branch:\n             return True\n@@ -282,12 +285,11 @@ def branch_exists(branch):\n def create_branch(new_branch, tree_id = None):\n     \"\"\"Create a new branch in the git repository\n     \"\"\"\n-    new_head = os.path.join('refs', 'heads', new_branch)\n-    if branch_exists(new_head):\n+    if branch_exists(new_branch):\n         raise GitException, 'Branch \"%s\" already exists' % new_branch\n \n     current_head = get_head()\n-    set_head_file(new_head)\n+    set_head_file(new_branch)\n     __set_head(current_head)\n \n     # a checkout isn't needed if new branch points to the current head\n@@ -297,22 +299,22 @@ def create_branch(new_branch, tree_id = \n     if os.path.isfile(os.path.join(basedir.get(), 'MERGE_HEAD')):\n         os.remove(os.path.join(basedir.get(), 'MERGE_HEAD'))\n \n-def switch_branch(name):\n+def switch_branch(new_branch):\n     \"\"\"Switch to a git branch\n     \"\"\"\n     global __head\n \n-    new_head = os.path.join('refs', 'heads', name)\n-    if not branch_exists(new_head):\n-        raise GitException, 'Branch \"%s\" does not exist' % name\n+    if not branch_exists(new_branch):\n+        raise GitException, 'Branch \"%s\" does not exist' % new_branch\n \n-    tree_id = rev_parse(new_head + '^{commit}')\n+    tree_id = rev_parse(os.path.join('refs', 'heads', new_branch)\n+                        + '^{commit}')\n     if tree_id != get_head():\n         refresh_index()\n         if __run('git-read-tree -u -m', [get_head(), tree_id]) != 0:\n             raise GitException, 'git-read-tree failed (local changes maybe?)'\n         __head = tree_id\n-    set_head_file(new_head)\n+    set_head_file(new_branch)\n \n     if os.path.isfile(os.path.join(basedir.get(), 'MERGE_HEAD')):\n         os.remove(os.path.join(basedir.get(), 'MERGE_HEAD'))\n@@ -320,25 +322,23 @@ def switch_branch(name):\n def delete_branch(name):\n     \"\"\"Delete a git branch\n     \"\"\"\n-    branch_head = os.path.join('refs', 'heads', name)\n-    if not branch_exists(branch_head):\n+    if not branch_exists(name):\n         raise GitException, 'Branch \"%s\" does not exist' % name\n-    os.remove(os.path.join(basedir.get(), branch_head))\n+    remove_file_and_dirs(os.path.join(basedir.get(), 'refs', 'heads'),\n+                         name)\n \n def rename_branch(from_name, to_name):\n     \"\"\"Rename a git branch\n     \"\"\"\n-    from_head = os.path.join('refs', 'heads', from_name)\n-    if not branch_exists(from_head):\n+    if not branch_exists(from_name):\n         raise GitException, 'Branch \"%s\" does not exist' % from_name\n-    to_head = os.path.join('refs', 'heads', to_name)\n-    if branch_exists(to_head):\n+    if branch_exists(to_name):\n         raise GitException, 'Branch \"%s\" already exists' % to_name\n \n     if get_head_file() == from_name:\n-        set_head_file(to_head)\n-    os.rename(os.path.join(basedir.get(), from_head), \\\n-              os.path.join(basedir.get(), to_head))\n+        set_head_file(to_name)\n+    rename(os.path.join(basedir.get(), 'refs', 'heads'),\n+           from_name, to_name)\n \n def add(names):\n     \"\"\"Add the files or recursively add the directory contents\ndiff --git a/stgit/stack.py b/stgit/stack.py\nindex f83161b..49b50e7 100644\n--- a/stgit/stack.py\n+++ b/stgit/stack.py\n@@ -443,8 +443,7 @@ class Series:\n \n         os.makedirs(self.__patch_dir)\n \n-        if not os.path.isdir(bases_dir):\n-            os.makedirs(bases_dir)\n+        create_dirs(bases_dir)\n \n         create_empty_file(self.__applied_file)\n         create_empty_file(self.__unapplied_file)\n@@ -502,11 +501,14 @@ class Series:\n         git.rename_branch(self.__name, to_name)\n \n         if os.path.isdir(self.__series_dir):\n-            os.rename(self.__series_dir, to_stack.__series_dir)\n+            rename(os.path.join(self.__base_dir, 'patches'),\n+                   self.__name, to_stack.__name)\n         if os.path.exists(self.__base_file):\n-            os.rename(self.__base_file, to_stack.__base_file)\n+            rename(os.path.join(self.__base_dir, 'refs', 'bases'),\n+                   self.__name, to_stack.__name)\n         if os.path.exists(self.__refs_dir):\n-            os.rename(self.__refs_dir, to_stack.__refs_dir)\n+            rename(os.path.join(self.__base_dir, 'refs', 'patches'),\n+                   self.__name, to_stack.__name)\n \n         self.__init__(to_name)\n \n@@ -560,16 +562,19 @@ class Series:\n             else:\n                 print 'Patch directory %s is not empty.' % self.__name\n             if not os.listdir(self.__series_dir):\n-                os.rmdir(self.__series_dir)\n+                remove_dirs(os.path.join(self.__base_dir, 'patches'),\n+                            self.__name)\n             else:\n                 print 'Series directory %s is not empty.' % self.__name\n             if not os.listdir(self.__refs_dir):\n-                os.rmdir(self.__refs_dir)\n+                remove_dirs(os.path.join(self.__base_dir, 'refs', 'patches'),\n+                            self.__name)\n             else:\n                 print 'Refs directory %s is not empty.' % self.__refs_dir\n \n         if os.path.exists(self.__base_file):\n-            os.remove(self.__base_file)\n+            remove_file_and_dirs(\n+                os.path.join(self.__base_dir, 'refs', 'bases'), self.__name)\n \n     def refresh_patch(self, files = None, message = None, edit = False,\n                       show_patch = False,\ndiff --git a/stgit/utils.py b/stgit/utils.py\nindex 5749b3b..68b8f58 100644\n--- a/stgit/utils.py\n+++ b/stgit/utils.py\n@@ -1,6 +1,8 @@\n \"\"\"Common utility functions\n \"\"\"\n \n+import errno, os, os.path\n+\n __copyright__ = \"\"\"\n Copyright (C) 2005, Catalin Marinas <catalin.marinas@gmail.com>\n \n@@ -18,6 +20,12 @@ along with this program; if not, write t\n Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA\n \"\"\"\n \n+def mkdir_file(filename, mode):\n+    \"\"\"Opens filename with the given mode, creating the directory it's\n+    in if it doesn't already exist.\"\"\"\n+    create_dirs(os.path.dirname(filename))\n+    return file(filename, mode)\n+\n def read_string(filename, multiline = False):\n     \"\"\"Reads the first line from a file\n     \"\"\"\n@@ -32,7 +40,7 @@ def read_string(filename, multiline = Fa\n def write_string(filename, line, multiline = False):\n     \"\"\"Writes 'line' to file and truncates it\n     \"\"\"\n-    f = file(filename, 'w+')\n+    f = mkdir_file(filename, 'w+')\n     if multiline:\n         f.write(line)\n     else:\n@@ -42,7 +50,7 @@ def write_string(filename, line, multili\n def append_strings(filename, lines):\n     \"\"\"Appends 'lines' sequence to file\n     \"\"\"\n-    f = file(filename, 'a+')\n+    f = mkdir_file(filename, 'a+')\n     for line in lines:\n         print >> f, line\n     f.close()\n@@ -50,14 +58,14 @@ def append_strings(filename, lines):\n def append_string(filename, line):\n     \"\"\"Appends 'line' to file\n     \"\"\"\n-    f = file(filename, 'a+')\n+    f = mkdir_file(filename, 'a+')\n     print >> f, line\n     f.close()\n \n def insert_string(filename, line):\n     \"\"\"Inserts 'line' at the beginning of the file\n     \"\"\"\n-    f = file(filename, 'r+')\n+    f = mkdir_file(filename, 'r+')\n     lines = f.readlines()\n     f.seek(0); f.truncate()\n     print >> f, line\n@@ -67,4 +75,74 @@ def insert_string(filename, line):\n def create_empty_file(name):\n     \"\"\"Creates an empty file\n     \"\"\"\n-    file(name, 'w+').close()\n+    mkdir_file(name, 'w+').close()\n+\n+def list_files_and_dirs(path):\n+    \"\"\"Return the sets of filenames and directory names in a\n+    directory.\"\"\"\n+    files, dirs = [], []\n+    for fd in os.listdir(path):\n+        full_fd = os.path.join(path, fd)\n+        if os.path.isfile(full_fd):\n+            files.append(fd)\n+        elif os.path.isdir(full_fd):\n+            dirs.append(fd)\n+    return files, dirs\n+\n+def walk_tree(basedir):\n+    \"\"\"Starting in the given directory, iterate through all its\n+    subdirectories. For each subdirectory, yield the name of the\n+    subdirectory (relative to the base directory), the list of\n+    filenames in the subdirectory, and the list of directory names in\n+    the subdirectory.\"\"\"\n+    subdirs = ['']\n+    while subdirs:\n+        subdir = subdirs.pop()\n+        files, dirs = list_files_and_dirs(os.path.join(basedir, subdir))\n+        for d in dirs:\n+            subdirs.append(os.path.join(subdir, d))\n+        yield subdir, files, dirs\n+\n+def strip_prefix(prefix, string):\n+    \"\"\"Return string, without the prefix. Blow up if string doesn't\n+    start with prefix.\"\"\"\n+    assert string.startswith(prefix)\n+    return string[len(prefix):]\n+\n+def remove_dirs(basedir, dirs):\n+    \"\"\"Starting at join(basedir, dirs), remove the directory if empty,\n+    and try the same with its parent, until we find a nonempty\n+    directory or reach basedir.\"\"\"\n+    path = dirs\n+    while path:\n+        try:\n+            os.rmdir(os.path.join(basedir, path))\n+        except OSError:\n+            return # can't remove nonempty directory\n+        path = os.path.dirname(path)\n+\n+def remove_file_and_dirs(basedir, file):\n+    \"\"\"Remove join(basedir, file), and then remove the directory it\n+    was in if empty, and try the same with its parent, until we find a\n+    nonempty directory or reach basedir.\"\"\"\n+    os.remove(os.path.join(basedir, file))\n+    remove_dirs(basedir, os.path.dirname(file))\n+\n+def create_dirs(directory):\n+    \"\"\"Create the given directory, if the path doesn't already exist.\"\"\"\n+    if directory:\n+        create_dirs(os.path.dirname(directory))\n+        try:\n+            os.mkdir(directory)\n+        except OSError, e:\n+            if e.errno != errno.EEXIST:\n+                raise e\n+\n+def rename(basedir, file1, file2):\n+    \"\"\"Rename join(basedir, file1) to join(basedir, file2), not\n+    leaving any empty directories behind and creating any directories\n+    necessary.\"\"\"\n+    full_file2 = os.path.join(basedir, file2)\n+    create_dirs(os.path.dirname(full_file2))\n+    os.rename(os.path.join(basedir, file1), full_file2)\n+    remove_dirs(basedir, os.path.dirname(file1))\n-- \n1.3.2.g639c\n\n\n-- \nKarl Hasselström, kha@treskal.com\n      www.treskal.com/kalle\n"},{"id":"20049","messageId":"20060516063716.GA11230@backpacker.hemma.treskal.com","threadId":"4145","inReplyTo":"20060515105810.GA27077@diana.vm.bytemark.co.uk","subject":"[PATCH 2/2] Tests for branch names with slashes","fromName":"Karl Hasselström","fromEmail":"kha@treskal.com","sentAt":"2006-05-16T06:37:16Z","receivedAt":"2006-05-16T06:37:16Z","isPatch":true,"sender":{"key":"kha@treskal.com","avatar":"https://gravatar.com/avatar/f0120c734b5279b345075a28521e1ac66acb20c9913ffe9bf6ae97e53f7f3f13?d=mp&s=160"},"body":"Test a number of operations on a repository that has branch names\ncontaining slashes (that is, branches living in a subdirectory of\n.git/refs/heads).\n\nSigned-off-by: Karl Hasselström <kha@treskal.com>\n\n\n---\n\nThe test also had to be changed to use % instead of #.\n\n t/t0001-subdir-branches.sh |   59 ++++++++++++++++++++++++++++++++++++++++++++\n 1 files changed, 59 insertions(+), 0 deletions(-)\n create mode 100644 t/t0001-subdir-branches.sh\n\n2278d3988ae3fee7624aac6db6bd92677173749f\ndiff --git a/t/t0001-subdir-branches.sh b/t/t0001-subdir-branches.sh\nnew file mode 100644\nindex 0000000..64f583c\n--- /dev/null\n+++ b/t/t0001-subdir-branches.sh\n@@ -0,0 +1,59 @@\n+#!/bin/sh\n+#\n+# Copyright (c) 2006 Karl Hasselström\n+#\n+\n+test_description='Branch names containing slashes\n+\n+Test a number of operations on a repository that has branch names\n+containing slashes (that is, branches living in a subdirectory of\n+.git/refs/heads).'\n+\n+. ./test-lib.sh\n+\n+test_expect_success 'Create a patch' \\\n+  'stg init &&\n+   echo \"foo\" > foo.txt &&\n+   stg add foo.txt &&\n+   stg new foo -m \"Add foo.txt\" &&\n+   stg refresh'\n+\n+test_expect_success 'Old and new id with non-slashy branch' \\\n+  'stg id foo &&\n+   stg id foo% &&\n+   stg id foo/ &&\n+   stg id foo%top &&\n+   stg id foo/top &&\n+   stg id foo@master &&\n+   stg id foo@master%top &&\n+   stg id foo@master/top'\n+\n+test_expect_success 'Clone branch to slashier name' \\\n+  'stg branch --clone x/y/z'\n+\n+test_expect_success 'Try new form of id with slashy branch' \\\n+  'stg id foo &&\n+   stg id foo% &&\n+   stg id foo%top &&\n+   stg id foo@x/y/z &&\n+   stg id foo@x/y/z%top'\n+\n+test_expect_failure 'Try old id with slashy branch' \\\n+  'stg id foo/ ||\n+   stg id foo/top ||\n+   stg id foo@x/y/z/top'\n+\n+test_expect_success 'Create patch in slashy branch' \\\n+  'echo \"bar\" >> foo.txt &&\n+   stg new bar -m \"Add another line\" &&\n+   stg refresh'\n+\n+test_expect_success 'Rename branches' \\\n+  'stg branch --rename master goo/gaa &&\n+   test ! -e .git/refs/heads/master &&\n+   stg branch --rename goo/gaa x1/x2/x3/x4 &&\n+   test ! -e .git/refs/heads/goo &&\n+   stg branch --rename x1/x2/x3/x4 servant &&\n+   test ! -e .git/refs/heads/x1'\n+\n+test_done\n-- \n1.3.2.g639c\n\n\n-- \nKarl Hasselström, kha@treskal.com\n      www.treskal.com/kalle\n"},{"id":"20050","messageId":"7v64k6ea8r.fsf@assigned-by-dhcp.cox.net","threadId":"4145","inReplyTo":"20060516063541.GA11218@backpacker.hemma.treskal.com","subject":"Re: [PATCH 1/2] Handle branch names with slashes","fromName":"Junio C Hamano","fromEmail":"junkio@cox.net","sentAt":"2006-05-16T06:48:04Z","receivedAt":"2006-05-16T06:48:04Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Karl Hasselström <kha@treskal.com> writes:\n\n> Teach stgit to handle branch names with slashes in them; that is,\n> branches living in a subdirectory of .git/refs/heads.\n>\n> I had to change the patch@branch/top command-line syntax to\n> patch@branch%top, in order to get sane parsing. The /top variant is\n> still available for repositories that have no slashy branches; it is\n> disabled as soon as there exists at least one subdirectory of\n> refs/heads. Preferably, this compatibility hack can be killed some\n> time in the future.\n\nI wonder if using double-slashes is an easier alternative to\ntype than '%', like \"patch@branch//top\".  That way, you do not\nhave to forbid per-cent sign in branch names.\n"},{"id":"20054","messageId":"20060516074504.GA27234@diana.vm.bytemark.co.uk","threadId":"4145","inReplyTo":"7v64k6ea8r.fsf@assigned-by-dhcp.cox.net","subject":"Re: [PATCH 1/2] Handle branch names with slashes","fromName":"Karl Hasselström","fromEmail":"kha@treskal.com","sentAt":"2006-05-16T07:45:04Z","receivedAt":"2006-05-16T07:45:04Z","isPatch":true,"sender":{"key":"kha@treskal.com","avatar":"https://gravatar.com/avatar/f0120c734b5279b345075a28521e1ac66acb20c9913ffe9bf6ae97e53f7f3f13?d=mp&s=160"},"body":"On 2006-05-15 23:48:04 -0700, Junio C Hamano wrote:\n\n> Karl Hasselström <kha@treskal.com> writes:\n>\n> > I had to change the patch@branch/top command-line syntax to\n> > patch@branch%top, in order to get sane parsing. The /top variant\n> > is still available for repositories that have no slashy branches;\n> > it is disabled as soon as there exists at least one subdirectory\n> > of refs/heads. Preferably, this compatibility hack can be killed\n> > some time in the future.\n>\n> I wonder if using double-slashes is an easier alternative to type\n> than '%', like \"patch@branch//top\". That way, you do not have to\n> forbid per-cent sign in branch names.\n\nGood argument. And // does look slightly better than %, too. But I'll\nwait a few days this time, or else someone will surely come along with\nan even better idea. :-)\n\n-- \nKarl Hasselström, kha@treskal.com\n      www.treskal.com/kalle\n"},{"id":"20150","messageId":"b0943d9e0605170818j380a6daaif42f2acbf627104f@mail.gmail.com","threadId":"4145","inReplyTo":"20060516074504.GA27234@diana.vm.bytemark.co.uk","subject":"Re: [PATCH 1/2] Handle branch names with slashes","fromName":"Catalin Marinas","fromEmail":"catalin.marinas@gmail.com","sentAt":"2006-05-17T15:18:49Z","receivedAt":"2006-05-17T15:18:49Z","isPatch":true,"sender":{"key":"catalin.marinas@gmail.com","avatar":null},"body":"On 16/05/06, Karl Hasselström <kha@treskal.com> wrote:\n> On 2006-05-15 23:48:04 -0700, Junio C Hamano wrote:\n>\n> > Karl Hasselström <kha@treskal.com> writes:\n> >\n> > > I had to change the patch@branch/top command-line syntax to\n> > > patch@branch%top, in order to get sane parsing. The /top variant\n> > > is still available for repositories that have no slashy branches;\n> > > it is disabled as soon as there exists at least one subdirectory\n> > > of refs/heads. Preferably, this compatibility hack can be killed\n> > > some time in the future.\n> >\n> > I wonder if using double-slashes is an easier alternative to type\n> > than '%', like \"patch@branch//top\". That way, you do not have to\n> > forbid per-cent sign in branch names.\n>\n> Good argument. And // does look slightly better than %, too. But I'll\n> wait a few days this time, or else someone will surely come along with\n> an even better idea. :-)\n\nI also thing // is a better solution than %.\n\nThere is another option, a bit more complicated, \"patch@branch:top\".\nThis would imply replacing : with .. in the current handling of the -r\noption in diff/mail/export but it would make the commit or patch\nranges look more like GIT's arguments.\n\n-- \nCatalin\n"},{"id":"20207","messageId":"20060518064214.GA10390@backpacker.hemma.treskal.com","threadId":"4145","inReplyTo":"20060516074504.GA27234@diana.vm.bytemark.co.uk","subject":"[PATCH 1/2] Handle branch names with slashes","fromName":"Karl Hasselström","fromEmail":"kha@treskal.com","sentAt":"2006-05-18T06:42:14Z","receivedAt":"2006-05-18T06:42:14Z","isPatch":true,"sender":{"key":"kha@treskal.com","avatar":"https://gravatar.com/avatar/f0120c734b5279b345075a28521e1ac66acb20c9913ffe9bf6ae97e53f7f3f13?d=mp&s=160"},"body":"Teach stgit to handle branch names with slashes in them; that is,\nbranches living in a subdirectory of .git/refs/heads.\n\nI had to change the patch@branch/top command-line syntax to\npatch@branch%top, in order to get sane parsing. The /top variant is\nstill available for repositories that have no slashy branches; it is\ndisabled as soon as there exists at least one subdirectory of\nrefs/heads. Preferably, this compatibility hack can be killed some\ntime in the future.\n\nSigned-off-by: Karl Hasselström <kha@treskal.com>\n\n\n---\n\n stgit/commands/branch.py |    5 ++\n stgit/commands/common.py |  108 +++++++++++++++++++++++++++-------------------\n stgit/commands/diff.py   |   16 ++++---\n stgit/commands/files.py  |    4 +-\n stgit/commands/id.py     |    2 -\n stgit/commands/mail.py   |    8 ++-\n stgit/git.py             |   42 +++++++++---------\n stgit/stack.py           |   21 ++++++---\n stgit/utils.py           |   88 +++++++++++++++++++++++++++++++++++--\n 9 files changed, 202 insertions(+), 92 deletions(-)\n\nfb5b39c8867474f4b23f0b52c4090c76aee6b1e8\ndiff --git a/stgit/commands/branch.py b/stgit/commands/branch.py\nindex 2218bbb..d348409 100644\n--- a/stgit/commands/branch.py\n+++ b/stgit/commands/branch.py\n@@ -172,7 +172,10 @@ def func(parser, options, args):\n         if len(args) != 0:\n             parser.error('incorrect number of arguments')\n \n-        branches = os.listdir(os.path.join(basedir.get(), 'refs', 'heads'))\n+        branches = []\n+        basepath = os.path.join(basedir.get(), 'refs', 'heads')\n+        for path, files, dirs in walk_tree(basepath):\n+            branches += [os.path.join(path, f) for f in files]\n         branches.sort()\n \n         if branches:\ndiff --git a/stgit/commands/common.py b/stgit/commands/common.py\nindex c6ca514..93344aa 100644\n--- a/stgit/commands/common.py\n+++ b/stgit/commands/common.py\n@@ -18,7 +18,7 @@ along with this program; if not, write t\n Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA\n \"\"\"\n \n-import sys, os, re\n+import sys, os, os.path, re\n from optparse import OptionParser, make_option\n \n from stgit.utils import *\n@@ -34,54 +34,74 @@ class CmdException(Exception):\n \n \n # Utility functions\n+class RevParseException(Exception):\n+    \"\"\"Revision spec parse error.\"\"\"\n+    pass\n+\n+def parse_rev(rev):\n+    \"\"\"Parse a revision specification into its\n+    patchname@branchname//patch_id parts. If no branch name has a slash\n+    in it, also accept / instead of //.\"\"\"\n+    files, dirs = list_files_and_dirs(os.path.join(basedir.get(),\n+                                                   'refs', 'heads'))\n+    if len(dirs) != 0:\n+        # We have branch names with / in them.\n+        branch_chars = r'[^@]'\n+        patch_id_mark = r'//'\n+    else:\n+        # No / in branch names.\n+        branch_chars = r'[^@%/]'\n+        patch_id_mark = r'(/|//)'\n+    patch_re = r'(?P<patch>[^@/]+)'\n+    branch_re = r'@(?P<branch>%s+)' % branch_chars\n+    patch_id_re = r'%s(?P<patch_id>[a-z.]*)' % patch_id_mark\n+\n+    # Try //patch_id.\n+    m = re.match(r'^%s$' % patch_id_re, rev)\n+    if m:\n+        return None, None, m.group('patch_id')\n+\n+    # Try path[@branch]//patch_id.\n+    m = re.match(r'^%s(%s)?%s$' % (patch_re, branch_re, patch_id_re), rev)\n+    if m:\n+        return m.group('patch'), m.group('branch'), m.group('patch_id')\n+\n+    # Try patch[@branch].\n+    m = re.match(r'^%s(%s)?$' % (patch_re, branch_re), rev)\n+    if m:\n+        return m.group('patch'), m.group('branch'), None\n+\n+    # No, we can't parse that.\n+    raise RevParseException\n+\n def git_id(rev):\n     \"\"\"Return the GIT id\n     \"\"\"\n     if not rev:\n         return None\n-    \n-    rev_list = rev.split('/')\n-    if len(rev_list) == 2:\n-        patch_id = rev_list[1]\n-        if not patch_id:\n-            patch_id = 'top'\n-    elif len(rev_list) == 1:\n-        patch_id = 'top'\n-    else:\n-        patch_id = None\n-\n-    patch_branch = rev_list[0].split('@')\n-    if len(patch_branch) == 1:\n-        series = crt_series\n-    elif len(patch_branch) == 2:\n-        series = stack.Series(patch_branch[1])\n-    else:\n-        raise CmdException, 'Unknown id: %s' % rev\n-\n-    patch_name = patch_branch[0]\n-    if not patch_name:\n-        patch_name = series.get_current()\n-        if not patch_name:\n-            raise CmdException, 'No patches applied'\n-\n-    # patch\n-    if patch_name in series.get_applied() \\\n-           or patch_name in series.get_unapplied():\n-        if patch_id == 'top':\n-            return series.get_patch(patch_name).get_top()\n-        elif patch_id == 'bottom':\n-            return series.get_patch(patch_name).get_bottom()\n-        # Note we can return None here.\n-        elif patch_id == 'top.old':\n-            return series.get_patch(patch_name).get_old_top()\n-        elif patch_id == 'bottom.old':\n-            return series.get_patch(patch_name).get_old_bottom()\n-\n-    # base\n-    if patch_name == 'base' and len(rev_list) == 1:\n-        return read_string(series.get_base_file())\n-\n-    # anything else failed\n+    try:\n+        patch, branch, patch_id = parse_rev(rev)\n+        if branch == None:\n+            series = crt_series\n+        else:\n+            series = stack.Series(branch)\n+        if patch == None:\n+            patch = series.get_current()\n+            if not patch:\n+                raise CmdException, 'No patches applied'\n+        if patch in series.get_applied() or patch in series.get_unapplied():\n+            if patch_id in ['top', '', None]:\n+                return series.get_patch(patch).get_top()\n+            elif patch_id == 'bottom':\n+                return series.get_patch(patch).get_bottom()\n+            elif patch_id == 'top.old':\n+                return series.get_patch(patch).get_old_top()\n+            elif patch_id == 'bottom.old':\n+                return series.get_patch(patch).get_old_bottom()\n+        if patch == 'base' and patch_id == None:\n+            return read_string(series.get_base_file())\n+    except RevParseException:\n+        pass\n     return git.rev_parse(rev + '^{commit}')\n \n def check_local_changes():\ndiff --git a/stgit/commands/diff.py b/stgit/commands/diff.py\nindex 7dc6c5d..d765784 100644\n--- a/stgit/commands/diff.py\n+++ b/stgit/commands/diff.py\n@@ -33,12 +33,12 @@ or a tree-ish object and another tree-is\n be given to restrict the diff output. The tree-ish object can be a\n standard git commit, tag or tree. In addition to these, the command\n also supports 'base', representing the bottom of the current stack,\n-and '[patch]/[bottom | top]' for the patch boundaries (defaulting to\n+and '[patch][//[bottom | top]]' for the patch boundaries (defaulting to\n the current one):\n \n-rev = '([patch]/[bottom | top]) | <tree-ish> | base'\n+rev = '([patch][//[bottom | top]]) | <tree-ish> | base'\n \n-If neither bottom or top are given but a '/' is present, the command\n+If neither bottom nor top are given but a '//' is present, the command\n shows the specified patch (defaulting to the current one).\"\"\"\n \n options = [make_option('-r', metavar = 'rev1[:[rev2]]', dest = 'revs',\n@@ -55,10 +55,14 @@ def func(parser, options, args):\n         rev_list = options.revs.split(':')\n         rev_list_len = len(rev_list)\n         if rev_list_len == 1:\n-            if rev_list[0][-1] == '/':\n+            rev = rev_list[0]\n+            if rev[-1] == '/':\n                 # the whole patch\n-                rev1 = rev_list[0] + 'bottom'\n-                rev2 = rev_list[0] + 'top'\n+                rev = rev[:-1]\n+                if rev[-1] == '/':\n+                    rev = rev[:-1]\n+                rev1 = rev + '//bottom'\n+                rev2 = rev + '//top'\n             else:\n                 rev1 = rev_list[0]\n                 rev2 = None\ndiff --git a/stgit/commands/files.py b/stgit/commands/files.py\nindex 0694d83..b33bd2a 100644\n--- a/stgit/commands/files.py\n+++ b/stgit/commands/files.py\n@@ -53,8 +53,8 @@ def func(parser, options, args):\n     else:\n         parser.error('incorrect number of arguments')\n \n-    rev1 = git_id('%s/bottom' % patch)\n-    rev2 = git_id('%s/top' % patch)\n+    rev1 = git_id('%s//bottom' % patch)\n+    rev2 = git_id('%s//top' % patch)\n \n     if options.stat:\n         print git.diffstat(rev1 = rev1, rev2 = rev2)\ndiff --git a/stgit/commands/id.py b/stgit/commands/id.py\nindex 1cf6ea6..284589a 100644\n--- a/stgit/commands/id.py\n+++ b/stgit/commands/id.py\n@@ -28,7 +28,7 @@ usage = \"\"\"%prog [options] [id]\n \n Print the hash value of a GIT id (defaulting to HEAD). In addition to\n the standard GIT id's like heads and tags, this command also accepts\n-'base[@<branch>]' and '[<patch>[@<branch>]][/(bottom | top)]'. If no\n+'base[@<branch>]' and '[<patch>[@<branch>]][//[bottom | top]]'. If no\n 'top' or 'bottom' are passed and <patch> is a valid patch name, 'top'\n will be used by default.\"\"\"\n \ndiff --git a/stgit/commands/mail.py b/stgit/commands/mail.py\nindex 5e01ea1..3928b81 100644\n--- a/stgit/commands/mail.py\n+++ b/stgit/commands/mail.py\n@@ -324,10 +324,10 @@ def __build_message(tmpl, patch, patch_n\n                  'shortdescr':   short_descr,\n                  'longdescr':    long_descr,\n                  'endofheaders': headers_end,\n-                 'diff':         git.diff(rev1 = git_id('%s/bottom' % patch),\n-                                          rev2 = git_id('%s/top' % patch)),\n-                 'diffstat':     git.diffstat(rev1 = git_id('%s/bottom'%patch),\n-                                              rev2 = git_id('%s/top' % patch)),\n+                 'diff':         git.diff(rev1 = git_id('%s//bottom' % patch),\n+                                          rev2 = git_id('%s//top' % patch)),\n+                 'diffstat':     git.diffstat(rev1 = git_id('%s//bottom'%patch),\n+                                              rev2 = git_id('%s//top' % patch)),\n                  'date':         email.Utils.formatdate(localtime = True),\n                  'version':      version_str,\n                  'patchnr':      patch_nr_str,\ndiff --git a/stgit/git.py b/stgit/git.py\nindex 2884f36..716609c 100644\n--- a/stgit/git.py\n+++ b/stgit/git.py\n@@ -225,7 +225,8 @@ def get_head():\n def get_head_file():\n     \"\"\"Returns the name of the file pointed to by the HEAD link\n     \"\"\"\n-    return os.path.basename(_output_one_line('git-symbolic-ref HEAD'))\n+    return strip_prefix('refs/heads/',\n+                        _output_one_line('git-symbolic-ref HEAD'))\n \n def set_head_file(ref):\n     \"\"\"Resets HEAD to point to a new ref\n@@ -233,7 +234,8 @@ def set_head_file(ref):\n     # head cache flushing is needed since we might have a different value\n     # in the new head\n     __clear_head_cache()\n-    if __run('git-symbolic-ref HEAD', [ref]) != 0:\n+    if __run('git-symbolic-ref HEAD',\n+             [os.path.join('refs', 'heads', ref)]) != 0:\n         raise GitException, 'Could not set head to \"%s\"' % ref\n \n def __set_head(val):\n@@ -272,6 +274,7 @@ def rev_parse(git_id):\n def branch_exists(branch):\n     \"\"\"Existence check for the named branch\n     \"\"\"\n+    branch = os.path.join('refs', 'heads', branch)\n     for line in _output_lines('git-rev-parse --symbolic --all 2>&1'):\n         if line.strip() == branch:\n             return True\n@@ -282,12 +285,11 @@ def branch_exists(branch):\n def create_branch(new_branch, tree_id = None):\n     \"\"\"Create a new branch in the git repository\n     \"\"\"\n-    new_head = os.path.join('refs', 'heads', new_branch)\n-    if branch_exists(new_head):\n+    if branch_exists(new_branch):\n         raise GitException, 'Branch \"%s\" already exists' % new_branch\n \n     current_head = get_head()\n-    set_head_file(new_head)\n+    set_head_file(new_branch)\n     __set_head(current_head)\n \n     # a checkout isn't needed if new branch points to the current head\n@@ -297,22 +299,22 @@ def create_branch(new_branch, tree_id = \n     if os.path.isfile(os.path.join(basedir.get(), 'MERGE_HEAD')):\n         os.remove(os.path.join(basedir.get(), 'MERGE_HEAD'))\n \n-def switch_branch(name):\n+def switch_branch(new_branch):\n     \"\"\"Switch to a git branch\n     \"\"\"\n     global __head\n \n-    new_head = os.path.join('refs', 'heads', name)\n-    if not branch_exists(new_head):\n-        raise GitException, 'Branch \"%s\" does not exist' % name\n+    if not branch_exists(new_branch):\n+        raise GitException, 'Branch \"%s\" does not exist' % new_branch\n \n-    tree_id = rev_parse(new_head + '^{commit}')\n+    tree_id = rev_parse(os.path.join('refs', 'heads', new_branch)\n+                        + '^{commit}')\n     if tree_id != get_head():\n         refresh_index()\n         if __run('git-read-tree -u -m', [get_head(), tree_id]) != 0:\n             raise GitException, 'git-read-tree failed (local changes maybe?)'\n         __head = tree_id\n-    set_head_file(new_head)\n+    set_head_file(new_branch)\n \n     if os.path.isfile(os.path.join(basedir.get(), 'MERGE_HEAD')):\n         os.remove(os.path.join(basedir.get(), 'MERGE_HEAD'))\n@@ -320,25 +322,23 @@ def switch_branch(name):\n def delete_branch(name):\n     \"\"\"Delete a git branch\n     \"\"\"\n-    branch_head = os.path.join('refs', 'heads', name)\n-    if not branch_exists(branch_head):\n+    if not branch_exists(name):\n         raise GitException, 'Branch \"%s\" does not exist' % name\n-    os.remove(os.path.join(basedir.get(), branch_head))\n+    remove_file_and_dirs(os.path.join(basedir.get(), 'refs', 'heads'),\n+                         name)\n \n def rename_branch(from_name, to_name):\n     \"\"\"Rename a git branch\n     \"\"\"\n-    from_head = os.path.join('refs', 'heads', from_name)\n-    if not branch_exists(from_head):\n+    if not branch_exists(from_name):\n         raise GitException, 'Branch \"%s\" does not exist' % from_name\n-    to_head = os.path.join('refs', 'heads', to_name)\n-    if branch_exists(to_head):\n+    if branch_exists(to_name):\n         raise GitException, 'Branch \"%s\" already exists' % to_name\n \n     if get_head_file() == from_name:\n-        set_head_file(to_head)\n-    os.rename(os.path.join(basedir.get(), from_head), \\\n-              os.path.join(basedir.get(), to_head))\n+        set_head_file(to_name)\n+    rename(os.path.join(basedir.get(), 'refs', 'heads'),\n+           from_name, to_name)\n \n def add(names):\n     \"\"\"Add the files or recursively add the directory contents\ndiff --git a/stgit/stack.py b/stgit/stack.py\nindex f83161b..49b50e7 100644\n--- a/stgit/stack.py\n+++ b/stgit/stack.py\n@@ -443,8 +443,7 @@ class Series:\n \n         os.makedirs(self.__patch_dir)\n \n-        if not os.path.isdir(bases_dir):\n-            os.makedirs(bases_dir)\n+        create_dirs(bases_dir)\n \n         create_empty_file(self.__applied_file)\n         create_empty_file(self.__unapplied_file)\n@@ -502,11 +501,14 @@ class Series:\n         git.rename_branch(self.__name, to_name)\n \n         if os.path.isdir(self.__series_dir):\n-            os.rename(self.__series_dir, to_stack.__series_dir)\n+            rename(os.path.join(self.__base_dir, 'patches'),\n+                   self.__name, to_stack.__name)\n         if os.path.exists(self.__base_file):\n-            os.rename(self.__base_file, to_stack.__base_file)\n+            rename(os.path.join(self.__base_dir, 'refs', 'bases'),\n+                   self.__name, to_stack.__name)\n         if os.path.exists(self.__refs_dir):\n-            os.rename(self.__refs_dir, to_stack.__refs_dir)\n+            rename(os.path.join(self.__base_dir, 'refs', 'patches'),\n+                   self.__name, to_stack.__name)\n \n         self.__init__(to_name)\n \n@@ -560,16 +562,19 @@ class Series:\n             else:\n                 print 'Patch directory %s is not empty.' % self.__name\n             if not os.listdir(self.__series_dir):\n-                os.rmdir(self.__series_dir)\n+                remove_dirs(os.path.join(self.__base_dir, 'patches'),\n+                            self.__name)\n             else:\n                 print 'Series directory %s is not empty.' % self.__name\n             if not os.listdir(self.__refs_dir):\n-                os.rmdir(self.__refs_dir)\n+                remove_dirs(os.path.join(self.__base_dir, 'refs', 'patches'),\n+                            self.__name)\n             else:\n                 print 'Refs directory %s is not empty.' % self.__refs_dir\n \n         if os.path.exists(self.__base_file):\n-            os.remove(self.__base_file)\n+            remove_file_and_dirs(\n+                os.path.join(self.__base_dir, 'refs', 'bases'), self.__name)\n \n     def refresh_patch(self, files = None, message = None, edit = False,\n                       show_patch = False,\ndiff --git a/stgit/utils.py b/stgit/utils.py\nindex 5749b3b..68b8f58 100644\n--- a/stgit/utils.py\n+++ b/stgit/utils.py\n@@ -1,6 +1,8 @@\n \"\"\"Common utility functions\n \"\"\"\n \n+import errno, os, os.path\n+\n __copyright__ = \"\"\"\n Copyright (C) 2005, Catalin Marinas <catalin.marinas@gmail.com>\n \n@@ -18,6 +20,12 @@ along with this program; if not, write t\n Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA\n \"\"\"\n \n+def mkdir_file(filename, mode):\n+    \"\"\"Opens filename with the given mode, creating the directory it's\n+    in if it doesn't already exist.\"\"\"\n+    create_dirs(os.path.dirname(filename))\n+    return file(filename, mode)\n+\n def read_string(filename, multiline = False):\n     \"\"\"Reads the first line from a file\n     \"\"\"\n@@ -32,7 +40,7 @@ def read_string(filename, multiline = Fa\n def write_string(filename, line, multiline = False):\n     \"\"\"Writes 'line' to file and truncates it\n     \"\"\"\n-    f = file(filename, 'w+')\n+    f = mkdir_file(filename, 'w+')\n     if multiline:\n         f.write(line)\n     else:\n@@ -42,7 +50,7 @@ def write_string(filename, line, multili\n def append_strings(filename, lines):\n     \"\"\"Appends 'lines' sequence to file\n     \"\"\"\n-    f = file(filename, 'a+')\n+    f = mkdir_file(filename, 'a+')\n     for line in lines:\n         print >> f, line\n     f.close()\n@@ -50,14 +58,14 @@ def append_strings(filename, lines):\n def append_string(filename, line):\n     \"\"\"Appends 'line' to file\n     \"\"\"\n-    f = file(filename, 'a+')\n+    f = mkdir_file(filename, 'a+')\n     print >> f, line\n     f.close()\n \n def insert_string(filename, line):\n     \"\"\"Inserts 'line' at the beginning of the file\n     \"\"\"\n-    f = file(filename, 'r+')\n+    f = mkdir_file(filename, 'r+')\n     lines = f.readlines()\n     f.seek(0); f.truncate()\n     print >> f, line\n@@ -67,4 +75,74 @@ def insert_string(filename, line):\n def create_empty_file(name):\n     \"\"\"Creates an empty file\n     \"\"\"\n-    file(name, 'w+').close()\n+    mkdir_file(name, 'w+').close()\n+\n+def list_files_and_dirs(path):\n+    \"\"\"Return the sets of filenames and directory names in a\n+    directory.\"\"\"\n+    files, dirs = [], []\n+    for fd in os.listdir(path):\n+        full_fd = os.path.join(path, fd)\n+        if os.path.isfile(full_fd):\n+            files.append(fd)\n+        elif os.path.isdir(full_fd):\n+            dirs.append(fd)\n+    return files, dirs\n+\n+def walk_tree(basedir):\n+    \"\"\"Starting in the given directory, iterate through all its\n+    subdirectories. For each subdirectory, yield the name of the\n+    subdirectory (relative to the base directory), the list of\n+    filenames in the subdirectory, and the list of directory names in\n+    the subdirectory.\"\"\"\n+    subdirs = ['']\n+    while subdirs:\n+        subdir = subdirs.pop()\n+        files, dirs = list_files_and_dirs(os.path.join(basedir, subdir))\n+        for d in dirs:\n+            subdirs.append(os.path.join(subdir, d))\n+        yield subdir, files, dirs\n+\n+def strip_prefix(prefix, string):\n+    \"\"\"Return string, without the prefix. Blow up if string doesn't\n+    start with prefix.\"\"\"\n+    assert string.startswith(prefix)\n+    return string[len(prefix):]\n+\n+def remove_dirs(basedir, dirs):\n+    \"\"\"Starting at join(basedir, dirs), remove the directory if empty,\n+    and try the same with its parent, until we find a nonempty\n+    directory or reach basedir.\"\"\"\n+    path = dirs\n+    while path:\n+        try:\n+            os.rmdir(os.path.join(basedir, path))\n+        except OSError:\n+            return # can't remove nonempty directory\n+        path = os.path.dirname(path)\n+\n+def remove_file_and_dirs(basedir, file):\n+    \"\"\"Remove join(basedir, file), and then remove the directory it\n+    was in if empty, and try the same with its parent, until we find a\n+    nonempty directory or reach basedir.\"\"\"\n+    os.remove(os.path.join(basedir, file))\n+    remove_dirs(basedir, os.path.dirname(file))\n+\n+def create_dirs(directory):\n+    \"\"\"Create the given directory, if the path doesn't already exist.\"\"\"\n+    if directory:\n+        create_dirs(os.path.dirname(directory))\n+        try:\n+            os.mkdir(directory)\n+        except OSError, e:\n+            if e.errno != errno.EEXIST:\n+                raise e\n+\n+def rename(basedir, file1, file2):\n+    \"\"\"Rename join(basedir, file1) to join(basedir, file2), not\n+    leaving any empty directories behind and creating any directories\n+    necessary.\"\"\"\n+    full_file2 = os.path.join(basedir, file2)\n+    create_dirs(os.path.dirname(full_file2))\n+    os.rename(os.path.join(basedir, file1), full_file2)\n+    remove_dirs(basedir, os.path.dirname(file1))\n-- \n1.3.2.g639c\n\n\n-- \nKarl Hasselström, kha@treskal.com\n      www.treskal.com/kalle\n"},{"id":"20208","messageId":"20060518064321.GA10400@backpacker.hemma.treskal.com","threadId":"4145","inReplyTo":"20060516074504.GA27234@diana.vm.bytemark.co.uk","subject":"[PATCH 2/2] Tests for branch names with slashes","fromName":"Karl Hasselström","fromEmail":"kha@treskal.com","sentAt":"2006-05-18T06:43:21Z","receivedAt":"2006-05-18T06:43:21Z","isPatch":true,"sender":{"key":"kha@treskal.com","avatar":"https://gravatar.com/avatar/f0120c734b5279b345075a28521e1ac66acb20c9913ffe9bf6ae97e53f7f3f13?d=mp&s=160"},"body":"Test a number of operations on a repository that has branch names\ncontaining slashes (that is, branches living in a subdirectory of\n.git/refs/heads).\n\nSigned-off-by: Karl Hasselström <kha@treskal.com>\n\n\n---\n\n t/t0001-subdir-branches.sh |   59 ++++++++++++++++++++++++++++++++++++++++++++\n 1 files changed, 59 insertions(+), 0 deletions(-)\n create mode 100644 t/t0001-subdir-branches.sh\n\nd54c84917b40b1e8b05081f0c063fd99b2bbb1ad\ndiff --git a/t/t0001-subdir-branches.sh b/t/t0001-subdir-branches.sh\nnew file mode 100644\nindex 0000000..ddde238\n--- /dev/null\n+++ b/t/t0001-subdir-branches.sh\n@@ -0,0 +1,59 @@\n+#!/bin/sh\n+#\n+# Copyright (c) 2006 Karl Hasselström\n+#\n+\n+test_description='Branch names containing slashes\n+\n+Test a number of operations on a repository that has branch names\n+containing slashes (that is, branches living in a subdirectory of\n+.git/refs/heads).'\n+\n+. ./test-lib.sh\n+\n+test_expect_success 'Create a patch' \\\n+  'stg init &&\n+   echo \"foo\" > foo.txt &&\n+   stg add foo.txt &&\n+   stg new foo -m \"Add foo.txt\" &&\n+   stg refresh'\n+\n+test_expect_success 'Old and new id with non-slashy branch' \\\n+  'stg id foo &&\n+   stg id foo// &&\n+   stg id foo/ &&\n+   stg id foo//top &&\n+   stg id foo/top &&\n+   stg id foo@master &&\n+   stg id foo@master//top &&\n+   stg id foo@master/top'\n+\n+test_expect_success 'Clone branch to slashier name' \\\n+  'stg branch --clone x/y/z'\n+\n+test_expect_success 'Try new form of id with slashy branch' \\\n+  'stg id foo &&\n+   stg id foo// &&\n+   stg id foo//top &&\n+   stg id foo@x/y/z &&\n+   stg id foo@x/y/z//top'\n+\n+test_expect_failure 'Try old id with slashy branch' \\\n+  'stg id foo/ ||\n+   stg id foo/top ||\n+   stg id foo@x/y/z/top'\n+\n+test_expect_success 'Create patch in slashy branch' \\\n+  'echo \"bar\" >> foo.txt &&\n+   stg new bar -m \"Add another line\" &&\n+   stg refresh'\n+\n+test_expect_success 'Rename branches' \\\n+  'stg branch --rename master goo/gaa &&\n+   test ! -e .git/refs/heads/master &&\n+   stg branch --rename goo/gaa x1/x2/x3/x4 &&\n+   test ! -e .git/refs/heads/goo &&\n+   stg branch --rename x1/x2/x3/x4 servant &&\n+   test ! -e .git/refs/heads/x1'\n+\n+test_done\n-- \n1.3.2.g639c\n\n\n-- \nKarl Hasselström, kha@treskal.com\n      www.treskal.com/kalle\n"},{"id":"20210","messageId":"20060518065040.GA10497@backpacker.hemma.treskal.com","threadId":"4145","inReplyTo":"20060518064214.GA10390@backpacker.hemma.treskal.com","subject":"[PATCH] Handle branch names with slashes","fromName":"Karl Hasselström","fromEmail":"kha@treskal.com","sentAt":"2006-05-18T06:50:40Z","receivedAt":"2006-05-18T06:50:40Z","isPatch":true,"sender":{"key":"kha@treskal.com","avatar":"https://gravatar.com/avatar/f0120c734b5279b345075a28521e1ac66acb20c9913ffe9bf6ae97e53f7f3f13?d=mp&s=160"},"body":"Teach stgit to handle branch names with slashes in them; that is,\nbranches living in a subdirectory of .git/refs/heads.\n\nI had to change the patch@branch/top command-line syntax to\npatch@branch//top, in order to get sane parsing. The /top variant is\nstill available for repositories that have no slashy branches; it is\ndisabled as soon as there exists at least one subdirectory of\nrefs/heads. Preferably, this compatibility hack can be killed some\ntime in the future.\n\nSigned-off-by: Karl Hasselström <kha@treskal.com>\n\n\n---\n\nOh yeah, remember to change % to // in the commit comment as well . . .\n\nCatalin, I hope you're paying attention when trying to pick the\ncorrect three patches out of the salvos I've sent you. :-)\n\n stgit/commands/branch.py |    5 ++\n stgit/commands/common.py |  108 +++++++++++++++++++++++++++-------------------\n stgit/commands/diff.py   |   16 ++++---\n stgit/commands/files.py  |    4 +-\n stgit/commands/id.py     |    2 -\n stgit/commands/mail.py   |    8 ++-\n stgit/git.py             |   42 +++++++++---------\n stgit/stack.py           |   21 ++++++---\n stgit/utils.py           |   88 +++++++++++++++++++++++++++++++++++--\n 9 files changed, 202 insertions(+), 92 deletions(-)\n\n4ce56cd9e2d39f3a98b6dd010d11beb6037f8ff3\ndiff --git a/stgit/commands/branch.py b/stgit/commands/branch.py\nindex 2218bbb..d348409 100644\n--- a/stgit/commands/branch.py\n+++ b/stgit/commands/branch.py\n@@ -172,7 +172,10 @@ def func(parser, options, args):\n         if len(args) != 0:\n             parser.error('incorrect number of arguments')\n \n-        branches = os.listdir(os.path.join(basedir.get(), 'refs', 'heads'))\n+        branches = []\n+        basepath = os.path.join(basedir.get(), 'refs', 'heads')\n+        for path, files, dirs in walk_tree(basepath):\n+            branches += [os.path.join(path, f) for f in files]\n         branches.sort()\n \n         if branches:\ndiff --git a/stgit/commands/common.py b/stgit/commands/common.py\nindex c6ca514..93344aa 100644\n--- a/stgit/commands/common.py\n+++ b/stgit/commands/common.py\n@@ -18,7 +18,7 @@ along with this program; if not, write t\n Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA\n \"\"\"\n \n-import sys, os, re\n+import sys, os, os.path, re\n from optparse import OptionParser, make_option\n \n from stgit.utils import *\n@@ -34,54 +34,74 @@ class CmdException(Exception):\n \n \n # Utility functions\n+class RevParseException(Exception):\n+    \"\"\"Revision spec parse error.\"\"\"\n+    pass\n+\n+def parse_rev(rev):\n+    \"\"\"Parse a revision specification into its\n+    patchname@branchname//patch_id parts. If no branch name has a slash\n+    in it, also accept / instead of //.\"\"\"\n+    files, dirs = list_files_and_dirs(os.path.join(basedir.get(),\n+                                                   'refs', 'heads'))\n+    if len(dirs) != 0:\n+        # We have branch names with / in them.\n+        branch_chars = r'[^@]'\n+        patch_id_mark = r'//'\n+    else:\n+        # No / in branch names.\n+        branch_chars = r'[^@%/]'\n+        patch_id_mark = r'(/|//)'\n+    patch_re = r'(?P<patch>[^@/]+)'\n+    branch_re = r'@(?P<branch>%s+)' % branch_chars\n+    patch_id_re = r'%s(?P<patch_id>[a-z.]*)' % patch_id_mark\n+\n+    # Try //patch_id.\n+    m = re.match(r'^%s$' % patch_id_re, rev)\n+    if m:\n+        return None, None, m.group('patch_id')\n+\n+    # Try path[@branch]//patch_id.\n+    m = re.match(r'^%s(%s)?%s$' % (patch_re, branch_re, patch_id_re), rev)\n+    if m:\n+        return m.group('patch'), m.group('branch'), m.group('patch_id')\n+\n+    # Try patch[@branch].\n+    m = re.match(r'^%s(%s)?$' % (patch_re, branch_re), rev)\n+    if m:\n+        return m.group('patch'), m.group('branch'), None\n+\n+    # No, we can't parse that.\n+    raise RevParseException\n+\n def git_id(rev):\n     \"\"\"Return the GIT id\n     \"\"\"\n     if not rev:\n         return None\n-    \n-    rev_list = rev.split('/')\n-    if len(rev_list) == 2:\n-        patch_id = rev_list[1]\n-        if not patch_id:\n-            patch_id = 'top'\n-    elif len(rev_list) == 1:\n-        patch_id = 'top'\n-    else:\n-        patch_id = None\n-\n-    patch_branch = rev_list[0].split('@')\n-    if len(patch_branch) == 1:\n-        series = crt_series\n-    elif len(patch_branch) == 2:\n-        series = stack.Series(patch_branch[1])\n-    else:\n-        raise CmdException, 'Unknown id: %s' % rev\n-\n-    patch_name = patch_branch[0]\n-    if not patch_name:\n-        patch_name = series.get_current()\n-        if not patch_name:\n-            raise CmdException, 'No patches applied'\n-\n-    # patch\n-    if patch_name in series.get_applied() \\\n-           or patch_name in series.get_unapplied():\n-        if patch_id == 'top':\n-            return series.get_patch(patch_name).get_top()\n-        elif patch_id == 'bottom':\n-            return series.get_patch(patch_name).get_bottom()\n-        # Note we can return None here.\n-        elif patch_id == 'top.old':\n-            return series.get_patch(patch_name).get_old_top()\n-        elif patch_id == 'bottom.old':\n-            return series.get_patch(patch_name).get_old_bottom()\n-\n-    # base\n-    if patch_name == 'base' and len(rev_list) == 1:\n-        return read_string(series.get_base_file())\n-\n-    # anything else failed\n+    try:\n+        patch, branch, patch_id = parse_rev(rev)\n+        if branch == None:\n+            series = crt_series\n+        else:\n+            series = stack.Series(branch)\n+        if patch == None:\n+            patch = series.get_current()\n+            if not patch:\n+                raise CmdException, 'No patches applied'\n+        if patch in series.get_applied() or patch in series.get_unapplied():\n+            if patch_id in ['top', '', None]:\n+                return series.get_patch(patch).get_top()\n+            elif patch_id == 'bottom':\n+                return series.get_patch(patch).get_bottom()\n+            elif patch_id == 'top.old':\n+                return series.get_patch(patch).get_old_top()\n+            elif patch_id == 'bottom.old':\n+                return series.get_patch(patch).get_old_bottom()\n+        if patch == 'base' and patch_id == None:\n+            return read_string(series.get_base_file())\n+    except RevParseException:\n+        pass\n     return git.rev_parse(rev + '^{commit}')\n \n def check_local_changes():\ndiff --git a/stgit/commands/diff.py b/stgit/commands/diff.py\nindex 7dc6c5d..d765784 100644\n--- a/stgit/commands/diff.py\n+++ b/stgit/commands/diff.py\n@@ -33,12 +33,12 @@ or a tree-ish object and another tree-is\n be given to restrict the diff output. The tree-ish object can be a\n standard git commit, tag or tree. In addition to these, the command\n also supports 'base', representing the bottom of the current stack,\n-and '[patch]/[bottom | top]' for the patch boundaries (defaulting to\n+and '[patch][//[bottom | top]]' for the patch boundaries (defaulting to\n the current one):\n \n-rev = '([patch]/[bottom | top]) | <tree-ish> | base'\n+rev = '([patch][//[bottom | top]]) | <tree-ish> | base'\n \n-If neither bottom or top are given but a '/' is present, the command\n+If neither bottom nor top are given but a '//' is present, the command\n shows the specified patch (defaulting to the current one).\"\"\"\n \n options = [make_option('-r', metavar = 'rev1[:[rev2]]', dest = 'revs',\n@@ -55,10 +55,14 @@ def func(parser, options, args):\n         rev_list = options.revs.split(':')\n         rev_list_len = len(rev_list)\n         if rev_list_len == 1:\n-            if rev_list[0][-1] == '/':\n+            rev = rev_list[0]\n+            if rev[-1] == '/':\n                 # the whole patch\n-                rev1 = rev_list[0] + 'bottom'\n-                rev2 = rev_list[0] + 'top'\n+                rev = rev[:-1]\n+                if rev[-1] == '/':\n+                    rev = rev[:-1]\n+                rev1 = rev + '//bottom'\n+                rev2 = rev + '//top'\n             else:\n                 rev1 = rev_list[0]\n                 rev2 = None\ndiff --git a/stgit/commands/files.py b/stgit/commands/files.py\nindex 0694d83..b33bd2a 100644\n--- a/stgit/commands/files.py\n+++ b/stgit/commands/files.py\n@@ -53,8 +53,8 @@ def func(parser, options, args):\n     else:\n         parser.error('incorrect number of arguments')\n \n-    rev1 = git_id('%s/bottom' % patch)\n-    rev2 = git_id('%s/top' % patch)\n+    rev1 = git_id('%s//bottom' % patch)\n+    rev2 = git_id('%s//top' % patch)\n \n     if options.stat:\n         print git.diffstat(rev1 = rev1, rev2 = rev2)\ndiff --git a/stgit/commands/id.py b/stgit/commands/id.py\nindex 1cf6ea6..284589a 100644\n--- a/stgit/commands/id.py\n+++ b/stgit/commands/id.py\n@@ -28,7 +28,7 @@ usage = \"\"\"%prog [options] [id]\n \n Print the hash value of a GIT id (defaulting to HEAD). In addition to\n the standard GIT id's like heads and tags, this command also accepts\n-'base[@<branch>]' and '[<patch>[@<branch>]][/(bottom | top)]'. If no\n+'base[@<branch>]' and '[<patch>[@<branch>]][//[bottom | top]]'. If no\n 'top' or 'bottom' are passed and <patch> is a valid patch name, 'top'\n will be used by default.\"\"\"\n \ndiff --git a/stgit/commands/mail.py b/stgit/commands/mail.py\nindex 5e01ea1..3928b81 100644\n--- a/stgit/commands/mail.py\n+++ b/stgit/commands/mail.py\n@@ -324,10 +324,10 @@ def __build_message(tmpl, patch, patch_n\n                  'shortdescr':   short_descr,\n                  'longdescr':    long_descr,\n                  'endofheaders': headers_end,\n-                 'diff':         git.diff(rev1 = git_id('%s/bottom' % patch),\n-                                          rev2 = git_id('%s/top' % patch)),\n-                 'diffstat':     git.diffstat(rev1 = git_id('%s/bottom'%patch),\n-                                              rev2 = git_id('%s/top' % patch)),\n+                 'diff':         git.diff(rev1 = git_id('%s//bottom' % patch),\n+                                          rev2 = git_id('%s//top' % patch)),\n+                 'diffstat':     git.diffstat(rev1 = git_id('%s//bottom'%patch),\n+                                              rev2 = git_id('%s//top' % patch)),\n                  'date':         email.Utils.formatdate(localtime = True),\n                  'version':      version_str,\n                  'patchnr':      patch_nr_str,\ndiff --git a/stgit/git.py b/stgit/git.py\nindex 2884f36..716609c 100644\n--- a/stgit/git.py\n+++ b/stgit/git.py\n@@ -225,7 +225,8 @@ def get_head():\n def get_head_file():\n     \"\"\"Returns the name of the file pointed to by the HEAD link\n     \"\"\"\n-    return os.path.basename(_output_one_line('git-symbolic-ref HEAD'))\n+    return strip_prefix('refs/heads/',\n+                        _output_one_line('git-symbolic-ref HEAD'))\n \n def set_head_file(ref):\n     \"\"\"Resets HEAD to point to a new ref\n@@ -233,7 +234,8 @@ def set_head_file(ref):\n     # head cache flushing is needed since we might have a different value\n     # in the new head\n     __clear_head_cache()\n-    if __run('git-symbolic-ref HEAD', [ref]) != 0:\n+    if __run('git-symbolic-ref HEAD',\n+             [os.path.join('refs', 'heads', ref)]) != 0:\n         raise GitException, 'Could not set head to \"%s\"' % ref\n \n def __set_head(val):\n@@ -272,6 +274,7 @@ def rev_parse(git_id):\n def branch_exists(branch):\n     \"\"\"Existence check for the named branch\n     \"\"\"\n+    branch = os.path.join('refs', 'heads', branch)\n     for line in _output_lines('git-rev-parse --symbolic --all 2>&1'):\n         if line.strip() == branch:\n             return True\n@@ -282,12 +285,11 @@ def branch_exists(branch):\n def create_branch(new_branch, tree_id = None):\n     \"\"\"Create a new branch in the git repository\n     \"\"\"\n-    new_head = os.path.join('refs', 'heads', new_branch)\n-    if branch_exists(new_head):\n+    if branch_exists(new_branch):\n         raise GitException, 'Branch \"%s\" already exists' % new_branch\n \n     current_head = get_head()\n-    set_head_file(new_head)\n+    set_head_file(new_branch)\n     __set_head(current_head)\n \n     # a checkout isn't needed if new branch points to the current head\n@@ -297,22 +299,22 @@ def create_branch(new_branch, tree_id = \n     if os.path.isfile(os.path.join(basedir.get(), 'MERGE_HEAD')):\n         os.remove(os.path.join(basedir.get(), 'MERGE_HEAD'))\n \n-def switch_branch(name):\n+def switch_branch(new_branch):\n     \"\"\"Switch to a git branch\n     \"\"\"\n     global __head\n \n-    new_head = os.path.join('refs', 'heads', name)\n-    if not branch_exists(new_head):\n-        raise GitException, 'Branch \"%s\" does not exist' % name\n+    if not branch_exists(new_branch):\n+        raise GitException, 'Branch \"%s\" does not exist' % new_branch\n \n-    tree_id = rev_parse(new_head + '^{commit}')\n+    tree_id = rev_parse(os.path.join('refs', 'heads', new_branch)\n+                        + '^{commit}')\n     if tree_id != get_head():\n         refresh_index()\n         if __run('git-read-tree -u -m', [get_head(), tree_id]) != 0:\n             raise GitException, 'git-read-tree failed (local changes maybe?)'\n         __head = tree_id\n-    set_head_file(new_head)\n+    set_head_file(new_branch)\n \n     if os.path.isfile(os.path.join(basedir.get(), 'MERGE_HEAD')):\n         os.remove(os.path.join(basedir.get(), 'MERGE_HEAD'))\n@@ -320,25 +322,23 @@ def switch_branch(name):\n def delete_branch(name):\n     \"\"\"Delete a git branch\n     \"\"\"\n-    branch_head = os.path.join('refs', 'heads', name)\n-    if not branch_exists(branch_head):\n+    if not branch_exists(name):\n         raise GitException, 'Branch \"%s\" does not exist' % name\n-    os.remove(os.path.join(basedir.get(), branch_head))\n+    remove_file_and_dirs(os.path.join(basedir.get(), 'refs', 'heads'),\n+                         name)\n \n def rename_branch(from_name, to_name):\n     \"\"\"Rename a git branch\n     \"\"\"\n-    from_head = os.path.join('refs', 'heads', from_name)\n-    if not branch_exists(from_head):\n+    if not branch_exists(from_name):\n         raise GitException, 'Branch \"%s\" does not exist' % from_name\n-    to_head = os.path.join('refs', 'heads', to_name)\n-    if branch_exists(to_head):\n+    if branch_exists(to_name):\n         raise GitException, 'Branch \"%s\" already exists' % to_name\n \n     if get_head_file() == from_name:\n-        set_head_file(to_head)\n-    os.rename(os.path.join(basedir.get(), from_head), \\\n-              os.path.join(basedir.get(), to_head))\n+        set_head_file(to_name)\n+    rename(os.path.join(basedir.get(), 'refs', 'heads'),\n+           from_name, to_name)\n \n def add(names):\n     \"\"\"Add the files or recursively add the directory contents\ndiff --git a/stgit/stack.py b/stgit/stack.py\nindex f83161b..49b50e7 100644\n--- a/stgit/stack.py\n+++ b/stgit/stack.py\n@@ -443,8 +443,7 @@ class Series:\n \n         os.makedirs(self.__patch_dir)\n \n-        if not os.path.isdir(bases_dir):\n-            os.makedirs(bases_dir)\n+        create_dirs(bases_dir)\n \n         create_empty_file(self.__applied_file)\n         create_empty_file(self.__unapplied_file)\n@@ -502,11 +501,14 @@ class Series:\n         git.rename_branch(self.__name, to_name)\n \n         if os.path.isdir(self.__series_dir):\n-            os.rename(self.__series_dir, to_stack.__series_dir)\n+            rename(os.path.join(self.__base_dir, 'patches'),\n+                   self.__name, to_stack.__name)\n         if os.path.exists(self.__base_file):\n-            os.rename(self.__base_file, to_stack.__base_file)\n+            rename(os.path.join(self.__base_dir, 'refs', 'bases'),\n+                   self.__name, to_stack.__name)\n         if os.path.exists(self.__refs_dir):\n-            os.rename(self.__refs_dir, to_stack.__refs_dir)\n+            rename(os.path.join(self.__base_dir, 'refs', 'patches'),\n+                   self.__name, to_stack.__name)\n \n         self.__init__(to_name)\n \n@@ -560,16 +562,19 @@ class Series:\n             else:\n                 print 'Patch directory %s is not empty.' % self.__name\n             if not os.listdir(self.__series_dir):\n-                os.rmdir(self.__series_dir)\n+                remove_dirs(os.path.join(self.__base_dir, 'patches'),\n+                            self.__name)\n             else:\n                 print 'Series directory %s is not empty.' % self.__name\n             if not os.listdir(self.__refs_dir):\n-                os.rmdir(self.__refs_dir)\n+                remove_dirs(os.path.join(self.__base_dir, 'refs', 'patches'),\n+                            self.__name)\n             else:\n                 print 'Refs directory %s is not empty.' % self.__refs_dir\n \n         if os.path.exists(self.__base_file):\n-            os.remove(self.__base_file)\n+            remove_file_and_dirs(\n+                os.path.join(self.__base_dir, 'refs', 'bases'), self.__name)\n \n     def refresh_patch(self, files = None, message = None, edit = False,\n                       show_patch = False,\ndiff --git a/stgit/utils.py b/stgit/utils.py\nindex 5749b3b..68b8f58 100644\n--- a/stgit/utils.py\n+++ b/stgit/utils.py\n@@ -1,6 +1,8 @@\n \"\"\"Common utility functions\n \"\"\"\n \n+import errno, os, os.path\n+\n __copyright__ = \"\"\"\n Copyright (C) 2005, Catalin Marinas <catalin.marinas@gmail.com>\n \n@@ -18,6 +20,12 @@ along with this program; if not, write t\n Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA\n \"\"\"\n \n+def mkdir_file(filename, mode):\n+    \"\"\"Opens filename with the given mode, creating the directory it's\n+    in if it doesn't already exist.\"\"\"\n+    create_dirs(os.path.dirname(filename))\n+    return file(filename, mode)\n+\n def read_string(filename, multiline = False):\n     \"\"\"Reads the first line from a file\n     \"\"\"\n@@ -32,7 +40,7 @@ def read_string(filename, multiline = Fa\n def write_string(filename, line, multiline = False):\n     \"\"\"Writes 'line' to file and truncates it\n     \"\"\"\n-    f = file(filename, 'w+')\n+    f = mkdir_file(filename, 'w+')\n     if multiline:\n         f.write(line)\n     else:\n@@ -42,7 +50,7 @@ def write_string(filename, line, multili\n def append_strings(filename, lines):\n     \"\"\"Appends 'lines' sequence to file\n     \"\"\"\n-    f = file(filename, 'a+')\n+    f = mkdir_file(filename, 'a+')\n     for line in lines:\n         print >> f, line\n     f.close()\n@@ -50,14 +58,14 @@ def append_strings(filename, lines):\n def append_string(filename, line):\n     \"\"\"Appends 'line' to file\n     \"\"\"\n-    f = file(filename, 'a+')\n+    f = mkdir_file(filename, 'a+')\n     print >> f, line\n     f.close()\n \n def insert_string(filename, line):\n     \"\"\"Inserts 'line' at the beginning of the file\n     \"\"\"\n-    f = file(filename, 'r+')\n+    f = mkdir_file(filename, 'r+')\n     lines = f.readlines()\n     f.seek(0); f.truncate()\n     print >> f, line\n@@ -67,4 +75,74 @@ def insert_string(filename, line):\n def create_empty_file(name):\n     \"\"\"Creates an empty file\n     \"\"\"\n-    file(name, 'w+').close()\n+    mkdir_file(name, 'w+').close()\n+\n+def list_files_and_dirs(path):\n+    \"\"\"Return the sets of filenames and directory names in a\n+    directory.\"\"\"\n+    files, dirs = [], []\n+    for fd in os.listdir(path):\n+        full_fd = os.path.join(path, fd)\n+        if os.path.isfile(full_fd):\n+            files.append(fd)\n+        elif os.path.isdir(full_fd):\n+            dirs.append(fd)\n+    return files, dirs\n+\n+def walk_tree(basedir):\n+    \"\"\"Starting in the given directory, iterate through all its\n+    subdirectories. For each subdirectory, yield the name of the\n+    subdirectory (relative to the base directory), the list of\n+    filenames in the subdirectory, and the list of directory names in\n+    the subdirectory.\"\"\"\n+    subdirs = ['']\n+    while subdirs:\n+        subdir = subdirs.pop()\n+        files, dirs = list_files_and_dirs(os.path.join(basedir, subdir))\n+        for d in dirs:\n+            subdirs.append(os.path.join(subdir, d))\n+        yield subdir, files, dirs\n+\n+def strip_prefix(prefix, string):\n+    \"\"\"Return string, without the prefix. Blow up if string doesn't\n+    start with prefix.\"\"\"\n+    assert string.startswith(prefix)\n+    return string[len(prefix):]\n+\n+def remove_dirs(basedir, dirs):\n+    \"\"\"Starting at join(basedir, dirs), remove the directory if empty,\n+    and try the same with its parent, until we find a nonempty\n+    directory or reach basedir.\"\"\"\n+    path = dirs\n+    while path:\n+        try:\n+            os.rmdir(os.path.join(basedir, path))\n+        except OSError:\n+            return # can't remove nonempty directory\n+        path = os.path.dirname(path)\n+\n+def remove_file_and_dirs(basedir, file):\n+    \"\"\"Remove join(basedir, file), and then remove the directory it\n+    was in if empty, and try the same with its parent, until we find a\n+    nonempty directory or reach basedir.\"\"\"\n+    os.remove(os.path.join(basedir, file))\n+    remove_dirs(basedir, os.path.dirname(file))\n+\n+def create_dirs(directory):\n+    \"\"\"Create the given directory, if the path doesn't already exist.\"\"\"\n+    if directory:\n+        create_dirs(os.path.dirname(directory))\n+        try:\n+            os.mkdir(directory)\n+        except OSError, e:\n+            if e.errno != errno.EEXIST:\n+                raise e\n+\n+def rename(basedir, file1, file2):\n+    \"\"\"Rename join(basedir, file1) to join(basedir, file2), not\n+    leaving any empty directories behind and creating any directories\n+    necessary.\"\"\"\n+    full_file2 = os.path.join(basedir, file2)\n+    create_dirs(os.path.dirname(full_file2))\n+    os.rename(os.path.join(basedir, file1), full_file2)\n+    remove_dirs(basedir, os.path.dirname(file1))\n-- \n1.3.2.g639c\n\n\n-- \nKarl Hasselström, kha@treskal.com\n      www.treskal.com/kalle\n"},{"id":"20225","messageId":"b0943d9e0605180511v5cf9256dx84825866b1691f51@mail.gmail.com","threadId":"4145","inReplyTo":"20060518065040.GA10497@backpacker.hemma.treskal.com","subject":"Re: [PATCH] Handle branch names with slashes","fromName":"Catalin Marinas","fromEmail":"catalin.marinas@gmail.com","sentAt":"2006-05-18T12:11:52Z","receivedAt":"2006-05-18T12:11:52Z","isPatch":true,"sender":{"key":"catalin.marinas@gmail.com","avatar":null},"body":"On 18/05/06, Karl Hasselström <kha@treskal.com> wrote:\n> Teach stgit to handle branch names with slashes in them; that is,\n> branches living in a subdirectory of .git/refs/heads.\n[...]\n> Catalin, I hope you're paying attention when trying to pick the\n> correct three patches out of the salvos I've sent you. :-)\n\nHopefully, I applied them correctly. I'll update the public repository\ntonight and you can check whether they are OK or not.\n\n> --- a/stgit/commands/common.py\n> +++ b/stgit/commands/common.py\n[...]\n> +    if len(dirs) != 0:\n> +        # We have branch names with / in them.\n> +        branch_chars = r'[^@]'\n> +        patch_id_mark = r'//'\n> +    else:\n> +        # No / in branch names.\n> +        branch_chars = r'[^@%/]'\n\nI removed % from the above regexp.\n\nThanks for the patches. Great work.\n\n-- \nCatalin\n"},{"id":"20232","messageId":"20060518160040.GA28270@diana.vm.bytemark.co.uk","threadId":"4145","inReplyTo":"b0943d9e0605180511v5cf9256dx84825866b1691f51@mail.gmail.com","subject":"Re: [PATCH] Handle branch names with slashes","fromName":"Karl Hasselström","fromEmail":"kha@treskal.com","sentAt":"2006-05-18T16:00:40Z","receivedAt":"2006-05-18T16:00:40Z","isPatch":true,"sender":{"key":"kha@treskal.com","avatar":"https://gravatar.com/avatar/f0120c734b5279b345075a28521e1ac66acb20c9913ffe9bf6ae97e53f7f3f13?d=mp&s=160"},"body":"On 2006-05-18 13:11:52 +0100, Catalin Marinas wrote:\n\n> On 18/05/06, Karl Hasselström <kha@treskal.com> wrote:\n>\n> > +    if len(dirs) != 0:\n> > +        # We have branch names with / in them.\n> > +        branch_chars = r'[^@]'\n> > +        patch_id_mark = r'//'\n> > +    else:\n> > +        # No / in branch names.\n> > +        branch_chars = r'[^@%/]'\n>\n> I removed % from the above regexp.\n\nAh, I missed one. Perhaps I should act surprised . . . :-)\n\n-- \nKarl Hasselström, kha@treskal.com\n      www.treskal.com/kalle\n"}]}