Add the 'sync' command
authorCatalin Marinas <catalin.marinas@gmail.com>
Thu, 7 Dec 2006 22:02:42 +0000 (22:02 +0000)
committerCatalin Marinas <catalin.marinas@gmail.com>
Thu, 7 Dec 2006 22:02:42 +0000 (22:02 +0000)
This command is used to keep patches on several branches/trees in
sync.

Signed-off-by: Catalin Marinas <catalin.marinas@gmail.com>
contrib/stgit-completion.bash
stgit/commands/common.py
stgit/commands/log.py
stgit/commands/sync.py [new file with mode: 0644]
stgit/git.py
stgit/main.py
stgit/stack.py
t/t1002-branch-clone.sh
t/t2000-sync.sh [new file with mode: 0755]

index 72861f9..8c50cbc 100644 (file)
@@ -43,6 +43,7 @@ _stg_commands="
     series
     show
     status
     series
     show
     status
+    sync
     top
     unapplied
     uncommit
     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 ;;
         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
         # working-copy commands
         diff)   _stg_patches_options $command _applied_patches "-r --range" ;;
         # all the other commands
index 8a625f8..466f584 100644 (file)
@@ -109,7 +109,7 @@ def git_id(rev):
 def check_local_changes():
     if git.local_changes():
         raise CmdException, \
 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():
 
 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')):
 
 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:
 
 def print_crt_patch(branch = None):
     if not branch:
index 033c797..a21789e 100644 (file)
@@ -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
 
   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'),
 
 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:
         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_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 (file)
index 0000000..594d7de
--- /dev/null
@@ -0,0 +1,174 @@
+__copyright__ = """
+Copyright (C) 2006, Catalin Marinas <catalin.marinas@gmail.com>
+
+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] [<patch1>] [<patch2>] [<patch3>..<patch4>]
+
+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)
index 21d74dd..eb8da4e 100644 (file)
@@ -262,10 +262,10 @@ def __tree_status(files = None, tree_id = 'HEAD', unknown = False,
 
     return cache_files
 
 
     return cache_files
 
-def local_changes():
+def local_changes(verbose = True):
     """Return true if there are local changes in the tree
     """
     """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
 
 # 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
     """
     """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)
     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 filename:
             f.close()
 
+    if base:
+        orig_head = get_head()
+        switch(base)
+    else:
+        refresh_index()
+
     try:
         _input_str('git-apply --index', diff)
     except GitException:
     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()
             f = file('.stgit-failed.patch', 'w+')
             f.write(diff)
             f.close()
+            print >> sys.stderr, 'Diff written to the .stgit-failed.patch file'
 
         raise
 
 
         raise
 
index 800513b..3c8e8f4 100644 (file)
@@ -67,6 +67,7 @@ commands = Commands({
     'series':           'series',
     'show':             'show',
     'status':           'status',
     'series':           'series',
     'show':             'show',
     'status':           'status',
+    'sync':             'sync',
     'top':              'top',
     'unapplied':        'unapplied',
     'uncommit':         'uncommit'
     'top':              'top',
     'unapplied':        'unapplied',
     'uncommit':         'uncommit'
@@ -106,7 +107,8 @@ patchcommands = (
     'pick',
     'refresh',
     'rename',
     'pick',
     'refresh',
     'rename',
-    'show'
+    'show',
+    'sync'
     )
 wccommands = (
     'add',
     )
 wccommands = (
     'add',
index 2200d33..e650713 100644 (file)
@@ -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():
         # 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():
 
         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():
         # 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)
 
         git.reset()
         self.pop_patch(name)
index 12dd0e6..1d7fc39 100755 (executable)
@@ -3,7 +3,7 @@
 # Copyright (c) 2006 Catalin Marinas
 #
 
 # Copyright (c) 2006 Catalin Marinas
 #
 
-test_description='Branch renames.
+test_description='Branch cloning.
 
 Exercises branch cloning options.
 '
 
 Exercises branch cloning options.
 '
diff --git a/t/t2000-sync.sh b/t/t2000-sync.sh
new file mode 100755 (executable)
index 0000000..3dd1cd6
--- /dev/null
@@ -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