[merge] bzr missing replacement

Martin Pool mbp at sourcefrog.net
Mon Dec 12 03:26:48 GMT 2005


On 11 Dec 2005, Aaron Bentley <aaron.bentley at utoronto.ca> wrote:
> Please merge from http://panoramicfeedback.com/opensource/bzr.ab
> 
> This contains a replacement for the merge command, based on work by
> Daniel Silverstone.

That is, "missing" command.

Here is the diff against my tree, for anyone that wants to review it.

I'm happy to merge this in.

-- 
Martin
-------------- next part --------------
=== added file 'bzrlib/tests/blackbox/test_missing.py'
--- /dev/null	
+++ bzrlib/tests/blackbox/test_missing.py	
@@ -0,0 +1,82 @@
+"""Black-box tests for bzr missing.
+"""
+
+import os
+
+from bzrlib.branch import Branch
+from bzrlib.tests import TestCaseInTempDir
+
+class TestMissing(TestCaseInTempDir):
+    def test_missing(self):
+        missing = "You are missing 1 revision(s):"
+
+        # create a source branch
+        os.mkdir('a')
+        os.chdir('a')
+        self.capture('init')
+        open('a', 'wb').write('initial\n')
+        self.capture('add a')
+        self.capture('commit -m inital')
+
+        # clone and add a differing revision
+        self.capture('branch . ../b')
+        os.chdir('../b')
+        open('a', 'ab').write('more\n')
+        self.capture('commit -m more')
+
+        # compare a against b
+        os.chdir('../a')
+        lines = self.capture('missing ../b', retcode=1).splitlines()
+        # we're missing the extra revision here
+        self.assertEqual(missing, lines[0])
+        self.assertEqual(8, len(lines))
+
+        # get extra revision from b
+        self.capture('merge ../b')
+        self.capture('commit -m merge')
+
+        # compare again, but now we have the 'merge' commit extra
+        lines = self.capture('missing ../b', retcode=1).splitlines()
+        self.assertEqual("You have 1 extra revision(s):", lines[0])
+        self.assertEqual(8, len(lines))
+        lines2 = self.capture('missing ../b --mine-only', retcode=1)
+        lines2 = lines2.splitlines()
+        self.assertEqual(lines, lines2)
+        lines3 = self.capture('missing ../b --theirs-only', retcode=1)
+        lines3 = lines3.splitlines()
+        self.assertEqual(0, len(lines3))
+
+        # relative to a, missing the 'merge' commit 
+        os.chdir('../b')
+        lines = self.capture('missing ../a', retcode=1).splitlines()
+        self.assertEqual(missing, lines[0])
+        self.assertEqual(8, len(lines))
+        lines2 = self.capture('missing ../a --theirs-only', retcode=1)
+        lines2 = lines2.splitlines()
+        self.assertEqual(lines, lines2)
+        lines3 = self.capture('missing ../a --mine-only', retcode=1)
+        lines3 = lines3.splitlines()
+        self.assertEqual(0, len(lines3))
+        lines4 = self.capture('missing ../a --short', retcode=1)
+        lines4 = lines4.splitlines()
+        self.assertEqual(4, len(lines4))
+        lines5 = self.capture('missing ../a --line', retcode=1)
+        lines5 = lines5.splitlines()
+        self.assertEqual(2, len(lines5))
+        lines6 = self.capture('missing ../a --reverse', retcode=1)
+        lines6 = lines6.splitlines()
+        self.assertEqual(lines6, lines)
+        lines7 = self.capture('missing ../a --show-ids', retcode=1)
+        lines7 = lines7.splitlines()
+        self.assertEqual(11, len(lines7))
+        lines8 = self.capture('missing ../a --verbose', retcode=1)
+        lines8 = lines8.splitlines()
+        self.assertEqual("modified:", lines8[-2])
+        self.assertEqual("  a", lines8[-1])
+
+        
+        # after a pull we're back on track
+        self.capture('pull')
+        self.assertEqual("Branches are up to date.\n", 
+                         self.capture('missing ../a'))
+

=== added file 'bzrlib/tests/test_missing.py'
--- /dev/null	
+++ bzrlib/tests/test_missing.py	
@@ -0,0 +1,49 @@
+# Copyright (C) 2005 by Canonical Ltd
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+
+# 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
+from bzrlib.missing import find_unmerged
+from bzrlib.branch import Branch
+from bzrlib.merge import merge
+from bzrlib.tests import TestCaseInTempDir
+import os
+
+class TestMissing(TestCaseInTempDir):
+    def test_find_unmerged(self):
+        os.mkdir('original')
+        os.mkdir('puller')
+        os.mkdir('merger')
+        original = Branch.initialize('original')
+        puller = Branch.initialize('puller')
+        merger = Branch.initialize('merger')
+        self.assertEqual(find_unmerged(original, puller), ([], []))
+        original.working_tree().commit('a', rev_id='a')
+        self.assertEqual(find_unmerged(original, puller), ([(1, u'a')], []))
+        puller.pull(original)
+        self.assertEqual(find_unmerged(original, puller), ([], []))
+        merger.pull(original)
+        original.working_tree().commit('b', rev_id='b')
+        original.working_tree().commit('c', rev_id='c')
+        self.assertEqual(find_unmerged(original, puller), ([(2, u'b'), 
+                                                            (3, u'c')], []))
+
+        puller.pull(original)
+        self.assertEqual(find_unmerged(original, puller), ([], []))
+        self.assertEqual(find_unmerged(original, merger), ([(2, u'b'), 
+                                                            (3, u'c')], []))
+        merge(['original', -1], [None, None], this_dir='merger')
+        self.assertEqual(find_unmerged(original, merger), ([(2, u'b'), 
+                                                            (3, u'c')], []))
+        merger.working_tree().commit('d', rev_id='d')
+        self.assertEqual(find_unmerged(original, merger), ([], [(2, 'd')]))

=== modified file 'bzrlib/builtins.py'
--- bzrlib/builtins.py	
+++ bzrlib/builtins.py	
@@ -36,6 +36,7 @@
 import bzrlib.trace
 from bzrlib.trace import mutter, note, log_error, warning, is_quiet
 from bzrlib.workingtree import WorkingTree
+from bzrlib.log import show_one_log
 
 
 def tree_files(file_list, default_branch=u'.'):
@@ -868,12 +869,11 @@
                             help='show from oldest to newest'),
                      'timezone', 'verbose', 
                      'show-ids', 'revision',
-                     Option('line', help='format with one line per revision'),
-                     'long', 
+                     'line', 'long', 
                      Option('message',
                             help='show revisions whose message matches this regexp',
                             type=str),
-                     Option('short', help='use moderately short format'),
+                     'short',
                      ]
     @display_command
     def run(self, filename=None, timezone='original',
@@ -937,11 +937,7 @@
         # in e.g. the default C locale.
         outf = codecs.getwriter(bzrlib.user_encoding)(sys.stdout, errors='replace')
 
-        log_format = 'long'
-        if short:
-            log_format = 'short'
-        if line:
-            log_format = 'line'
+        log_format = get_log_format(long=long, short=short, line=line)
         lf = log_formatter(log_format,
                            show_ids=show_ids,
                            to_file=outf,
@@ -956,6 +952,15 @@
                  end_revision=rev2,
                  search=message)
 
+def get_log_format(long=False, short=False, line=False, default='long'):
+    log_format = default
+    if long:
+        log_format = 'long'
+    if short:
+        log_format = 'short'
+    if line:
+        log_format = 'line'
+    return log_format
 
 
 class cmd_touching_revisions(Command):
@@ -1735,39 +1740,64 @@
 
 
 class cmd_missing(Command):
-    """What is missing in this branch relative to other branch.
-    """
-    # TODO: rewrite this in terms of ancestry so that it shows only
-    # unmerged things
-    
-    takes_args = ['remote?']
-    aliases = ['mis', 'miss']
-    takes_options = ['verbose']
-
-    @display_command
-    def run(self, remote=None, verbose=False):
-        from bzrlib.errors import BzrCommandError
-        from bzrlib.missing import show_missing
-
-        if verbose and is_quiet():
-            raise BzrCommandError('Cannot pass both quiet and verbose')
-
-        tree = WorkingTree.open_containing(u'.')[0]
-        parent = tree.branch.get_parent()
-        if remote is None:
-            if parent is None:
+    """Show unmerged/unpulled revisions between two branches.
+
+    OTHER_BRANCH may be local or remote."""
+    takes_args = ['other_branch?']
+    takes_options = [Option('reverse', 'Reverse the order of revisions'),
+                     Option('mine-only', 
+                            'Display changes in the local branch only'),
+                     Option('theirs-only', 
+                            'Display changes in the remote branch only'), 
+                     'line',
+                     'long', 
+                     'short',
+                     'show-ids',
+                     'verbose'
+                     ]
+
+    def run(self, other_branch=None, reverse=False, mine_only=False,
+            theirs_only=False, long=True, short=False, line=False, 
+            show_ids=False, verbose=False):
+        from bzrlib.missing import find_unmerged, iter_log_data
+        from bzrlib.log import log_formatter
+        local_branch = bzrlib.branch.Branch.open_containing(".")[0]
+        parent = local_branch.get_parent()
+        if other_branch is None:
+            other_branch = parent
+            if other_branch is None:
                 raise BzrCommandError("No missing location known or specified.")
-            else:
-                if not is_quiet():
-                    print "Using last location: %s" % parent
-                remote = parent
-        elif parent is None:
-            # We only update parent if it did not exist, missing
-            # should not change the parent
-            tree.branch.set_parent(remote)
-        br_remote = Branch.open_containing(remote)[0]
-        return show_missing(tree.branch, br_remote, verbose=verbose, 
-                            quiet=is_quiet())
+            print "Using last location: " + local_branch.get_parent()
+        remote_branch = bzrlib.branch.Branch.open(other_branch)
+        local_extra, remote_extra = find_unmerged(local_branch, remote_branch)
+        log_format = get_log_format(long=long, short=short, line=line)
+        lf = log_formatter(log_format, sys.stdout,
+                           show_ids=show_ids,
+                           show_timezone='original')
+        if reverse is False:
+            local_extra.reverse()
+            remote_extra.reverse()
+        if local_extra and not theirs_only:
+            print "You have %d extra revision(s):" % len(local_extra)
+            for data in iter_log_data(local_extra, local_branch, verbose):
+                lf.show(*data)
+            printed_local = True
+        else:
+            printed_local = False
+        if remote_extra and not mine_only:
+            if printed_local is True:
+                print "\n\n"
+            print "You are missing %d revision(s):" % len(remote_extra)
+            for data in iter_log_data(remote_extra, remote_branch, verbose):
+                lf.show(*data)
+        if not remote_extra and not local_extra:
+            status_code = 0
+            print "Branches are up to date."
+        else:
+            status_code = 1
+        if parent is None and other_branch is not None:
+            local_branch.set_parent(other_branch)
+        return status_code
 
 
 class cmd_plugins(Command):

=== modified file 'bzrlib/errors.py'
--- bzrlib/errors.py	
+++ bzrlib/errors.py	
@@ -142,6 +142,11 @@
     # Error from malformed user command
     # This is being misused as a generic exception
     # pleae subclass. RBC 20051030
+    #
+    # I think it's a waste of effort to differentiate between errors that
+    # are not intended to be caught anyway.  UI code need not subclass
+    # BzrCommandError, and non-UI code should not throw a subclass of
+    # BzrCommandError.  ADHB 20051211
     def __str__(self):
         return self.args[0]
 

=== modified file 'bzrlib/missing.py'
--- bzrlib/missing.py	
+++ bzrlib/missing.py	
@@ -1,77 +1,102 @@
 """\
 A plugin for displaying what revisions are in 'other' but not in local.
 """
-
-def show_missing(br_local, br_remote, verbose=False, quiet=False):
-    """Show the revisions which exist in br_remote, that 
-    do not exist in br_local.
-    """
-    from bzrlib.log import show_one_log
-    import sys
-    local_history = br_local.revision_history()
-    remote_history = br_remote.revision_history()
-    if local_history == remote_history:
-        if not quiet:
-            print 'Trees are identical.'
-        return 0
-    if local_history[:len(remote_history)] == remote_history:
-        # Local is not missing anything from remote, so consider it
-        # up-to-date
-        if not quiet:
-            print 'Local tree has all of remote revisions (remote is missing local)'
-        return 0
-    if quiet:
-        return 1
-
-    # Check for divergence
-    common_idx = min(len(local_history), len(remote_history)) - 1
-    if common_idx >= 0 and local_history[common_idx] != remote_history[common_idx]:
-        print 'Trees have diverged'
-
-    local_rev_set = set(local_history)
-
-    # Find the last common revision between the two trees
-    revno = 0
-    for revno, (local_rev, remote_rev) in enumerate(zip(local_history, remote_history)):
-        if local_rev != remote_rev:
-            break
-
-    missing_remote = []
-    for rno, rev_id in enumerate(remote_history[revno:]):
-        # This assumes that you can have a revision in the
-        # local history, which does not have the same ancestry
-        # as the remote ancestry.
-        # This may or may not be possible.
-        # In the future this should also checked for merged revisions.
-        if rev_id not in local_rev_set:
-            missing_remote.append((rno+revno+1, rev_id))
-
-    print 'Missing %d revisions' %  len(missing_remote)
-    print
-
-    if verbose:
-        from bzrlib.diff import compare_trees
-        from bzrlib.tree import EmptyTree
-        show_ids = True
-        last_tree = EmptyTree
-        last_rev_id = None
-    else:
-        show_ids = False
-    for revno, rev_id in missing_remote:
-        rev = br_remote.get_revision(rev_id)
+from bzrlib.ui import ui_factory
+def iter_log_data(revisions, revision_source, verbose):
+    from bzrlib.diff import compare_trees
+    from bzrlib.tree import EmptyTree
+    last_tree = EmptyTree
+    last_rev_id = None
+    for revno, rev_id in revisions:
+        rev = revision_source.get_revision(rev_id)
         if verbose:
             parent_rev_id = rev.parent_ids[0]
             if last_rev_id == parent_rev_id:
                 parent_tree = last_tree
             else:
-                parent_tree = br_remote.revision_tree(parent_rev_id)
-            revision_tree = br_remote.revision_tree(rev_id)
+                parent_tree = revision_source.revision_tree(parent_rev_id)
+            revision_tree = revision_source.revision_tree(rev_id)
             last_rev_id = rev_id
             last_tree = revision_tree
             delta = compare_trees(revision_tree, parent_tree)
         else:
             delta = None
+        yield revno, rev, delta
 
-        show_one_log(revno, rev, delta, verbose, sys.stdout, 'original')
-    return 1
 
+def find_unmerged(local_branch, remote_branch):
+    progress = ui_factory.progress_bar()
+    local_branch.lock_read()
+    try:
+        remote_branch.lock_read()
+        try:
+            local_rev_history, local_rev_history_map = \
+                _get_history(local_branch, progress, "local", 0)
+            remote_rev_history, remote_rev_history_map = \
+                _get_history(remote_branch, progress, "remote", 1)
+            result = _shortcut(local_rev_history, remote_rev_history)
+            if result is not None:
+                local_extra, remote_extra = result
+                local_extra = sorted_revisions(local_extra, 
+                                               local_rev_history_map)
+                remote_extra = sorted_revisions(remote_extra, 
+                                                remote_rev_history_map)
+                return local_extra, remote_extra
+
+            local_ancestry = _get_ancestry(local_branch, progress, "local",
+                                           2, local_rev_history)
+            remote_ancestry = _get_ancestry(remote_branch, progress, "remote",
+                                            3, remote_rev_history)
+            progress.update('pondering', 4, 5)
+            extras = local_ancestry.symmetric_difference(remote_ancestry) 
+            local_extra = extras.intersection(set(local_rev_history))
+            remote_extra = extras.intersection(set(remote_rev_history))
+            local_extra = sorted_revisions(local_extra, local_rev_history_map)
+            remote_extra = sorted_revisions(remote_extra, 
+                                            remote_rev_history_map)
+                    
+        finally:
+            remote_branch.unlock()
+    finally:
+        local_branch.unlock()
+        progress.clear()
+    return (local_extra, remote_extra)
+
+def _shortcut(local_rev_history, remote_rev_history):
+    local_history = set(local_rev_history)
+    remote_history = set(remote_rev_history)
+    if len(local_rev_history) == 0:
+        return set(), remote_history
+    elif len(remote_rev_history) == 0:
+        return local_history, set()
+    elif local_rev_history[-1] in remote_history:
+        return set(), _after(remote_rev_history, local_rev_history)
+    elif remote_rev_history[-1] in local_history:
+        return _after(local_rev_history, remote_rev_history), set()
+    else:
+        return None
+
+def _after(larger_history, smaller_history):
+    return set(larger_history[larger_history.index(smaller_history[-1])+1:])
+
+def _get_history(branch, progress, label, step):
+    progress.update('%s history' % label, step, 5)
+    rev_history = branch.revision_history()
+    rev_history_map = dict(
+        [(rev, rev_history.index(rev) + 1)
+         for rev in rev_history])
+    return rev_history, rev_history_map
+
+def _get_ancestry(branch, progress, label, step, rev_history):
+    progress.update('%s ancestry' % label, step, 5)
+    if len(rev_history) > 0:
+        ancestry = set(branch.get_ancestry(rev_history[-1]))
+    else:
+        ancestry = set()
+    return ancestry
+    
+
+def sorted_revisions(revisions, history_map):
+    revisions = [(history_map[r],r) for r in revisions]
+    revisions.sort()
+    return revisions

=== modified file 'bzrlib/option.py'
--- bzrlib/option.py	
+++ bzrlib/option.py	
@@ -191,7 +191,9 @@
 _global_option('version')
 _global_option('email')
 _global_option('update')
-_global_option('long')
+_global_option('long', help='Use detailed log format')
+_global_option('short', help='Use moderately short log format')
+_global_option('line', help='Use log format with one line per revision')
 _global_option('root', type=str)
 _global_option('no-backup')
 _global_option('merge-type', type=_parse_merge_type)

=== modified file 'bzrlib/tests/__init__.py'
--- bzrlib/tests/__init__.py	
+++ bzrlib/tests/__init__.py	
@@ -659,6 +659,14 @@
                    'bzrlib.tests.test_testament',
                    'bzrlib.tests.test_trace',
                    'bzrlib.tests.test_tsort',
+<<<<<<< TREE
+=======
+                   'bzrlib.tests.test_trace',
+                   'bzrlib.tests.test_rio',
+                   'bzrlib.tests.test_msgeditor',
+                   'bzrlib.tests.test_selftest',
+                   'bzrlib.tests.test_missing',
+>>>>>>> MERGE-SOURCE
                    ]
 
     print '%10s: %s' % ('bzr', os.path.realpath(sys.argv[0]))

=== modified file 'bzrlib/tests/blackbox/__init__.py'
--- bzrlib/tests/blackbox/__init__.py	
+++ bzrlib/tests/blackbox/__init__.py	
@@ -33,6 +33,7 @@
                      'bzrlib.tests.blackbox.test_pull',
                      'bzrlib.tests.blackbox.test_revno',
                      'bzrlib.tests.blackbox.test_versioning',
+                     'bzrlib.tests.blackbox.test_missing'
                      ]
     return TestLoader().loadTestsFromNames(testmod_names)
 

-------------- next part --------------
A non-text attachment was scrubbed...
Name: not available
Type: application/pgp-signature
Size: 189 bytes
Desc: Digital signature
Url : https://lists.ubuntu.com/archives/bazaar/attachments/20051212/e0c6a457/attachment.pgp 


More information about the bazaar mailing list