From: Catalin Marinas Date: Thu, 7 Dec 2006 22:02:42 +0000 (+0000) Subject: Add the 'sync' command X-Git-Tag: v0.14.3~388 X-Git-Url: https://git.distorted.org.uk/~mdw/stgit/commitdiff_plain/06848faba60e1c4e637b15b608e5bd94989c4196 Add the 'sync' command This command is used to keep patches on several branches/trees in sync. Signed-off-by: Catalin Marinas --- diff --git a/contrib/stgit-completion.bash b/contrib/stgit-completion.bash index 72861f9..8c50cbc 100644 --- a/contrib/stgit-completion.bash +++ b/contrib/stgit-completion.bash @@ -43,6 +43,7 @@ _stg_commands=" series show status + sync top unapplied uncommit @@ -198,6 +199,7 @@ _stg () refresh)_stg_patches_options $command _applied_patches "-p --patch" ;; rename) _stg_patches $command _all_patches ;; show) _stg_patches $command _all_patches ;; + sync) _stg_patches $command _applied_patches ;; # working-copy commands diff) _stg_patches_options $command _applied_patches "-r --range" ;; # all the other commands diff --git a/stgit/commands/common.py b/stgit/commands/common.py index 8a625f8..466f584 100644 --- a/stgit/commands/common.py +++ b/stgit/commands/common.py @@ -109,7 +109,7 @@ def git_id(rev): def check_local_changes(): if git.local_changes(): raise CmdException, \ - 'local changes in the tree. Use "refresh" to commit them' + 'local changes in the tree. Use "refresh" or "status --reset"' def check_head_top_equal(): if not crt_series.head_top_equal(): @@ -120,7 +120,9 @@ def check_head_top_equal(): def check_conflicts(): if os.path.exists(os.path.join(basedir.get(), 'conflicts')): - raise CmdException, 'Unsolved conflicts. Please resolve them first' + raise CmdException, \ + 'Unsolved conflicts. Please resolve them first or\n' \ + ' revert the changes with "status --reset"' def print_crt_patch(branch = None): if not branch: diff --git a/stgit/commands/log.py b/stgit/commands/log.py index 033c797..a21789e 100644 --- a/stgit/commands/log.py +++ b/stgit/commands/log.py @@ -36,10 +36,11 @@ can be one of the following: push(f) - the patch was fast-forwarded undo - the patch boundaries were restored to the old values -Note that only the diffs shown in the 'refresh' and 'undo' actions are -meaningful for the patch changes. The 'push' actions represent the -changes to the entire base of the current patch. Conflicts reset the -patch content and a subsequent 'refresh' will show the entire patch.""" +Note that only the diffs shown in the 'refresh', 'undo' and 'sync' +actions are meaningful for the patch changes. The 'push' actions +represent the changes to the entire base of the current +patch. Conflicts reset the patch content and a subsequent 'refresh' +will show the entire patch.""" options = [make_option('-b', '--branch', help = 'use BRANCH instead of the default one'), @@ -59,7 +60,8 @@ def show_log(log, show_patch): descr = commit.get_log().rstrip() if show_patch: - if descr.startswith('refresh') or descr.startswith('undo'): + if descr.startswith('refresh') or descr.startswith('undo') \ + or descr.startswith('sync'): diff_str = '%s%s\n' % (diff_str, git.pretty_commit(commit.get_id_hash())) else: diff --git a/stgit/commands/sync.py b/stgit/commands/sync.py new file mode 100644 index 0000000..594d7de --- /dev/null +++ b/stgit/commands/sync.py @@ -0,0 +1,174 @@ +__copyright__ = """ +Copyright (C) 2006, Catalin Marinas + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License version 2 as +published by the Free Software Foundation. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +""" + +import sys, os +from optparse import OptionParser, make_option + +import stgit.commands.common +from stgit.commands.common import * +from stgit.utils import * +from stgit import stack, git + + +help = 'synchronise patches with a branch or a series' +usage = """%prog [options] [] [] [..] + +For each of the specified patches perform a three-way merge with the +same patch in the specified branch or series. The command can be used +for keeping patches on several branches in sync. Note that the +operation may fail for some patches because of conflicts. The patches +in the series must apply cleanly. + +The sync operation can be reverted for individual patches with --undo.""" + +options = [make_option('-a', '--all', + help = 'synchronise all the patches', + action = 'store_true'), + make_option('-b', '--branch', + help = 'syncronise patches with BRANCH'), + make_option('-s', '--series', + help = 'syncronise patches with SERIES'), + make_option('--undo', + help = 'undo the synchronisation of the current patch', + action = 'store_true')] + +def __check_all(): + check_local_changes() + check_conflicts() + check_head_top_equal() + +def __branch_merge_patch(remote_series, pname): + """Merge a patch from a remote branch into the current tree. + """ + patch = remote_series.get_patch(pname) + git.merge(patch.get_bottom(), git.get_head(), patch.get_top()) + +def __series_merge_patch(base, patchdir, pname): + """Merge a patch file with the given StGIT patch. + """ + patchfile = os.path.join(patchdir, pname) + git.apply_patch(filename = patchfile, base = base) + +def func(parser, options, args): + """Synchronise a range of patches + """ + global crt_series + + if options.undo: + if options.branch or options.series: + raise CmdException, \ + '--undo cannot be specified with --branch or --series' + __check_all() + + print 'Undoing the "%s" sync...' % crt_series.get_current(), + sys.stdout.flush() + + crt_series.undo_refresh() + git.reset() + + print 'done' + return + + if options.branch: + # the main function already made crt_series to be the remote + # branch + remote_series = crt_series + stgit.commands.common.crt_series = crt_series = stack.Series() + if options.branch == crt_series.get_branch(): + raise CmdException, 'Cannot synchronise with the current branch' + remote_patches = remote_series.get_applied() + + # the merge function merge_patch(patch, pname) + merge_patch = lambda patch, pname: \ + __branch_merge_patch(remote_series, pname) + elif options.series: + patchdir = os.path.dirname(options.series) + + remote_patches = [] + f = file(options.series) + for line in f: + p = re.sub('#.*$', '', line).strip() + if not p: + continue + remote_patches.append(p) + f.close() + + # the merge function merge_patch(patch, pname) + merge_patch = lambda patch, pname: \ + __series_merge_patch(patch.get_bottom(), patchdir, pname) + else: + raise CmdException, 'No remote branch or series specified' + + applied = crt_series.get_applied() + + if options.all: + patches = applied + elif len(args) != 0: + patches = parse_patches(args, applied, ordered = True) + else: + parser.error('no patches specified') + + if not patches: + raise CmdException, 'No patches to synchronise' + + __check_all() + + # only keep the patches to be synchronised + sync_patches = [p for p in patches if p in remote_patches] + if not sync_patches: + raise CmdException, 'No common patches to be synchronised' + + # pop to the one before the first patch to be synchronised + popped = applied[applied.index(sync_patches[0]) + 1:] + if popped: + pop_patches(popped[::-1]) + + for p in sync_patches: + if p in popped: + # push to this patch + idx = popped.index(p) + 1 + push_patches(popped[:idx]) + del popped[:idx] + + # the actual sync + print 'Synchronising "%s"...' % p, + sys.stdout.flush() + + patch = crt_series.get_patch(p) + bottom = patch.get_bottom() + top = patch.get_top() + + # reset the patch backup information. That's needed in case we + # undo the sync but there were no changes made + patch.set_bottom(bottom, backup = True) + patch.set_top(top, backup = True) + + # the actual merging (either from a branch or an external file) + merge_patch(patch, p) + + if git.local_changes(verbose = False): + # index (cache) already updated by the git merge. The + # backup information was already reset above + crt_series.refresh_patch(cache_update = False, backup = False, + log = 'sync') + print 'done (updated)' + else: + print 'done' + + # push the remaining patches + if popped: + push_patches(popped) diff --git a/stgit/git.py b/stgit/git.py index 21d74dd..eb8da4e 100644 --- a/stgit/git.py +++ b/stgit/git.py @@ -262,10 +262,10 @@ def __tree_status(files = None, tree_id = 'HEAD', unknown = False, return cache_files -def local_changes(): +def local_changes(verbose = True): """Return true if there are local changes in the tree """ - return len(__tree_status(verbose = True)) != 0 + return len(__tree_status(verbose = verbose)) != 0 # HEAD value cached __head = None @@ -814,12 +814,6 @@ def apply_patch(filename = None, diff = None, base = None, """Apply a patch onto the current or given index. There must not be any local changes in the tree, otherwise the command fails """ - if base: - orig_head = get_head() - switch(base) - else: - refresh_index() - if diff is None: if filename: f = file(filename) @@ -829,6 +823,12 @@ def apply_patch(filename = None, diff = None, base = None, if filename: f.close() + if base: + orig_head = get_head() + switch(base) + else: + refresh_index() + try: _input_str('git-apply --index', diff) except GitException: @@ -839,6 +839,7 @@ def apply_patch(filename = None, diff = None, base = None, f = file('.stgit-failed.patch', 'w+') f.write(diff) f.close() + print >> sys.stderr, 'Diff written to the .stgit-failed.patch file' raise diff --git a/stgit/main.py b/stgit/main.py index 800513b..3c8e8f4 100644 --- a/stgit/main.py +++ b/stgit/main.py @@ -67,6 +67,7 @@ commands = Commands({ 'series': 'series', 'show': 'show', 'status': 'status', + 'sync': 'sync', 'top': 'top', 'unapplied': 'unapplied', 'uncommit': 'uncommit' @@ -106,7 +107,8 @@ patchcommands = ( 'pick', 'refresh', 'rename', - 'show' + 'show', + 'sync' ) wccommands = ( 'add', diff --git a/stgit/stack.py b/stgit/stack.py index 2200d33..e650713 100644 --- a/stgit/stack.py +++ b/stgit/stack.py @@ -694,7 +694,7 @@ class Series(StgitObject): # old_bottom is different, there wasn't any previous 'refresh' # command (probably only a 'push') if old_bottom != patch.get_bottom() or old_top == patch.get_top(): - raise StackException, 'No refresh undo information available' + raise StackException, 'No undo information available' git.reset(tree_id = old_top, check_out = False) if patch.restore_old_boundaries(): @@ -976,7 +976,7 @@ class Series(StgitObject): # modified by 'refresh'). If they are both unchanged, there # was a fast forward if old_bottom == patch.get_bottom() and old_top != patch.get_top(): - raise StackException, 'No push undo information available' + raise StackException, 'No undo information available' git.reset() self.pop_patch(name) diff --git a/t/t1002-branch-clone.sh b/t/t1002-branch-clone.sh index 12dd0e6..1d7fc39 100755 --- a/t/t1002-branch-clone.sh +++ b/t/t1002-branch-clone.sh @@ -3,7 +3,7 @@ # Copyright (c) 2006 Catalin Marinas # -test_description='Branch renames. +test_description='Branch cloning. Exercises branch cloning options. ' diff --git a/t/t2000-sync.sh b/t/t2000-sync.sh new file mode 100755 index 0000000..3dd1cd6 --- /dev/null +++ b/t/t2000-sync.sh @@ -0,0 +1,127 @@ +#!/bin/sh +# +# Copyright (c) 2006 Catalin Marinas +# + +test_description='Test the sync command.' + +. ./test-lib.sh + +test_expect_success \ + 'Initialize the StGIT repository' \ + ' + stg init + ' + +test_expect_success \ + 'Create some patches' \ + ' + stg new p1 -m p1 && + echo foo1 > foo1.txt && + stg add foo1.txt && + stg refresh && + stg new p2 -m p2 && + echo foo2 > foo2.txt && + stg add foo2.txt && + stg refresh && + stg new p3 -m p3 && + echo foo3 > foo3.txt && + stg add foo3.txt && + stg refresh && + stg export && + stg pop + ' + +test_expect_success \ + 'Create a branch with empty patches' \ + ' + stg branch -c foo base && + stg new p1 -m p1 && + stg new p2 -m p2 && + stg new p3 -m p3 + test $(stg applied -c) -eq 3 + ' + +test_expect_success \ + 'Synchronise second patch with the master branch' \ + ' + stg sync -b master p2 && + test $(stg applied -c) -eq 3 && + test $(cat foo2.txt) == "foo2" + ' + +test_expect_success \ + 'Synchronise the first two patches with the master branch' \ + ' + stg sync -b master -a && + test $(stg applied -c) -eq 3 && + test $(cat foo1.txt) == "foo1" && + test $(cat foo2.txt) == "foo2" + ' + +test_expect_success \ + 'Synchronise all the patches with the exported series' \ + ' + stg sync -s patches-master/series -a && + test $(stg applied -c) -eq 3 && + test $(cat foo1.txt) == "foo1" && + test $(cat foo2.txt) == "foo2" && + test $(cat foo3.txt) == "foo3" + ' + +test_expect_success \ + 'Modify the master patches' \ + ' + stg branch master && + stg goto p1 && + echo bar1 >> foo1.txt && + stg refresh && + stg goto p2 && + echo bar2 > bar2.txt && + stg add bar2.txt && + stg refresh && + stg goto p3 && + echo bar3 >> foo3.txt && + stg refresh && + stg export && + stg branch foo + ' + +test_expect_success \ + 'Synchronise second patch with the master branch' \ + ' + stg sync -b master p2 && + test $(stg applied -c) -eq 3 && + test $(cat bar2.txt) == "bar2" + ' + +test_expect_failure \ + 'Synchronise the first two patches with the master branch (to fail)' \ + ' + stg sync -b master -a + ' + +test_expect_success \ + 'Restore the stack status after the failed sync' \ + ' + test $(stg applied -c) -eq 1 && + stg resolved -a && + stg refresh && + stg goto p3 + ' + +test_expect_failure \ + 'Synchronise the third patch with the exported series (to fail)' \ + ' + stg sync -s patches-master/series p3 + ' + +test_expect_success \ + 'Restore the stack status after the failed sync' \ + ' + test $(stg applied -c) -eq 3 && + stg resolved -a && + stg refresh + ' + +test_done