[PATCH] Subrepository support

Alexander Solovyov piranha at piranha.org.ua
Tue Feb 10 13:30:34 CST 2009


# HG changeset patch
# User Alexander Solovyov <piranha at piranha.org.ua>
# Date 1234294229 -7200
# Node ID 2a6e49d46016c78941fb37360622ccc314fdaefd
# Parent  fff6e253e1f6eb7fa352617112bd48de7e39bd16
Subrepository support

diff --git a/mercurial/commands.py b/mercurial/commands.py
--- a/mercurial/commands.py
+++ b/mercurial/commands.py
@@ -2746,6 +2746,11 @@
                 if f in copy:
                     ui.write('  %s%s' % (repo.pathto(copy[f], cwd), end))
 
+def sublist(ui, repo, **opts):
+    '''List all subrepositories with their desired revisions'''
+    for path, info in repo.subrepos().items():
+        ui.write(" %-25s %s\n" % (path, info[1]))
+
 def tag(ui, repo, name1, *names, **opts):
     """add one or more tags for the current or given revision
 
@@ -3370,6 +3375,7 @@
           ('', 'rev', [], _('show difference from revision')),
          ] + walkopts,
          _('[OPTION]... [FILE]...')),
+    "sublist": (sublist, []),
     "tag":
         (tag,
          [('f', 'force', None, _('replace existing tag')),
diff --git a/mercurial/dirstate.py b/mercurial/dirstate.py
--- a/mercurial/dirstate.py
+++ b/mercurial/dirstate.py
@@ -10,7 +10,7 @@
 from node import nullid
 from i18n import _
 import struct, os, stat, util, errno, ignore
-import cStringIO, osutil, sys, parsers
+import cStringIO, osutil, sys, parsers, error
 
 _unknown = ('?', 0, 0, 0)
 _format = ">cllll"
@@ -37,13 +37,14 @@
 
 class dirstate(object):
 
-    def __init__(self, opener, ui, root):
+    def __init__(self, opener, ui, root, repo):
         self._opener = opener
         self._root = root
         self._rootdir = os.path.join(root, '')
         self._dirty = False
         self._dirtypl = False
         self._ui = ui
+        self._repo = repo
 
     def __getattr__(self, name):
         if name == '_map':
@@ -333,7 +334,8 @@
             self._droppath(f)
             del self._map[f]
         except KeyError:
-            self._ui.warn(_("not in dirstate: %s\n") % f)
+            if not f in self._repo.subrepos():
+                self._ui.warn(_("not in dirstate: %s\n") % f)
 
     def _normalize(self, path, knownpath=False):
         norm_path = os.path.normcase(path)
@@ -562,7 +564,8 @@
             state, mode, size, time = dmap[fn]
 
             if not st and state in "nma":
-                dadd(fn)
+                if fn not in self._repo.subrepos():
+                    dadd(fn)
             elif state == 'n':
                 if (size >= 0 and
                     (size != st.st_size
@@ -581,5 +584,13 @@
             elif state == 'r':
                 radd(fn)
 
+        for path in self._repo.subrepos():
+            try:
+                r = self._repo.subrepo(path)
+                if not r['.'].hex() == self._repo.subrepos()[path][1]:
+                    madd(path)
+            except error.RepoError:
+                radd(path)
+
         return (lookup, modified, added, removed, deleted, unknown, ignored,
                 clean)
diff --git a/mercurial/hg.py b/mercurial/hg.py
--- a/mercurial/hg.py
+++ b/mercurial/hg.py
@@ -80,7 +80,7 @@
     return path
 
 def clone(ui, source, dest=None, pull=False, rev=None, update=True,
-          stream=False):
+          stream=False, force=False):
     """Make a copy of an existing repository.
 
     Create a copy of an existing repository in a new directory.  The
@@ -112,6 +112,8 @@
     update: update working directory after clone completes, if
     destination is local repository (True means update to default rev,
     anything else is treated as a revision)
+
+    force: force cloning even if destination already exists
     """
 
     if isinstance(source, str):
@@ -130,7 +132,7 @@
     dest = localpath(dest)
     source = localpath(source)
 
-    if os.path.exists(dest):
+    if os.path.exists(dest) and not force:
         raise util.Abort(_("destination '%s' already exists") % dest)
 
     class DirCleanup(object):
diff --git a/mercurial/localrepo.py b/mercurial/localrepo.py
--- a/mercurial/localrepo.py
+++ b/mercurial/localrepo.py
@@ -9,7 +9,7 @@
 from i18n import _
 import repo, changegroup
 import changelog, dirstate, filelog, manifest, context, weakref
-import lock, transaction, stat, errno, ui, store
+import lock, transaction, stat, errno, ui, store, hg
 import os, time, util, extensions, hook, inspect, error
 import match as match_
 import merge as merge_
@@ -84,6 +84,7 @@
         self.filterpats = {}
         self._datafilters = {}
         self._transref = self._lockref = self._wlockref = None
+        self._subrepocache = {}
 
     def __getattr__(self, name):
         if name == 'changelog':
@@ -95,7 +96,7 @@
             self.manifest = manifest.manifest(self.sopener)
             return self.manifest
         if name == 'dirstate':
-            self.dirstate = dirstate.dirstate(self.opener, self.ui, self.root)
+            self.dirstate = dirstate.dirstate(self.opener, self.ui, self.root, self)
             return self.dirstate
         else:
             raise AttributeError(name)
@@ -565,7 +566,9 @@
         self._datafilters[name] = filter
 
     def wread(self, filename):
-        if self._link(filename):
+        if filename in self.subrepos():
+            data = self.subrepo(filename)['.'].hex() + '\n'
+        elif self._link(filename):
             data = os.readlink(self.wjoin(filename))
         else:
             data = self.wopener(filename, 'r').read()
@@ -2141,6 +2144,63 @@
             return self.stream_in(remote)
         return self.pull(remote, heads)
 
+    def subrepos(self, rev=None, usecache=True):
+        '''Parse .hgsubrepos and return a dictionary
+
+        Subrepos are stored in the format:
+
+          # comment
+          path/to/workdir http://path.to/repo/ [optional modes]
+          # comment
+          another/workdir http://path.to/another/ [optional modes]
+
+        Revisions of subrepos are stored inside a parent repository
+        store as a text file containing revision in a hex format. This
+        file is placed exactly at subrepo path:
+
+          .hg/store/data/path/to/workdir.i
+
+        The parsed dictionary is cached until a write() operation is done.
+        '''
+        ctx = self[rev]
+        rev = ctx.rev()
+        if rev is None:
+            # use parent revision for working ctx
+            rev = str(ctx.parents()[0].rev()) + '+'
+        try:
+            if self._subrepocache.get(rev) is not None and usecache:
+                return self._subrepocache[rev]
+            self._subrepocache[rev] = {}
+            for line in ctx['.hgsubrepos'].data().splitlines():
+                if '#' in line:
+                    line = line.split('#')[0]
+                line = line.strip().split()
+                if not line:
+                    continue
+                path = line[0]
+                source = line[1]
+                modes = line[2:]
+                try:
+                    node = ctx[path].data().strip()
+                except IOError:
+                    node = ctx.parents()[0][path].data().strip()
+                except error.LookupError:
+                    node = None
+                # path: source, rev, modes
+                self._subrepocache[rev][path] = (source, node, modes)
+        except:
+            pass
+        try:
+            return self._subrepocache[rev]
+        except KeyError:
+            return None
+
+    def subrepo(self, path, rev=None):
+        if not path in self.subrepos(rev):
+            raise error.RepoError('Subrepository %s unavailable' % path)
+        newui = ui.ui(parentui=self.ui, prepend='%s: ' % path)
+        return hg.repository(newui, self.wjoin(path))
+
 # used to avoid circular references so destructors work
 def aftertrans(files):
     renamefiles = [tuple(t) for t in files]
diff --git a/mercurial/merge.py b/mercurial/merge.py
--- a/mercurial/merge.py
+++ b/mercurial/merge.py
@@ -304,7 +304,15 @@
         f, m = a[:2]
         if f and f[0] == "/":
             continue
-        if m == "r": # remove
+        if f and f in repo.subrepos():
+            import subrepo
+            if m == "r":
+                subrepo.subremove(repo, f)
+            elif m in "fg":
+                subrepo.subupdate(repo, f)
+            else:
+                util.Abort("Something new with %s: %s\n" % (f, m))
+        elif m == "r": # remove
             repo.ui.note(_("removing %s\n") % f)
             audit_path(f)
             try:
diff --git a/mercurial/subrepo.py b/mercurial/subrepo.py
new file mode 100644
--- /dev/null
+++ b/mercurial/subrepo.py
@@ -0,0 +1,50 @@
+import os
+from i18n import _
+import hg, error, commands, util
+
+def subretrieve(repo, source, path, node):
+    '''Retrieve (clone/checkout) subrepository from a source.
+
+    Possibly use a cache (saved store) of subrepositories.'''
+    dest = repo.wjoin(path)
+    cache = repo.join('subcache/%s' % path)
+    if os.path.isdir(cache):
+        os.renames(cache, os.path.join(dest, '.hg'))
+    else:
+        repo.ui.status(_('%s: cloning from %s\n') % (path, source))
+        opts = dict(source=source, dest=dest, noupdate=True)
+        if node:
+            opts['rev'] = [node]
+        commands.clone(repo.ui, **opts)
+
+def subupdate(repo, path, rev=None):
+    '''Update subrepository to necessary revision.
+    '''
+    source, node, modes = repo.subrepos(rev)[path]
+
+    # clone if repo does not exist
+    try:
+        sub = repo.subrepo(path)
+    except error.RepoError:
+        subretrieve(repo, source, path, node)
+        sub = repo.subrepo(path, rev)
+
+    # pull if repo does not have necessary revision
+    try:
+        if node:
+            sub[node]
+    except error.RepoError:
+        src = hg.repository(sub.ui, source)
+        sub.pull(src, heads=[node])
+
+    hg.update(sub, node)
+
+def subremove(repo, path):
+    '''Remove subrepository, saving its store in a cache.
+    '''
+    try:
+        sub = repo.subrepo(path)
+    except error.RepoError:
+        return
+    hg.update(sub, 'null')
+    os.renames(sub.path, repo.join('subcache/%s' % path))
diff --git a/mercurial/ui.py b/mercurial/ui.py
--- a/mercurial/ui.py
+++ b/mercurial/ui.py
@@ -28,9 +28,10 @@
 
     def __init__(self, verbose=False, debug=False, quiet=False,
                  interactive=True, traceback=False, report_untrusted=True,
-                 parentui=None):
+                 parentui=None, prepend=''):
         self.overlay = None
         self.buffers = []
+        self.prepend = prepend
         if parentui is None:
             # this is the parent of all ui children
             self.parentui = None
@@ -65,6 +66,10 @@
                     self.overlay = util.configparser()
                 updateconfig(parentui.overlay, self.overlay)
             self.buffers = parentui.buffers
+            if parentui.prepend:
+                self.prepend = parentui.prepend
+            if prepend and prepend != self.prepend:
+                self.prepend += prepend
 
     def __getattr__(self, key):
         return getattr(self.parentui, key)
@@ -376,16 +381,16 @@
 
     def write(self, *args):
         if self.buffers:
-            self.buffers[-1].extend([str(a) for a in args])
+            self.buffers[-1].extend([self.prepend+str(a) for a in args])
         else:
             for a in args:
-                sys.stdout.write(str(a))
+                sys.stdout.write(self.prepend+str(a))
 
     def write_err(self, *args):
         try:
             if not sys.stdout.closed: sys.stdout.flush()
             for a in args:
-                sys.stderr.write(str(a))
+                sys.stderr.write(self.prepend+str(a))
             # stderr may be buffered under win32 when redirected to files,
             # including stdout.
             if not sys.stderr.closed: sys.stderr.flush()


More information about the Mercurial-devel mailing list