[PATCH RFC] subrepo: Add the '--thin' option to pull a thin strand of changes

Alistair Bell alistair.bell at netronome.com
Mon May 16 14:46:01 CDT 2011


# HG changeset patch
# User Alistair Bell <alistair.bell at netronome.com>
# Date 1305575046 14400
# Node ID 4d759579fb076f37cdf99d84ff59ceffeaefcf2a
# Parent  85c82ebc96a3611890b716f2ca5534cb92c6dbd5
subrepo: Add the '--thin' option to pull a thin strand of changes.

The idea of 'hg pull -r' or 'hg clone -r' is to pull a partial history of the
repo. An example may be to pull only those changes that are asserted to be
'good' from a 'dirty' repo to a 'clean' repo. Use of '--thin' extends this
functionality to subrepos. With 'hg update --thin' or 'hg pull -r <rev> -u
--thin' only the good data is pulled from subrepos, and other history is left
alone.

It could be argued that this should be the default behavior of 'hg pull -r' and
'hg clone -r' in the presence of subrepos and that the current behavior is a
bug, but for least surprise/backwards compatibility the current behavior is
retained as the default, with the good data only pulled with --thin.

diff -r 85c82ebc96a3 -r 4d759579fb07 mercurial/commands.py
--- a/mercurial/commands.py	Mon May 16 13:06:48 2011 +0200
+++ b/mercurial/commands.py	Mon May 16 15:44:06 2011 -0400
@@ -956,6 +956,7 @@
     ('r', 'rev', [], _('include the specified changeset'), _('REV')),
     ('b', 'branch', [], _('clone only the specified branch'), _('BRANCH')),
     ('', 'pull', None, _('use pull protocol to copy metadata')),
+    ('', 'thin', None, _('only pull needed data from subrepos')),
     ('', 'uncompressed', None, _('use uncompressed transfer (fast over LAN)')),
     ] + remoteopts,
     _('[OPTION]... SOURCE [DEST]'))
@@ -1031,7 +1032,8 @@
                  stream=opts.get('uncompressed'),
                  rev=opts.get('rev'),
                  update=opts.get('updaterev') or not opts.get('noupdate'),
-                 branch=opts.get('branch'))
+                 branch=opts.get('branch'),
+                 thinsubrepos=opts.get('thin'))
 
     return r is None
 
@@ -3421,7 +3423,8 @@
     ('t', 'tool', '', _('specify merge tool')),
     ('r', 'rev', '', _('revision to merge'), _('REV')),
     ('P', 'preview', None,
-     _('review revisions to merge (no merge is performed)'))],
+     _('review revisions to merge (no merge is performed)')),
+    ('', 'thin', None, _('only pull needed data from subrepos'))],
     _('[-P] [-f] [[-r] REV]'))
 def merge(ui, repo, node=None, **opts):
     """merge working directory with another revision
@@ -3501,7 +3504,8 @@
     try:
         # ui.forcemerge is an internal variable, do not document
         ui.setconfig('ui', 'forcemerge', opts.get('tool', ''))
-        return hg.merge(repo, node, force=opts.get('force'))
+        return hg.merge(repo, node, force=opts.get('force'),
+                        thinsubrepos=opts.get('thin'))
     finally:
         ui.setconfig('ui', 'forcemerge', '')
 
@@ -3628,12 +3632,12 @@
             else:
                 ui.write("%s = %s\n" % (name, util.hidepassword(path)))
 
-def postincoming(ui, repo, modheads, optupdate, checkout):
+def postincoming(ui, repo, modheads, optupdate, checkout, thinsubrepos=False):
     if modheads == 0:
         return
     if optupdate:
         if (modheads <= 1 or len(repo.branchheads()) == 1) or checkout:
-            return hg.update(repo, checkout)
+            return hg.update(repo, checkout, thinsubrepos=thinsubrepos)
         else:
             ui.status(_("not updating, since new heads added\n"))
     if modheads > 1:
@@ -3655,6 +3659,7 @@
     ('B', 'bookmark', [], _("bookmark to pull"), _('BOOKMARK')),
     ('b', 'branch', [], _('a specific branch you would like to pull'),
      _('BRANCH')),
+    ('', 'thin', None, _('only pull needed data from subrepos')),
     ] + remoteopts,
     _('[-u] [-f] [-r REV]... [-e CMD] [--remotecmd CMD] [SOURCE]'))
 def pull(ui, repo, source="default", **opts):
@@ -3705,7 +3710,8 @@
         checkout = str(repo.changelog.rev(other.lookup(checkout)))
     repo._subtoppath = source
     try:
-        ret = postincoming(ui, repo, modheads, opts.get('update'), checkout)
+        ret = postincoming(ui, repo, modheads, opts.get('update'), checkout,
+                           opts.get('thin'))
 
     finally:
         del repo._subtoppath
@@ -4900,7 +4906,8 @@
 
 @command('unbundle',
     [('u', 'update', None,
-     _('update to new branch head if changesets were unbundled'))],
+     _('update to new branch head if changesets were unbundled')),
+     ('', 'thin', None, _('only pull needed data from subrepos'))],
     _('[-u] FILE...'))
 def unbundle(ui, repo, fname1, *fnames, **opts):
     """apply one or more changegroup files
@@ -4923,16 +4930,19 @@
         bookmarks.updatecurrentbookmark(repo, wc.node(), wc.branch())
     finally:
         lock.release()
-    return postincoming(ui, repo, modheads, opts.get('update'), None)
+    return postincoming(ui, repo, modheads, opts.get('update'), None,
+                        opts.get('thin'))
 
 @command('^update|up|checkout|co',
     [('C', 'clean', None, _('discard uncommitted changes (no backup)')),
     ('c', 'check', None,
      _('update across branches if no uncommitted changes')),
     ('d', 'date', '', _('tipmost revision matching date'), _('DATE')),
-    ('r', 'rev', '', _('revision'), _('REV'))],
+    ('r', 'rev', '', _('revision'), _('REV')),
+    ('', 'thin', None, _('only pull needed data from subrepos'))],
     _('[-c] [-C] [-d DATE] [[-r] REV]'))
-def update(ui, repo, node=None, rev=None, clean=False, date=None, check=False):
+def update(ui, repo, node=None, rev=None, clean=False, date=None, check=False,
+           thin=False):
     """update working directory (or switch revisions)
 
     Update the repository's working directory to the specified
@@ -4998,9 +5008,9 @@
         rev = cmdutil.finddate(ui, repo, date)
 
     if clean or check:
-        ret = hg.clean(repo, rev)
+        ret = hg.clean(repo, rev, thinsubrepos=thin)
     else:
-        ret = hg.update(repo, rev)
+        ret = hg.update(repo, rev, thinsubrepos=thin)
 
     if brev in repo._bookmarks:
         bookmarks.setcurrent(repo, brev)
diff -r 85c82ebc96a3 -r 4d759579fb07 mercurial/hg.py
--- a/mercurial/hg.py	Mon May 16 13:06:48 2011 +0200
+++ b/mercurial/hg.py	Mon May 16 15:44:06 2011 -0400
@@ -170,7 +170,7 @@
         _update(r, uprev)
 
 def clone(ui, source, dest=None, pull=False, rev=None, update=True,
-          stream=False, branch=None):
+          stream=False, branch=None, thinsubrepos=False):
     """Make a copy of an existing repository.
 
     Create a copy of an existing repository in a new directory.  The
@@ -355,7 +355,7 @@
                         continue
                 bn = dest_repo[uprev].branch()
                 dest_repo.ui.status(_("updating to branch %s\n") % bn)
-                _update(dest_repo, uprev)
+                _update(dest_repo, uprev, thinsubrepos)
 
         # clone all bookmarks
         if dest_repo.local() and src_repo.capable("pushkey"):
@@ -382,9 +382,10 @@
     repo.ui.status(_("%d files updated, %d files merged, "
                      "%d files removed, %d files unresolved\n") % stats)
 
-def update(repo, node):
+def update(repo, node, thinsubrepos=False):
     """update the working directory to node, merging linear changes"""
-    stats = mergemod.update(repo, node, False, False, None)
+    stats = mergemod.update(repo, node, False, False, None,
+                            thinsubrepos=thinsubrepos)
     _showstats(repo, stats)
     if stats[3]:
         repo.ui.status(_("use 'hg resolve' to retry unresolved file merges\n"))
@@ -393,17 +394,19 @@
 # naming conflict in clone()
 _update = update
 
-def clean(repo, node, show_stats=True):
+def clean(repo, node, show_stats=True, thinsubrepos=False):
     """forcibly switch the working directory to node, clobbering changes"""
-    stats = mergemod.update(repo, node, False, True, None)
+    stats = mergemod.update(repo, node, False, True, None,
+                            thinsubrepos=thinsubrepos)
     if show_stats:
         _showstats(repo, stats)
     return stats[3] > 0
 
-def merge(repo, node, force=None, remind=True):
+def merge(repo, node, force=None, remind=True, thinsubrepos=False):
     """Branch merge with node, resolving changes. Return true if any
     unresolved conflicts."""
-    stats = mergemod.update(repo, node, True, force, False)
+    stats = mergemod.update(repo, node, True, force, False,
+                            thinsubrepos=thinsubrepos)
     _showstats(repo, stats)
     if stats[3]:
         repo.ui.status(_("use 'hg resolve' to retry unresolved file merges "
diff -r 85c82ebc96a3 -r 4d759579fb07 mercurial/merge.py
--- a/mercurial/merge.py	Mon May 16 13:06:48 2011 +0200
+++ b/mercurial/merge.py	Mon May 16 15:44:06 2011 -0400
@@ -255,7 +255,7 @@
 def actionkey(a):
     return a[1] == 'r' and -1 or 0, a
 
-def applyupdates(repo, action, wctx, mctx, actx, overwrite):
+def applyupdates(repo, action, wctx, mctx, actx, overwrite, thinsubrepos=False):
     """apply the merge action list to the working directory
 
     wctx is the working copy context
@@ -315,7 +315,8 @@
             repo.ui.note(_("removing %s\n") % f)
             audit_path(f)
             if f == '.hgsubstate': # subrepo states need updating
-                subrepo.submerge(repo, wctx, mctx, wctx, overwrite)
+                subrepo.submerge(repo, wctx, mctx, wctx, overwrite,
+                                 thinsubrepos=thinsubrepos)
             try:
                 util.unlinkpath(repo.wjoin(f))
             except OSError, inst:
@@ -325,7 +326,8 @@
             removed += 1
         elif m == "m": # merge
             if f == '.hgsubstate': # subrepo states need updating
-                subrepo.submerge(repo, wctx, mctx, wctx.ancestor(mctx), overwrite)
+                subrepo.submerge(repo, wctx, mctx, wctx.ancestor(mctx),
+                                 overwrite, thinsubrepos=thinsubrepos)
                 continue
             f2, fd, flags, move = a[2:]
             r = ms.resolve(fd, wctx, mctx)
@@ -349,7 +351,8 @@
             t = None
             updated += 1
             if f == '.hgsubstate': # subrepo states need updating
-                subrepo.submerge(repo, wctx, mctx, wctx, overwrite)
+                subrepo.submerge(repo, wctx, mctx, wctx, overwrite,
+                                 thinsubrepos=thinsubrepos)
         elif m == "d": # directory rename
             f2, fd, flags = a[2:]
             if f:
@@ -438,7 +441,8 @@
                 if f:
                     repo.dirstate.forget(f)
 
-def update(repo, node, branchmerge, force, partial, ancestor=None):
+def update(repo, node, branchmerge, force, partial, ancestor=None,
+           thinsubrepos=False):
     """
     Perform a merge between the working directory and the given node
 
@@ -546,7 +550,8 @@
         if not partial:
             repo.hook('preupdate', throw=True, parent1=xp1, parent2=xp2)
 
-        stats = applyupdates(repo, action, wc, p2, pa, overwrite)
+        stats = applyupdates(repo, action, wc, p2, pa, overwrite,
+                             thinsubrepos=thinsubrepos)
 
         if not partial:
             repo.dirstate.setparents(fp1, fp2)
diff -r 85c82ebc96a3 -r 4d759579fb07 mercurial/subrepo.py
--- a/mercurial/subrepo.py	Mon May 16 13:06:48 2011 +0200
+++ b/mercurial/subrepo.py	Mon May 16 15:44:06 2011 -0400
@@ -83,7 +83,7 @@
                 ''.join(['%s %s\n' % (state[s][1], s)
                          for s in sorted(state)]), '')
 
-def submerge(repo, wctx, mctx, actx, overwrite):
+def submerge(repo, wctx, mctx, actx, overwrite, thinsubrepos=False):
     """delegated from merge.applyupdates: merging of .hgsubstate file
     in working context, merging context and ancestor context"""
     if mctx == actx: # backwards?
@@ -93,7 +93,8 @@
     sa = actx.substate
     sm = {}
 
-    repo.ui.debug("subrepo merge %s %s %s\n" % (wctx, mctx, actx))
+    repo.ui.debug("subrepo merge %s %s %s %s\n" %
+                  (wctx, mctx, actx, thinsubrepos))
 
     def debug(s, msg, r=""):
         if r:
@@ -115,7 +116,7 @@
                 continue
             elif ld == a: # other side changed
                 debug(s, "other changed, get", r)
-                wctx.sub(s).get(r, overwrite)
+                wctx.sub(s).get(r, overwrite, thinsubrepos)
                 sm[s] = r
             elif ld[0] != r[0]: # sources differ
                 if repo.ui.promptchoice(
@@ -124,15 +125,15 @@
                       % (s, l[0], r[0]),
                       (_('&Local'), _('&Remote')), 0):
                     debug(s, "prompt changed, get", r)
-                    wctx.sub(s).get(r, overwrite)
+                    wctx.sub(s).get(r, overwrite, thinsubrepos)
                     sm[s] = r
             elif ld[1] == a[1]: # local side is unchanged
                 debug(s, "other side changed, get", r)
-                wctx.sub(s).get(r, overwrite)
+                wctx.sub(s).get(r, overwrite, thinsubrepos)
                 sm[s] = r
             else:
                 debug(s, "both sides changed, merge with", r)
-                wctx.sub(s).merge(r)
+                wctx.sub(s).merge(r, thinsubrepos)
                 sm[s] = l
         elif ld == a: # remote removed, local unchanged
             debug(s, "remote removed, remove")
@@ -150,7 +151,7 @@
             continue
         elif s not in sa:
             debug(s, "remote added, get", r)
-            mctx.sub(s).get(r)
+            mctx.sub(s).get(r, False, thinsubrepos)
             sm[s] = r
         elif r != sa[s]:
             if repo.ui.promptchoice(
@@ -158,7 +159,7 @@
                   'use (c)hanged version or (d)elete?') % s,
                 (_('&Changed'), _('&Delete')), 0) == 0:
                 debug(s, "prompt recreate", r)
-                wctx.sub(s).get(r)
+                wctx.sub(s).get(r, False, thinsubrepos)
                 sm[s] = r
 
     # record merged .hgsubstate
@@ -270,13 +271,13 @@
         """
         raise NotImplementedError
 
-    def get(self, state, overwrite=False):
+    def get(self, state, overwrite=False, thinsubrepos=False):
         """run whatever commands are needed to put the subrepo into
         this state
         """
         raise NotImplementedError
 
-    def merge(self, state):
+    def merge(self, state, thinsubrepos=False):
         """merge currently-saved state with the new state."""
         raise NotImplementedError
 
@@ -429,7 +430,7 @@
         self._repo.ui.note(_('removing subrepo %s\n') % subrelpath(self))
         hg.clean(self._repo, node.nullid, False)
 
-    def _get(self, state):
+    def _get(self, state, thinsubrepos):
         source, revision, kind = state
         if revision not in self._repo:
             self._repo._subsource = source
@@ -440,23 +441,31 @@
                                      % (subrelpath(self), srcurl))
                 parentrepo = self._repo._subparent
                 shutil.rmtree(self._repo.root)
+                subrev = None
+                if thinsubrepos:
+                    subrev = [revision]
                 other, self._repo = hg.clone(self._repo._subparent.ui, other,
-                                             self._repo.root, update=False)
+                                             self._repo.root, rev=subrev,
+                                             update=False,
+                                             thinsubrepos=thinsubrepos)
                 self._initrepo(parentrepo, source, create=True)
             else:
                 self._repo.ui.status(_('pulling subrepo %s from %s\n')
                                      % (subrelpath(self), srcurl))
-                self._repo.pull(other)
+                subrev = None
+                if thinsubrepos:
+                    subrev = [other.lookup(revision)]
+                self._repo.pull(other, heads=subrev)
             bookmarks.updatefromremote(self._repo.ui, self._repo, other)
 
-    def get(self, state, overwrite=False):
-        self._get(state)
+    def get(self, state, overwrite=False, thinsubrepos=False):
+        self._get(state, thinsubrepos)
         source, revision, kind = state
         self._repo.ui.debug("getting subrepo %s\n" % self._path)
-        hg.clean(self._repo, revision, False)
+        hg.clean(self._repo, revision, False, thinsubrepos=thinsubrepos)
 
-    def merge(self, state):
-        self._get(state)
+    def merge(self, state, thinsubrepos):
+        self._get(state, thinsubrepos)
         cur = self._repo['.']
         dst = self._repo[state[1]]
         anc = dst.ancestor(cur)
@@ -464,12 +473,13 @@
         def mergefunc():
             if anc == cur:
                 self._repo.ui.debug("updating subrepo %s\n" % subrelpath(self))
-                hg.update(self._repo, state[1])
+                hg.update(self._repo, state[1], thinsubrepos=thinsubrepos)
             elif anc == dst:
                 self._repo.ui.debug("skipping subrepo %s\n" % subrelpath(self))
             else:
                 self._repo.ui.debug("merging subrepo %s\n" % subrelpath(self))
-                hg.merge(self._repo, state[1], remind=False)
+                hg.merge(self._repo, state[1], remind=False,
+                         thinsubrepos=thinsubrepos)
 
         wctx = self._repo[None]
         if self.dirty():
@@ -647,7 +657,7 @@
         except OSError:
             pass
 
-    def get(self, state, overwrite=False):
+    def get(self, state, overwrite=False, thinsubrepos=False):
         if overwrite:
             self._svncommand(['revert', '--recursive'])
         args = ['checkout']
@@ -659,7 +669,7 @@
             raise util.Abort(status.splitlines()[-1])
         self._ui.status(status)
 
-    def merge(self, state):
+    def merge(self, state, thinsubrepos=False):
         old = self._state[1]
         new = state[1]
         if new != self._wcrev():
@@ -826,7 +836,7 @@
         out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
         return code == 1
 
-    def get(self, state, overwrite=False):
+    def get(self, state, overwrite=False, thinsubrepos=False):
         source, revision, kind = state
         self._fetch(source, revision)
         # if the repo was set to be bare, unbare it
@@ -923,7 +933,7 @@
         # circumstances
         return self._gitstate()
 
-    def merge(self, state):
+    def merge(self, state, thinsubrepos=False):
         source, revision, kind = state
         self._fetch(source, revision)
         base = self._gitcommand(['merge-base', revision, self._state[1]])
diff -r 85c82ebc96a3 -r 4d759579fb07 tests/test-debugcomplete.t
--- a/tests/test-debugcomplete.t	Mon May 16 13:06:48 2011 +0200
+++ b/tests/test-debugcomplete.t	Mon May 16 15:44:06 2011 -0400
@@ -187,21 +187,21 @@
   $ hg debugcommands
   add: include, exclude, subrepos, dry-run
   annotate: rev, follow, no-follow, text, user, file, date, number, changeset, line-number, include, exclude
-  clone: noupdate, updaterev, rev, branch, pull, uncompressed, ssh, remotecmd, insecure
+  clone: noupdate, updaterev, rev, branch, pull, thin, uncompressed, ssh, remotecmd, insecure
   commit: addremove, close-branch, include, exclude, message, logfile, date, user
   diff: rev, change, text, git, nodates, show-function, reverse, ignore-all-space, ignore-space-change, ignore-blank-lines, unified, stat, include, exclude, subrepos
   export: output, switch-parent, rev, text, git, nodates
   forget: include, exclude
   init: ssh, remotecmd, insecure
   log: follow, follow-first, date, copies, keyword, rev, removed, only-merges, user, only-branch, branch, prune, patch, git, limit, no-merges, stat, style, template, include, exclude
-  merge: force, tool, rev, preview
-  pull: update, force, rev, bookmark, branch, ssh, remotecmd, insecure
+  merge: force, tool, rev, preview, thin
+  pull: update, force, rev, bookmark, branch, thin, ssh, remotecmd, insecure
   push: force, rev, bookmark, branch, new-branch, ssh, remotecmd, insecure
   remove: after, force, include, exclude
   serve: accesslog, daemon, daemon-pipefds, errorlog, port, address, prefix, name, web-conf, webdir-conf, pid-file, stdio, templates, style, ipv6, certificate
   status: all, modified, added, removed, deleted, clean, unknown, ignored, no-status, copies, print0, rev, change, include, exclude, subrepos
   summary: remote
-  update: clean, check, date, rev
+  update: clean, check, date, rev, thin
   addremove: similarity, include, exclude, dry-run
   archive: no-decode, prefix, rev, type, subrepos, include, exclude
   backout: merge, parent, tool, rev, include, exclude, message, logfile, date, user
@@ -260,6 +260,6 @@
   tag: force, local, rev, remove, edit, message, date, user
   tags: 
   tip: patch, git, style, template
-  unbundle: update
+  unbundle: update, thin
   verify: 
   version: 
diff -r 85c82ebc96a3 -r 4d759579fb07 tests/test-subrepo.t
--- a/tests/test-subrepo.t	Mon May 16 13:06:48 2011 +0200
+++ b/tests/test-subrepo.t	Mon May 16 15:44:06 2011 -0400
@@ -177,7 +177,7 @@
    ancestor 1f14a2e2d3ec local f0d2028bf86d+ remote 1831e14459c4
    .hgsubstate: versions differ -> m
   updating: .hgsubstate 1/1 files (100.00%)
-  subrepo merge f0d2028bf86d+ 1831e14459c4 1f14a2e2d3ec
+  subrepo merge f0d2028bf86d+ 1831e14459c4 1f14a2e2d3ec None
     subrepo t: other changed, get t:6747d179aa9a688023c4b0cad32e4c92bb7f34ad:hg
   getting subrepo t
   resolving manifests
@@ -205,7 +205,7 @@
    ancestor 1831e14459c4 local e45c8b14af55+ remote f94576341bcf
    .hgsubstate: versions differ -> m
   updating: .hgsubstate 1/1 files (100.00%)
-  subrepo merge e45c8b14af55+ f94576341bcf 1831e14459c4
+  subrepo merge e45c8b14af55+ f94576341bcf 1831e14459c4 None
     subrepo t: both sides changed, merge with t:7af322bc1198a32402fe903e0b7ebcfc5c9bf8f4:hg
   merging subrepo t
     searching for copies back to rev 2


More information about the Mercurial-devel mailing list