Teach stgit to handle branch names with slashes in them; that is, branches living in a subdirectory of .git/refs/heads. I had to change the patch@branch/top command-line syntax to patch@branch//top, in order to get sane parsing. The /top variant is still available for repositories that have no slashy branches; it is disabled as soon as there exists at least one subdirectory of refs/heads. Preferably, this compatibility hack can be killed some time in the future. Signed-off-by: Karl Hasselström <kha@xxxxxxxxxxx> --- Oh yeah, remember to change % to // in the commit comment as well . . . Catalin, I hope you're paying attention when trying to pick the correct three patches out of the salvos I've sent you. :-) stgit/commands/branch.py | 5 ++ stgit/commands/common.py | 108 +++++++++++++++++++++++++++------------------- stgit/commands/diff.py | 16 ++++--- stgit/commands/files.py | 4 +- stgit/commands/id.py | 2 - stgit/commands/mail.py | 8 ++- stgit/git.py | 42 +++++++++--------- stgit/stack.py | 21 ++++++--- stgit/utils.py | 88 +++++++++++++++++++++++++++++++++++-- 9 files changed, 202 insertions(+), 92 deletions(-) 4ce56cd9e2d39f3a98b6dd010d11beb6037f8ff3 diff --git a/stgit/commands/branch.py b/stgit/commands/branch.py index 2218bbb..d348409 100644 --- a/stgit/commands/branch.py +++ b/stgit/commands/branch.py @@ -172,7 +172,10 @@ def func(parser, options, args): if len(args) != 0: parser.error('incorrect number of arguments') - branches = os.listdir(os.path.join(basedir.get(), 'refs', 'heads')) + branches = [] + basepath = os.path.join(basedir.get(), 'refs', 'heads') + for path, files, dirs in walk_tree(basepath): + branches += [os.path.join(path, f) for f in files] branches.sort() if branches: diff --git a/stgit/commands/common.py b/stgit/commands/common.py index c6ca514..93344aa 100644 --- a/stgit/commands/common.py +++ b/stgit/commands/common.py @@ -18,7 +18,7 @@ along with this program; if not, write t Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ -import sys, os, re +import sys, os, os.path, re from optparse import OptionParser, make_option from stgit.utils import * @@ -34,54 +34,74 @@ class CmdException(Exception): # Utility functions +class RevParseException(Exception): + """Revision spec parse error.""" + pass + +def parse_rev(rev): + """Parse a revision specification into its + patchname@branchname//patch_id parts. If no branch name has a slash + in it, also accept / instead of //.""" + files, dirs = list_files_and_dirs(os.path.join(basedir.get(), + 'refs', 'heads')) + if len(dirs) != 0: + # We have branch names with / in them. + branch_chars = r'[^@]' + patch_id_mark = r'//' + else: + # No / in branch names. + branch_chars = r'[^@%/]' + patch_id_mark = r'(/|//)' + patch_re = r'(?P<patch>[^@/]+)' + branch_re = r'@(?P<branch>%s+)' % branch_chars + patch_id_re = r'%s(?P<patch_id>[a-z.]*)' % patch_id_mark + + # Try //patch_id. + m = re.match(r'^%s$' % patch_id_re, rev) + if m: + return None, None, m.group('patch_id') + + # Try path[@branch]//patch_id. + m = re.match(r'^%s(%s)?%s$' % (patch_re, branch_re, patch_id_re), rev) + if m: + return m.group('patch'), m.group('branch'), m.group('patch_id') + + # Try patch[@branch]. + m = re.match(r'^%s(%s)?$' % (patch_re, branch_re), rev) + if m: + return m.group('patch'), m.group('branch'), None + + # No, we can't parse that. + raise RevParseException + def git_id(rev): """Return the GIT id """ if not rev: return None - - rev_list = rev.split('/') - if len(rev_list) == 2: - patch_id = rev_list[1] - if not patch_id: - patch_id = 'top' - elif len(rev_list) == 1: - patch_id = 'top' - else: - patch_id = None - - patch_branch = rev_list[0].split('@') - if len(patch_branch) == 1: - series = crt_series - elif len(patch_branch) == 2: - series = stack.Series(patch_branch[1]) - else: - raise CmdException, 'Unknown id: %s' % rev - - patch_name = patch_branch[0] - if not patch_name: - patch_name = series.get_current() - if not patch_name: - raise CmdException, 'No patches applied' - - # patch - if patch_name in series.get_applied() \ - or patch_name in series.get_unapplied(): - if patch_id == 'top': - return series.get_patch(patch_name).get_top() - elif patch_id == 'bottom': - return series.get_patch(patch_name).get_bottom() - # Note we can return None here. - elif patch_id == 'top.old': - return series.get_patch(patch_name).get_old_top() - elif patch_id == 'bottom.old': - return series.get_patch(patch_name).get_old_bottom() - - # base - if patch_name == 'base' and len(rev_list) == 1: - return read_string(series.get_base_file()) - - # anything else failed + try: + patch, branch, patch_id = parse_rev(rev) + if branch == None: + series = crt_series + else: + series = stack.Series(branch) + if patch == None: + patch = series.get_current() + if not patch: + raise CmdException, 'No patches applied' + if patch in series.get_applied() or patch in series.get_unapplied(): + if patch_id in ['top', '', None]: + return series.get_patch(patch).get_top() + elif patch_id == 'bottom': + return series.get_patch(patch).get_bottom() + elif patch_id == 'top.old': + return series.get_patch(patch).get_old_top() + elif patch_id == 'bottom.old': + return series.get_patch(patch).get_old_bottom() + if patch == 'base' and patch_id == None: + return read_string(series.get_base_file()) + except RevParseException: + pass return git.rev_parse(rev + '^{commit}') def check_local_changes(): diff --git a/stgit/commands/diff.py b/stgit/commands/diff.py index 7dc6c5d..d765784 100644 --- a/stgit/commands/diff.py +++ b/stgit/commands/diff.py @@ -33,12 +33,12 @@ or a tree-ish object and another tree-is be given to restrict the diff output. The tree-ish object can be a standard git commit, tag or tree. In addition to these, the command also supports 'base', representing the bottom of the current stack, -and '[patch]/[bottom | top]' for the patch boundaries (defaulting to +and '[patch][//[bottom | top]]' for the patch boundaries (defaulting to the current one): -rev = '([patch]/[bottom | top]) | <tree-ish> | base' +rev = '([patch][//[bottom | top]]) | <tree-ish> | base' -If neither bottom or top are given but a '/' is present, the command +If neither bottom nor top are given but a '//' is present, the command shows the specified patch (defaulting to the current one).""" options = [make_option('-r', metavar = 'rev1[:[rev2]]', dest = 'revs', @@ -55,10 +55,14 @@ def func(parser, options, args): rev_list = options.revs.split(':') rev_list_len = len(rev_list) if rev_list_len == 1: - if rev_list[0][-1] == '/': + rev = rev_list[0] + if rev[-1] == '/': # the whole patch - rev1 = rev_list[0] + 'bottom' - rev2 = rev_list[0] + 'top' + rev = rev[:-1] + if rev[-1] == '/': + rev = rev[:-1] + rev1 = rev + '//bottom' + rev2 = rev + '//top' else: rev1 = rev_list[0] rev2 = None diff --git a/stgit/commands/files.py b/stgit/commands/files.py index 0694d83..b33bd2a 100644 --- a/stgit/commands/files.py +++ b/stgit/commands/files.py @@ -53,8 +53,8 @@ def func(parser, options, args): else: parser.error('incorrect number of arguments') - rev1 = git_id('%s/bottom' % patch) - rev2 = git_id('%s/top' % patch) + rev1 = git_id('%s//bottom' % patch) + rev2 = git_id('%s//top' % patch) if options.stat: print git.diffstat(rev1 = rev1, rev2 = rev2) diff --git a/stgit/commands/id.py b/stgit/commands/id.py index 1cf6ea6..284589a 100644 --- a/stgit/commands/id.py +++ b/stgit/commands/id.py @@ -28,7 +28,7 @@ usage = """%prog [options] [id] Print the hash value of a GIT id (defaulting to HEAD). In addition to the standard GIT id's like heads and tags, this command also accepts -'base[@<branch>]' and '[<patch>[@<branch>]][/(bottom | top)]'. If no +'base[@<branch>]' and '[<patch>[@<branch>]][//[bottom | top]]'. If no 'top' or 'bottom' are passed and <patch> is a valid patch name, 'top' will be used by default.""" diff --git a/stgit/commands/mail.py b/stgit/commands/mail.py index 5e01ea1..3928b81 100644 --- a/stgit/commands/mail.py +++ b/stgit/commands/mail.py @@ -324,10 +324,10 @@ def __build_message(tmpl, patch, patch_n 'shortdescr': short_descr, 'longdescr': long_descr, 'endofheaders': headers_end, - 'diff': git.diff(rev1 = git_id('%s/bottom' % patch), - rev2 = git_id('%s/top' % patch)), - 'diffstat': git.diffstat(rev1 = git_id('%s/bottom'%patch), - rev2 = git_id('%s/top' % patch)), + 'diff': git.diff(rev1 = git_id('%s//bottom' % patch), + rev2 = git_id('%s//top' % patch)), + 'diffstat': git.diffstat(rev1 = git_id('%s//bottom'%patch), + rev2 = git_id('%s//top' % patch)), 'date': email.Utils.formatdate(localtime = True), 'version': version_str, 'patchnr': patch_nr_str, diff --git a/stgit/git.py b/stgit/git.py index 2884f36..716609c 100644 --- a/stgit/git.py +++ b/stgit/git.py @@ -225,7 +225,8 @@ def get_head(): def get_head_file(): """Returns the name of the file pointed to by the HEAD link """ - return os.path.basename(_output_one_line('git-symbolic-ref HEAD')) + return strip_prefix('refs/heads/', + _output_one_line('git-symbolic-ref HEAD')) def set_head_file(ref): """Resets HEAD to point to a new ref @@ -233,7 +234,8 @@ def set_head_file(ref): # head cache flushing is needed since we might have a different value # in the new head __clear_head_cache() - if __run('git-symbolic-ref HEAD', [ref]) != 0: + if __run('git-symbolic-ref HEAD', + [os.path.join('refs', 'heads', ref)]) != 0: raise GitException, 'Could not set head to "%s"' % ref def __set_head(val): @@ -272,6 +274,7 @@ def rev_parse(git_id): def branch_exists(branch): """Existence check for the named branch """ + branch = os.path.join('refs', 'heads', branch) for line in _output_lines('git-rev-parse --symbolic --all 2>&1'): if line.strip() == branch: return True @@ -282,12 +285,11 @@ def branch_exists(branch): def create_branch(new_branch, tree_id = None): """Create a new branch in the git repository """ - new_head = os.path.join('refs', 'heads', new_branch) - if branch_exists(new_head): + if branch_exists(new_branch): raise GitException, 'Branch "%s" already exists' % new_branch current_head = get_head() - set_head_file(new_head) + set_head_file(new_branch) __set_head(current_head) # a checkout isn't needed if new branch points to the current head @@ -297,22 +299,22 @@ def create_branch(new_branch, tree_id = if os.path.isfile(os.path.join(basedir.get(), 'MERGE_HEAD')): os.remove(os.path.join(basedir.get(), 'MERGE_HEAD')) -def switch_branch(name): +def switch_branch(new_branch): """Switch to a git branch """ global __head - new_head = os.path.join('refs', 'heads', name) - if not branch_exists(new_head): - raise GitException, 'Branch "%s" does not exist' % name + if not branch_exists(new_branch): + raise GitException, 'Branch "%s" does not exist' % new_branch - tree_id = rev_parse(new_head + '^{commit}') + tree_id = rev_parse(os.path.join('refs', 'heads', new_branch) + + '^{commit}') if tree_id != get_head(): refresh_index() if __run('git-read-tree -u -m', [get_head(), tree_id]) != 0: raise GitException, 'git-read-tree failed (local changes maybe?)' __head = tree_id - set_head_file(new_head) + set_head_file(new_branch) if os.path.isfile(os.path.join(basedir.get(), 'MERGE_HEAD')): os.remove(os.path.join(basedir.get(), 'MERGE_HEAD')) @@ -320,25 +322,23 @@ def switch_branch(name): def delete_branch(name): """Delete a git branch """ - branch_head = os.path.join('refs', 'heads', name) - if not branch_exists(branch_head): + if not branch_exists(name): raise GitException, 'Branch "%s" does not exist' % name - os.remove(os.path.join(basedir.get(), branch_head)) + remove_file_and_dirs(os.path.join(basedir.get(), 'refs', 'heads'), + name) def rename_branch(from_name, to_name): """Rename a git branch """ - from_head = os.path.join('refs', 'heads', from_name) - if not branch_exists(from_head): + if not branch_exists(from_name): raise GitException, 'Branch "%s" does not exist' % from_name - to_head = os.path.join('refs', 'heads', to_name) - if branch_exists(to_head): + if branch_exists(to_name): raise GitException, 'Branch "%s" already exists' % to_name if get_head_file() == from_name: - set_head_file(to_head) - os.rename(os.path.join(basedir.get(), from_head), \ - os.path.join(basedir.get(), to_head)) + set_head_file(to_name) + rename(os.path.join(basedir.get(), 'refs', 'heads'), + from_name, to_name) def add(names): """Add the files or recursively add the directory contents diff --git a/stgit/stack.py b/stgit/stack.py index f83161b..49b50e7 100644 --- a/stgit/stack.py +++ b/stgit/stack.py @@ -443,8 +443,7 @@ class Series: os.makedirs(self.__patch_dir) - if not os.path.isdir(bases_dir): - os.makedirs(bases_dir) + create_dirs(bases_dir) create_empty_file(self.__applied_file) create_empty_file(self.__unapplied_file) @@ -502,11 +501,14 @@ class Series: git.rename_branch(self.__name, to_name) if os.path.isdir(self.__series_dir): - os.rename(self.__series_dir, to_stack.__series_dir) + rename(os.path.join(self.__base_dir, 'patches'), + self.__name, to_stack.__name) if os.path.exists(self.__base_file): - os.rename(self.__base_file, to_stack.__base_file) + rename(os.path.join(self.__base_dir, 'refs', 'bases'), + self.__name, to_stack.__name) if os.path.exists(self.__refs_dir): - os.rename(self.__refs_dir, to_stack.__refs_dir) + rename(os.path.join(self.__base_dir, 'refs', 'patches'), + self.__name, to_stack.__name) self.__init__(to_name) @@ -560,16 +562,19 @@ class Series: else: print 'Patch directory %s is not empty.' % self.__name if not os.listdir(self.__series_dir): - os.rmdir(self.__series_dir) + remove_dirs(os.path.join(self.__base_dir, 'patches'), + self.__name) else: print 'Series directory %s is not empty.' % self.__name if not os.listdir(self.__refs_dir): - os.rmdir(self.__refs_dir) + remove_dirs(os.path.join(self.__base_dir, 'refs', 'patches'), + self.__name) else: print 'Refs directory %s is not empty.' % self.__refs_dir if os.path.exists(self.__base_file): - os.remove(self.__base_file) + remove_file_and_dirs( + os.path.join(self.__base_dir, 'refs', 'bases'), self.__name) def refresh_patch(self, files = None, message = None, edit = False, show_patch = False, diff --git a/stgit/utils.py b/stgit/utils.py index 5749b3b..68b8f58 100644 --- a/stgit/utils.py +++ b/stgit/utils.py @@ -1,6 +1,8 @@ """Common utility functions """ +import errno, os, os.path + __copyright__ = """ Copyright (C) 2005, Catalin Marinas <catalin.marinas@xxxxxxxxx> @@ -18,6 +20,12 @@ along with this program; if not, write t Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ +def mkdir_file(filename, mode): + """Opens filename with the given mode, creating the directory it's + in if it doesn't already exist.""" + create_dirs(os.path.dirname(filename)) + return file(filename, mode) + def read_string(filename, multiline = False): """Reads the first line from a file """ @@ -32,7 +40,7 @@ def read_string(filename, multiline = Fa def write_string(filename, line, multiline = False): """Writes 'line' to file and truncates it """ - f = file(filename, 'w+') + f = mkdir_file(filename, 'w+') if multiline: f.write(line) else: @@ -42,7 +50,7 @@ def write_string(filename, line, multili def append_strings(filename, lines): """Appends 'lines' sequence to file """ - f = file(filename, 'a+') + f = mkdir_file(filename, 'a+') for line in lines: print >> f, line f.close() @@ -50,14 +58,14 @@ def append_strings(filename, lines): def append_string(filename, line): """Appends 'line' to file """ - f = file(filename, 'a+') + f = mkdir_file(filename, 'a+') print >> f, line f.close() def insert_string(filename, line): """Inserts 'line' at the beginning of the file """ - f = file(filename, 'r+') + f = mkdir_file(filename, 'r+') lines = f.readlines() f.seek(0); f.truncate() print >> f, line @@ -67,4 +75,74 @@ def insert_string(filename, line): def create_empty_file(name): """Creates an empty file """ - file(name, 'w+').close() + mkdir_file(name, 'w+').close() + +def list_files_and_dirs(path): + """Return the sets of filenames and directory names in a + directory.""" + files, dirs = [], [] + for fd in os.listdir(path): + full_fd = os.path.join(path, fd) + if os.path.isfile(full_fd): + files.append(fd) + elif os.path.isdir(full_fd): + dirs.append(fd) + return files, dirs + +def walk_tree(basedir): + """Starting in the given directory, iterate through all its + subdirectories. For each subdirectory, yield the name of the + subdirectory (relative to the base directory), the list of + filenames in the subdirectory, and the list of directory names in + the subdirectory.""" + subdirs = [''] + while subdirs: + subdir = subdirs.pop() + files, dirs = list_files_and_dirs(os.path.join(basedir, subdir)) + for d in dirs: + subdirs.append(os.path.join(subdir, d)) + yield subdir, files, dirs + +def strip_prefix(prefix, string): + """Return string, without the prefix. Blow up if string doesn't + start with prefix.""" + assert string.startswith(prefix) + return string[len(prefix):] + +def remove_dirs(basedir, dirs): + """Starting at join(basedir, dirs), remove the directory if empty, + and try the same with its parent, until we find a nonempty + directory or reach basedir.""" + path = dirs + while path: + try: + os.rmdir(os.path.join(basedir, path)) + except OSError: + return # can't remove nonempty directory + path = os.path.dirname(path) + +def remove_file_and_dirs(basedir, file): + """Remove join(basedir, file), and then remove the directory it + was in if empty, and try the same with its parent, until we find a + nonempty directory or reach basedir.""" + os.remove(os.path.join(basedir, file)) + remove_dirs(basedir, os.path.dirname(file)) + +def create_dirs(directory): + """Create the given directory, if the path doesn't already exist.""" + if directory: + create_dirs(os.path.dirname(directory)) + try: + os.mkdir(directory) + except OSError, e: + if e.errno != errno.EEXIST: + raise e + +def rename(basedir, file1, file2): + """Rename join(basedir, file1) to join(basedir, file2), not + leaving any empty directories behind and creating any directories + necessary.""" + full_file2 = os.path.join(basedir, file2) + create_dirs(os.path.dirname(full_file2)) + os.rename(os.path.join(basedir, file1), full_file2) + remove_dirs(basedir, os.path.dirname(file1)) -- 1.3.2.g639c -- Karl Hasselström, kha@xxxxxxxxxxx www.treskal.com/kalle - : send the line "unsubscribe git" in the body of a message to majordomo@xxxxxxxxxxxxxxx More majordomo info at http://vger.kernel.org/majordomo-info.html