##// END OF EJS Templates
subrepo: add progress bar support to archive
Martin Geisler -
r13144:aae2d5cb default
parent child Browse files
Show More
@@ -1,279 +1,279 b''
1 1 # archival.py - revision archival for mercurial
2 2 #
3 3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from i18n import _
9 9 from node import hex
10 10 import cmdutil
11 11 import util, encoding
12 12 import cStringIO, os, stat, tarfile, time, zipfile
13 13 import zlib, gzip
14 14
15 15 def tidyprefix(dest, kind, prefix):
16 16 '''choose prefix to use for names in archive. make sure prefix is
17 17 safe for consumers.'''
18 18
19 19 if prefix:
20 20 prefix = util.normpath(prefix)
21 21 else:
22 22 if not isinstance(dest, str):
23 23 raise ValueError('dest must be string if no prefix')
24 24 prefix = os.path.basename(dest)
25 25 lower = prefix.lower()
26 26 for sfx in exts.get(kind, []):
27 27 if lower.endswith(sfx):
28 28 prefix = prefix[:-len(sfx)]
29 29 break
30 30 lpfx = os.path.normpath(util.localpath(prefix))
31 31 prefix = util.pconvert(lpfx)
32 32 if not prefix.endswith('/'):
33 33 prefix += '/'
34 34 if prefix.startswith('../') or os.path.isabs(lpfx) or '/../' in prefix:
35 35 raise util.Abort(_('archive prefix contains illegal components'))
36 36 return prefix
37 37
38 38 exts = {
39 39 'tar': ['.tar'],
40 40 'tbz2': ['.tbz2', '.tar.bz2'],
41 41 'tgz': ['.tgz', '.tar.gz'],
42 42 'zip': ['.zip'],
43 43 }
44 44
45 45 def guesskind(dest):
46 46 for kind, extensions in exts.iteritems():
47 47 if util.any(dest.endswith(ext) for ext in extensions):
48 48 return kind
49 49 return None
50 50
51 51
52 52 class tarit(object):
53 53 '''write archive to tar file or stream. can write uncompressed,
54 54 or compress with gzip or bzip2.'''
55 55
56 56 class GzipFileWithTime(gzip.GzipFile):
57 57
58 58 def __init__(self, *args, **kw):
59 59 timestamp = None
60 60 if 'timestamp' in kw:
61 61 timestamp = kw.pop('timestamp')
62 62 if timestamp is None:
63 63 self.timestamp = time.time()
64 64 else:
65 65 self.timestamp = timestamp
66 66 gzip.GzipFile.__init__(self, *args, **kw)
67 67
68 68 def _write_gzip_header(self):
69 69 self.fileobj.write('\037\213') # magic header
70 70 self.fileobj.write('\010') # compression method
71 71 # Python 2.6 deprecates self.filename
72 72 fname = getattr(self, 'name', None) or self.filename
73 73 if fname and fname.endswith('.gz'):
74 74 fname = fname[:-3]
75 75 flags = 0
76 76 if fname:
77 77 flags = gzip.FNAME
78 78 self.fileobj.write(chr(flags))
79 79 gzip.write32u(self.fileobj, long(self.timestamp))
80 80 self.fileobj.write('\002')
81 81 self.fileobj.write('\377')
82 82 if fname:
83 83 self.fileobj.write(fname + '\000')
84 84
85 85 def __init__(self, dest, mtime, kind=''):
86 86 self.mtime = mtime
87 87
88 88 def taropen(name, mode, fileobj=None):
89 89 if kind == 'gz':
90 90 mode = mode[0]
91 91 if not fileobj:
92 92 fileobj = open(name, mode + 'b')
93 93 gzfileobj = self.GzipFileWithTime(name, mode + 'b',
94 94 zlib.Z_BEST_COMPRESSION,
95 95 fileobj, timestamp=mtime)
96 96 return tarfile.TarFile.taropen(name, mode, gzfileobj)
97 97 else:
98 98 return tarfile.open(name, mode + kind, fileobj)
99 99
100 100 if isinstance(dest, str):
101 101 self.z = taropen(dest, mode='w:')
102 102 else:
103 103 # Python 2.5-2.5.1 have a regression that requires a name arg
104 104 self.z = taropen(name='', mode='w|', fileobj=dest)
105 105
106 106 def addfile(self, name, mode, islink, data):
107 107 i = tarfile.TarInfo(name)
108 108 i.mtime = self.mtime
109 109 i.size = len(data)
110 110 if islink:
111 111 i.type = tarfile.SYMTYPE
112 112 i.mode = 0777
113 113 i.linkname = data
114 114 data = None
115 115 i.size = 0
116 116 else:
117 117 i.mode = mode
118 118 data = cStringIO.StringIO(data)
119 119 self.z.addfile(i, data)
120 120
121 121 def done(self):
122 122 self.z.close()
123 123
124 124 class tellable(object):
125 125 '''provide tell method for zipfile.ZipFile when writing to http
126 126 response file object.'''
127 127
128 128 def __init__(self, fp):
129 129 self.fp = fp
130 130 self.offset = 0
131 131
132 132 def __getattr__(self, key):
133 133 return getattr(self.fp, key)
134 134
135 135 def write(self, s):
136 136 self.fp.write(s)
137 137 self.offset += len(s)
138 138
139 139 def tell(self):
140 140 return self.offset
141 141
142 142 class zipit(object):
143 143 '''write archive to zip file or stream. can write uncompressed,
144 144 or compressed with deflate.'''
145 145
146 146 def __init__(self, dest, mtime, compress=True):
147 147 if not isinstance(dest, str):
148 148 try:
149 149 dest.tell()
150 150 except (AttributeError, IOError):
151 151 dest = tellable(dest)
152 152 self.z = zipfile.ZipFile(dest, 'w',
153 153 compress and zipfile.ZIP_DEFLATED or
154 154 zipfile.ZIP_STORED)
155 155
156 156 # Python's zipfile module emits deprecation warnings if we try
157 157 # to store files with a date before 1980.
158 158 epoch = 315532800 # calendar.timegm((1980, 1, 1, 0, 0, 0, 1, 1, 0))
159 159 if mtime < epoch:
160 160 mtime = epoch
161 161
162 162 self.date_time = time.gmtime(mtime)[:6]
163 163
164 164 def addfile(self, name, mode, islink, data):
165 165 i = zipfile.ZipInfo(name, self.date_time)
166 166 i.compress_type = self.z.compression
167 167 # unzip will not honor unix file modes unless file creator is
168 168 # set to unix (id 3).
169 169 i.create_system = 3
170 170 ftype = stat.S_IFREG
171 171 if islink:
172 172 mode = 0777
173 173 ftype = stat.S_IFLNK
174 174 i.external_attr = (mode | ftype) << 16L
175 175 self.z.writestr(i, data)
176 176
177 177 def done(self):
178 178 self.z.close()
179 179
180 180 class fileit(object):
181 181 '''write archive as files in directory.'''
182 182
183 183 def __init__(self, name, mtime):
184 184 self.basedir = name
185 185 self.opener = util.opener(self.basedir)
186 186
187 187 def addfile(self, name, mode, islink, data):
188 188 if islink:
189 189 self.opener.symlink(data, name)
190 190 return
191 191 f = self.opener(name, "w", atomictemp=True)
192 192 f.write(data)
193 193 f.rename()
194 194 destfile = os.path.join(self.basedir, name)
195 195 os.chmod(destfile, mode)
196 196
197 197 def done(self):
198 198 pass
199 199
200 200 archivers = {
201 201 'files': fileit,
202 202 'tar': tarit,
203 203 'tbz2': lambda name, mtime: tarit(name, mtime, 'bz2'),
204 204 'tgz': lambda name, mtime: tarit(name, mtime, 'gz'),
205 205 'uzip': lambda name, mtime: zipit(name, mtime, False),
206 206 'zip': zipit,
207 207 }
208 208
209 209 def archive(repo, dest, node, kind, decode=True, matchfn=None,
210 210 prefix=None, mtime=None, subrepos=False):
211 211 '''create archive of repo as it was at node.
212 212
213 213 dest can be name of directory, name of archive file, or file
214 214 object to write archive to.
215 215
216 216 kind is type of archive to create.
217 217
218 218 decode tells whether to put files through decode filters from
219 219 hgrc.
220 220
221 221 matchfn is function to filter names of files to write to archive.
222 222
223 223 prefix is name of path to put before every archive member.'''
224 224
225 225 if kind == 'files':
226 226 if prefix:
227 227 raise util.Abort(_('cannot give prefix when archiving to files'))
228 228 else:
229 229 prefix = tidyprefix(dest, kind, prefix)
230 230
231 231 def write(name, mode, islink, getdata):
232 232 if matchfn and not matchfn(name):
233 233 return
234 234 data = getdata()
235 235 if decode:
236 236 data = repo.wwritedata(name, data)
237 237 archiver.addfile(prefix + name, mode, islink, data)
238 238
239 239 if kind not in archivers:
240 240 raise util.Abort(_("unknown archive type '%s'") % kind)
241 241
242 242 ctx = repo[node]
243 243 archiver = archivers[kind](dest, mtime or ctx.date()[0])
244 244
245 245 if repo.ui.configbool("ui", "archivemeta", True):
246 246 def metadata():
247 247 base = 'repo: %s\nnode: %s\nbranch: %s\n' % (
248 248 repo[0].hex(), hex(node), encoding.fromlocal(ctx.branch()))
249 249
250 250 tags = ''.join('tag: %s\n' % t for t in ctx.tags()
251 251 if repo.tagtype(t) == 'global')
252 252 if not tags:
253 253 repo.ui.pushbuffer()
254 254 opts = {'template': '{latesttag}\n{latesttagdistance}',
255 255 'style': '', 'patch': None, 'git': None}
256 256 cmdutil.show_changeset(repo.ui, repo, opts).show(ctx)
257 257 ltags, dist = repo.ui.popbuffer().split('\n')
258 258 tags = ''.join('latesttag: %s\n' % t for t in ltags.split(':'))
259 259 tags += 'latesttagdistance: %s\n' % dist
260 260
261 261 return base + tags
262 262
263 263 write('.hg_archival.txt', 0644, False, metadata)
264 264
265 265 total = len(ctx.manifest())
266 266 repo.ui.progress(_('archiving'), 0, unit=_('files'), total=total)
267 267 for i, f in enumerate(ctx):
268 268 ff = ctx.flags(f)
269 269 write(f, 'x' in ff and 0755 or 0644, 'l' in ff, ctx[f].data)
270 270 repo.ui.progress(_('archiving'), i + 1, item=f,
271 271 unit=_('files'), total=total)
272 272 repo.ui.progress(_('archiving'), None)
273 273
274 274 if subrepos:
275 275 for subpath in ctx.substate:
276 276 sub = ctx.sub(subpath)
277 sub.archive(archiver, prefix)
277 sub.archive(repo.ui, archiver, prefix)
278 278
279 279 archiver.done()
@@ -1,881 +1,895 b''
1 1 # subrepo.py - sub-repository handling for Mercurial
2 2 #
3 3 # Copyright 2009-2010 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 import errno, os, re, xml.dom.minidom, shutil, urlparse, posixpath
9 9 import stat, subprocess, tarfile
10 10 from i18n import _
11 11 import config, util, node, error, cmdutil
12 12 hg = None
13 13
14 14 nullstate = ('', '', 'empty')
15 15
16 16 def state(ctx, ui):
17 17 """return a state dict, mapping subrepo paths configured in .hgsub
18 18 to tuple: (source from .hgsub, revision from .hgsubstate, kind
19 19 (key in types dict))
20 20 """
21 21 p = config.config()
22 22 def read(f, sections=None, remap=None):
23 23 if f in ctx:
24 24 try:
25 25 data = ctx[f].data()
26 26 except IOError, err:
27 27 if err.errno != errno.ENOENT:
28 28 raise
29 29 # handle missing subrepo spec files as removed
30 30 ui.warn(_("warning: subrepo spec file %s not found\n") % f)
31 31 return
32 32 p.parse(f, data, sections, remap, read)
33 33 else:
34 34 raise util.Abort(_("subrepo spec file %s not found") % f)
35 35
36 36 if '.hgsub' in ctx:
37 37 read('.hgsub')
38 38
39 39 for path, src in ui.configitems('subpaths'):
40 40 p.set('subpaths', path, src, ui.configsource('subpaths', path))
41 41
42 42 rev = {}
43 43 if '.hgsubstate' in ctx:
44 44 try:
45 45 for l in ctx['.hgsubstate'].data().splitlines():
46 46 revision, path = l.split(" ", 1)
47 47 rev[path] = revision
48 48 except IOError, err:
49 49 if err.errno != errno.ENOENT:
50 50 raise
51 51
52 52 state = {}
53 53 for path, src in p[''].items():
54 54 kind = 'hg'
55 55 if src.startswith('['):
56 56 if ']' not in src:
57 57 raise util.Abort(_('missing ] in subrepo source'))
58 58 kind, src = src.split(']', 1)
59 59 kind = kind[1:]
60 60
61 61 for pattern, repl in p.items('subpaths'):
62 62 # Turn r'C:\foo\bar' into r'C:\\foo\\bar' since re.sub
63 63 # does a string decode.
64 64 repl = repl.encode('string-escape')
65 65 # However, we still want to allow back references to go
66 66 # through unharmed, so we turn r'\\1' into r'\1'. Again,
67 67 # extra escapes are needed because re.sub string decodes.
68 68 repl = re.sub(r'\\\\([0-9]+)', r'\\\1', repl)
69 69 try:
70 70 src = re.sub(pattern, repl, src, 1)
71 71 except re.error, e:
72 72 raise util.Abort(_("bad subrepository pattern in %s: %s")
73 73 % (p.source('subpaths', pattern), e))
74 74
75 75 state[path] = (src.strip(), rev.get(path, ''), kind)
76 76
77 77 return state
78 78
79 79 def writestate(repo, state):
80 80 """rewrite .hgsubstate in (outer) repo with these subrepo states"""
81 81 repo.wwrite('.hgsubstate',
82 82 ''.join(['%s %s\n' % (state[s][1], s)
83 83 for s in sorted(state)]), '')
84 84
85 85 def submerge(repo, wctx, mctx, actx):
86 86 """delegated from merge.applyupdates: merging of .hgsubstate file
87 87 in working context, merging context and ancestor context"""
88 88 if mctx == actx: # backwards?
89 89 actx = wctx.p1()
90 90 s1 = wctx.substate
91 91 s2 = mctx.substate
92 92 sa = actx.substate
93 93 sm = {}
94 94
95 95 repo.ui.debug("subrepo merge %s %s %s\n" % (wctx, mctx, actx))
96 96
97 97 def debug(s, msg, r=""):
98 98 if r:
99 99 r = "%s:%s:%s" % r
100 100 repo.ui.debug(" subrepo %s: %s %s\n" % (s, msg, r))
101 101
102 102 for s, l in s1.items():
103 103 a = sa.get(s, nullstate)
104 104 ld = l # local state with possible dirty flag for compares
105 105 if wctx.sub(s).dirty():
106 106 ld = (l[0], l[1] + "+")
107 107 if wctx == actx: # overwrite
108 108 a = ld
109 109
110 110 if s in s2:
111 111 r = s2[s]
112 112 if ld == r or r == a: # no change or local is newer
113 113 sm[s] = l
114 114 continue
115 115 elif ld == a: # other side changed
116 116 debug(s, "other changed, get", r)
117 117 wctx.sub(s).get(r)
118 118 sm[s] = r
119 119 elif ld[0] != r[0]: # sources differ
120 120 if repo.ui.promptchoice(
121 121 _(' subrepository sources for %s differ\n'
122 122 'use (l)ocal source (%s) or (r)emote source (%s)?')
123 123 % (s, l[0], r[0]),
124 124 (_('&Local'), _('&Remote')), 0):
125 125 debug(s, "prompt changed, get", r)
126 126 wctx.sub(s).get(r)
127 127 sm[s] = r
128 128 elif ld[1] == a[1]: # local side is unchanged
129 129 debug(s, "other side changed, get", r)
130 130 wctx.sub(s).get(r)
131 131 sm[s] = r
132 132 else:
133 133 debug(s, "both sides changed, merge with", r)
134 134 wctx.sub(s).merge(r)
135 135 sm[s] = l
136 136 elif ld == a: # remote removed, local unchanged
137 137 debug(s, "remote removed, remove")
138 138 wctx.sub(s).remove()
139 139 else:
140 140 if repo.ui.promptchoice(
141 141 _(' local changed subrepository %s which remote removed\n'
142 142 'use (c)hanged version or (d)elete?') % s,
143 143 (_('&Changed'), _('&Delete')), 0):
144 144 debug(s, "prompt remove")
145 145 wctx.sub(s).remove()
146 146
147 147 for s, r in s2.items():
148 148 if s in s1:
149 149 continue
150 150 elif s not in sa:
151 151 debug(s, "remote added, get", r)
152 152 mctx.sub(s).get(r)
153 153 sm[s] = r
154 154 elif r != sa[s]:
155 155 if repo.ui.promptchoice(
156 156 _(' remote changed subrepository %s which local removed\n'
157 157 'use (c)hanged version or (d)elete?') % s,
158 158 (_('&Changed'), _('&Delete')), 0) == 0:
159 159 debug(s, "prompt recreate", r)
160 160 wctx.sub(s).get(r)
161 161 sm[s] = r
162 162
163 163 # record merged .hgsubstate
164 164 writestate(repo, sm)
165 165
166 166 def reporelpath(repo):
167 167 """return path to this (sub)repo as seen from outermost repo"""
168 168 parent = repo
169 169 while hasattr(parent, '_subparent'):
170 170 parent = parent._subparent
171 171 return repo.root[len(parent.root)+1:]
172 172
173 173 def subrelpath(sub):
174 174 """return path to this subrepo as seen from outermost repo"""
175 175 if not hasattr(sub, '_repo'):
176 176 return sub._path
177 177 return reporelpath(sub._repo)
178 178
179 179 def _abssource(repo, push=False, abort=True):
180 180 """return pull/push path of repo - either based on parent repo .hgsub info
181 181 or on the top repo config. Abort or return None if no source found."""
182 182 if hasattr(repo, '_subparent'):
183 183 source = repo._subsource
184 184 if source.startswith('/') or '://' in source:
185 185 return source
186 186 parent = _abssource(repo._subparent, push, abort=False)
187 187 if parent:
188 188 if '://' in parent:
189 189 if parent[-1] == '/':
190 190 parent = parent[:-1]
191 191 r = urlparse.urlparse(parent + '/' + source)
192 192 r = urlparse.urlunparse((r[0], r[1],
193 193 posixpath.normpath(r[2]),
194 194 r[3], r[4], r[5]))
195 195 return r
196 196 else: # plain file system path
197 197 return posixpath.normpath(os.path.join(parent, repo._subsource))
198 198 else: # recursion reached top repo
199 199 if hasattr(repo, '_subtoppath'):
200 200 return repo._subtoppath
201 201 if push and repo.ui.config('paths', 'default-push'):
202 202 return repo.ui.config('paths', 'default-push')
203 203 if repo.ui.config('paths', 'default'):
204 204 return repo.ui.config('paths', 'default')
205 205 if abort:
206 206 raise util.Abort(_("default path for subrepository %s not found") %
207 207 reporelpath(repo))
208 208
209 209 def itersubrepos(ctx1, ctx2):
210 210 """find subrepos in ctx1 or ctx2"""
211 211 # Create a (subpath, ctx) mapping where we prefer subpaths from
212 212 # ctx1. The subpaths from ctx2 are important when the .hgsub file
213 213 # has been modified (in ctx2) but not yet committed (in ctx1).
214 214 subpaths = dict.fromkeys(ctx2.substate, ctx2)
215 215 subpaths.update(dict.fromkeys(ctx1.substate, ctx1))
216 216 for subpath, ctx in sorted(subpaths.iteritems()):
217 217 yield subpath, ctx.sub(subpath)
218 218
219 219 def subrepo(ctx, path):
220 220 """return instance of the right subrepo class for subrepo in path"""
221 221 # subrepo inherently violates our import layering rules
222 222 # because it wants to make repo objects from deep inside the stack
223 223 # so we manually delay the circular imports to not break
224 224 # scripts that don't use our demand-loading
225 225 global hg
226 226 import hg as h
227 227 hg = h
228 228
229 229 util.path_auditor(ctx._repo.root)(path)
230 230 state = ctx.substate.get(path, nullstate)
231 231 if state[2] not in types:
232 232 raise util.Abort(_('unknown subrepo type %s') % state[2])
233 233 return types[state[2]](ctx, path, state[:2])
234 234
235 235 # subrepo classes need to implement the following abstract class:
236 236
237 237 class abstractsubrepo(object):
238 238
239 239 def dirty(self):
240 240 """returns true if the dirstate of the subrepo does not match
241 241 current stored state
242 242 """
243 243 raise NotImplementedError
244 244
245 245 def checknested(self, path):
246 246 """check if path is a subrepository within this repository"""
247 247 return False
248 248
249 249 def commit(self, text, user, date):
250 250 """commit the current changes to the subrepo with the given
251 251 log message. Use given user and date if possible. Return the
252 252 new state of the subrepo.
253 253 """
254 254 raise NotImplementedError
255 255
256 256 def remove(self):
257 257 """remove the subrepo
258 258
259 259 (should verify the dirstate is not dirty first)
260 260 """
261 261 raise NotImplementedError
262 262
263 263 def get(self, state):
264 264 """run whatever commands are needed to put the subrepo into
265 265 this state
266 266 """
267 267 raise NotImplementedError
268 268
269 269 def merge(self, state):
270 270 """merge currently-saved state with the new state."""
271 271 raise NotImplementedError
272 272
273 273 def push(self, force):
274 274 """perform whatever action is analogous to 'hg push'
275 275
276 276 This may be a no-op on some systems.
277 277 """
278 278 raise NotImplementedError
279 279
280 280 def add(self, ui, match, dryrun, prefix):
281 281 return []
282 282
283 283 def status(self, rev2, **opts):
284 284 return [], [], [], [], [], [], []
285 285
286 286 def diff(self, diffopts, node2, match, prefix, **opts):
287 287 pass
288 288
289 289 def outgoing(self, ui, dest, opts):
290 290 return 1
291 291
292 292 def incoming(self, ui, source, opts):
293 293 return 1
294 294
295 295 def files(self):
296 296 """return filename iterator"""
297 297 raise NotImplementedError
298 298
299 299 def filedata(self, name):
300 300 """return file data"""
301 301 raise NotImplementedError
302 302
303 303 def fileflags(self, name):
304 304 """return file flags"""
305 305 return ''
306 306
307 def archive(self, archiver, prefix):
308 for name in self.files():
307 def archive(self, ui, archiver, prefix):
308 files = self.files()
309 total = len(files)
310 relpath = subrelpath(self)
311 ui.progress(_('archiving (%s)') % relpath, 0,
312 unit=_('files'), total=total)
313 for i, name in enumerate(files):
309 314 flags = self.fileflags(name)
310 315 mode = 'x' in flags and 0755 or 0644
311 316 symlink = 'l' in flags
312 317 archiver.addfile(os.path.join(prefix, self._path, name),
313 318 mode, symlink, self.filedata(name))
319 ui.progress(_('archiving (%s)') % relpath, i + 1,
320 unit=_('files'), total=total)
321 ui.progress(_('archiving (%s)') % relpath, None)
314 322
315 323
316 324 class hgsubrepo(abstractsubrepo):
317 325 def __init__(self, ctx, path, state):
318 326 self._path = path
319 327 self._state = state
320 328 r = ctx._repo
321 329 root = r.wjoin(path)
322 330 create = False
323 331 if not os.path.exists(os.path.join(root, '.hg')):
324 332 create = True
325 333 util.makedirs(root)
326 334 self._repo = hg.repository(r.ui, root, create=create)
327 335 self._repo._subparent = r
328 336 self._repo._subsource = state[0]
329 337
330 338 if create:
331 339 fp = self._repo.opener("hgrc", "w", text=True)
332 340 fp.write('[paths]\n')
333 341
334 342 def addpathconfig(key, value):
335 343 if value:
336 344 fp.write('%s = %s\n' % (key, value))
337 345 self._repo.ui.setconfig('paths', key, value)
338 346
339 347 defpath = _abssource(self._repo, abort=False)
340 348 defpushpath = _abssource(self._repo, True, abort=False)
341 349 addpathconfig('default', defpath)
342 350 if defpath != defpushpath:
343 351 addpathconfig('default-push', defpushpath)
344 352 fp.close()
345 353
346 354 def add(self, ui, match, dryrun, prefix):
347 355 return cmdutil.add(ui, self._repo, match, dryrun, True,
348 356 os.path.join(prefix, self._path))
349 357
350 358 def status(self, rev2, **opts):
351 359 try:
352 360 rev1 = self._state[1]
353 361 ctx1 = self._repo[rev1]
354 362 ctx2 = self._repo[rev2]
355 363 return self._repo.status(ctx1, ctx2, **opts)
356 364 except error.RepoLookupError, inst:
357 365 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
358 366 % (inst, subrelpath(self)))
359 367 return [], [], [], [], [], [], []
360 368
361 369 def diff(self, diffopts, node2, match, prefix, **opts):
362 370 try:
363 371 node1 = node.bin(self._state[1])
364 372 # We currently expect node2 to come from substate and be
365 373 # in hex format
366 374 if node2 is not None:
367 375 node2 = node.bin(node2)
368 376 cmdutil.diffordiffstat(self._repo.ui, self._repo, diffopts,
369 377 node1, node2, match,
370 378 prefix=os.path.join(prefix, self._path),
371 379 listsubrepos=True, **opts)
372 380 except error.RepoLookupError, inst:
373 381 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
374 382 % (inst, subrelpath(self)))
375 383
376 def archive(self, archiver, prefix):
377 abstractsubrepo.archive(self, archiver, prefix)
384 def archive(self, ui, archiver, prefix):
385 abstractsubrepo.archive(self, ui, archiver, prefix)
378 386
379 387 rev = self._state[1]
380 388 ctx = self._repo[rev]
381 389 for subpath in ctx.substate:
382 390 s = subrepo(ctx, subpath)
383 s.archive(archiver, os.path.join(prefix, self._path))
391 s.archive(ui, archiver, os.path.join(prefix, self._path))
384 392
385 393 def dirty(self):
386 394 r = self._state[1]
387 395 if r == '':
388 396 return True
389 397 w = self._repo[None]
390 398 if w.p1() != self._repo[r]: # version checked out change
391 399 return True
392 400 return w.dirty() # working directory changed
393 401
394 402 def checknested(self, path):
395 403 return self._repo._checknested(self._repo.wjoin(path))
396 404
397 405 def commit(self, text, user, date):
398 406 self._repo.ui.debug("committing subrepo %s\n" % subrelpath(self))
399 407 n = self._repo.commit(text, user, date)
400 408 if not n:
401 409 return self._repo['.'].hex() # different version checked out
402 410 return node.hex(n)
403 411
404 412 def remove(self):
405 413 # we can't fully delete the repository as it may contain
406 414 # local-only history
407 415 self._repo.ui.note(_('removing subrepo %s\n') % subrelpath(self))
408 416 hg.clean(self._repo, node.nullid, False)
409 417
410 418 def _get(self, state):
411 419 source, revision, kind = state
412 420 try:
413 421 self._repo.lookup(revision)
414 422 except error.RepoError:
415 423 self._repo._subsource = source
416 424 srcurl = _abssource(self._repo)
417 425 self._repo.ui.status(_('pulling subrepo %s from %s\n')
418 426 % (subrelpath(self), srcurl))
419 427 other = hg.repository(self._repo.ui, srcurl)
420 428 self._repo.pull(other)
421 429
422 430 def get(self, state):
423 431 self._get(state)
424 432 source, revision, kind = state
425 433 self._repo.ui.debug("getting subrepo %s\n" % self._path)
426 434 hg.clean(self._repo, revision, False)
427 435
428 436 def merge(self, state):
429 437 self._get(state)
430 438 cur = self._repo['.']
431 439 dst = self._repo[state[1]]
432 440 anc = dst.ancestor(cur)
433 441 if anc == cur:
434 442 self._repo.ui.debug("updating subrepo %s\n" % subrelpath(self))
435 443 hg.update(self._repo, state[1])
436 444 elif anc == dst:
437 445 self._repo.ui.debug("skipping subrepo %s\n" % subrelpath(self))
438 446 else:
439 447 self._repo.ui.debug("merging subrepo %s\n" % subrelpath(self))
440 448 hg.merge(self._repo, state[1], remind=False)
441 449
442 450 def push(self, force):
443 451 # push subrepos depth-first for coherent ordering
444 452 c = self._repo['']
445 453 subs = c.substate # only repos that are committed
446 454 for s in sorted(subs):
447 455 if not c.sub(s).push(force):
448 456 return False
449 457
450 458 dsturl = _abssource(self._repo, True)
451 459 self._repo.ui.status(_('pushing subrepo %s to %s\n') %
452 460 (subrelpath(self), dsturl))
453 461 other = hg.repository(self._repo.ui, dsturl)
454 462 return self._repo.push(other, force)
455 463
456 464 def outgoing(self, ui, dest, opts):
457 465 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
458 466
459 467 def incoming(self, ui, source, opts):
460 468 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
461 469
462 470 def files(self):
463 471 rev = self._state[1]
464 472 ctx = self._repo[rev]
465 473 return ctx.manifest()
466 474
467 475 def filedata(self, name):
468 476 rev = self._state[1]
469 477 return self._repo[rev][name].data()
470 478
471 479 def fileflags(self, name):
472 480 rev = self._state[1]
473 481 ctx = self._repo[rev]
474 482 return ctx.flags(name)
475 483
476 484
477 485 class svnsubrepo(abstractsubrepo):
478 486 def __init__(self, ctx, path, state):
479 487 self._path = path
480 488 self._state = state
481 489 self._ctx = ctx
482 490 self._ui = ctx._repo.ui
483 491
484 492 def _svncommand(self, commands, filename=''):
485 493 path = os.path.join(self._ctx._repo.origroot, self._path, filename)
486 494 cmd = ['svn'] + commands + [path]
487 495 env = dict(os.environ)
488 496 # Avoid localized output, preserve current locale for everything else.
489 497 env['LC_MESSAGES'] = 'C'
490 498 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
491 499 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
492 500 universal_newlines=True, env=env)
493 501 stdout, stderr = p.communicate()
494 502 stderr = stderr.strip()
495 503 if stderr:
496 504 raise util.Abort(stderr)
497 505 return stdout
498 506
499 507 def _wcrev(self):
500 508 output = self._svncommand(['info', '--xml'])
501 509 doc = xml.dom.minidom.parseString(output)
502 510 entries = doc.getElementsByTagName('entry')
503 511 if not entries:
504 512 return '0'
505 513 return str(entries[0].getAttribute('revision')) or '0'
506 514
507 515 def _wcchanged(self):
508 516 """Return (changes, extchanges) where changes is True
509 517 if the working directory was changed, and extchanges is
510 518 True if any of these changes concern an external entry.
511 519 """
512 520 output = self._svncommand(['status', '--xml'])
513 521 externals, changes = [], []
514 522 doc = xml.dom.minidom.parseString(output)
515 523 for e in doc.getElementsByTagName('entry'):
516 524 s = e.getElementsByTagName('wc-status')
517 525 if not s:
518 526 continue
519 527 item = s[0].getAttribute('item')
520 528 props = s[0].getAttribute('props')
521 529 path = e.getAttribute('path')
522 530 if item == 'external':
523 531 externals.append(path)
524 532 if (item not in ('', 'normal', 'unversioned', 'external')
525 533 or props not in ('', 'none')):
526 534 changes.append(path)
527 535 for path in changes:
528 536 for ext in externals:
529 537 if path == ext or path.startswith(ext + os.sep):
530 538 return True, True
531 539 return bool(changes), False
532 540
533 541 def dirty(self):
534 542 if self._wcrev() == self._state[1] and not self._wcchanged()[0]:
535 543 return False
536 544 return True
537 545
538 546 def commit(self, text, user, date):
539 547 # user and date are out of our hands since svn is centralized
540 548 changed, extchanged = self._wcchanged()
541 549 if not changed:
542 550 return self._wcrev()
543 551 if extchanged:
544 552 # Do not try to commit externals
545 553 raise util.Abort(_('cannot commit svn externals'))
546 554 commitinfo = self._svncommand(['commit', '-m', text])
547 555 self._ui.status(commitinfo)
548 556 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
549 557 if not newrev:
550 558 raise util.Abort(commitinfo.splitlines()[-1])
551 559 newrev = newrev.groups()[0]
552 560 self._ui.status(self._svncommand(['update', '-r', newrev]))
553 561 return newrev
554 562
555 563 def remove(self):
556 564 if self.dirty():
557 565 self._ui.warn(_('not removing repo %s because '
558 566 'it has changes.\n' % self._path))
559 567 return
560 568 self._ui.note(_('removing subrepo %s\n') % self._path)
561 569
562 570 def onerror(function, path, excinfo):
563 571 if function is not os.remove:
564 572 raise
565 573 # read-only files cannot be unlinked under Windows
566 574 s = os.stat(path)
567 575 if (s.st_mode & stat.S_IWRITE) != 0:
568 576 raise
569 577 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
570 578 os.remove(path)
571 579
572 580 path = self._ctx._repo.wjoin(self._path)
573 581 shutil.rmtree(path, onerror=onerror)
574 582 try:
575 583 os.removedirs(os.path.dirname(path))
576 584 except OSError:
577 585 pass
578 586
579 587 def get(self, state):
580 588 status = self._svncommand(['checkout', state[0], '--revision', state[1]])
581 589 if not re.search('Checked out revision [0-9]+.', status):
582 590 raise util.Abort(status.splitlines()[-1])
583 591 self._ui.status(status)
584 592
585 593 def merge(self, state):
586 594 old = int(self._state[1])
587 595 new = int(state[1])
588 596 if new > old:
589 597 self.get(state)
590 598
591 599 def push(self, force):
592 600 # push is a no-op for SVN
593 601 return True
594 602
595 603 def files(self):
596 604 output = self._svncommand(['list'])
597 605 # This works because svn forbids \n in filenames.
598 606 return output.splitlines()
599 607
600 608 def filedata(self, name):
601 609 return self._svncommand(['cat'], name)
602 610
603 611
604 612 class gitsubrepo(abstractsubrepo):
605 613 def __init__(self, ctx, path, state):
606 614 # TODO add git version check.
607 615 self._state = state
608 616 self._ctx = ctx
609 617 self._relpath = path
610 618 self._path = ctx._repo.wjoin(path)
611 619 self._ui = ctx._repo.ui
612 620
613 621 def _gitcommand(self, commands, env=None, stream=False):
614 622 return self._gitdir(commands, env=env, stream=stream)[0]
615 623
616 624 def _gitdir(self, commands, env=None, stream=False):
617 625 return self._gitnodir(commands, env=env, stream=stream, cwd=self._path)
618 626
619 627 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
620 628 """Calls the git command
621 629
622 630 The methods tries to call the git command. versions previor to 1.6.0
623 631 are not supported and very probably fail.
624 632 """
625 633 self._ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
626 634 # unless ui.quiet is set, print git's stderr,
627 635 # which is mostly progress and useful info
628 636 errpipe = None
629 637 if self._ui.quiet:
630 638 errpipe = open(os.devnull, 'w')
631 639 p = subprocess.Popen(['git'] + commands, bufsize=-1, cwd=cwd, env=env,
632 640 close_fds=util.closefds,
633 641 stdout=subprocess.PIPE, stderr=errpipe)
634 642 if stream:
635 643 return p.stdout, None
636 644
637 645 retdata = p.stdout.read().strip()
638 646 # wait for the child to exit to avoid race condition.
639 647 p.wait()
640 648
641 649 if p.returncode != 0 and p.returncode != 1:
642 650 # there are certain error codes that are ok
643 651 command = commands[0]
644 652 if command == 'cat-file':
645 653 return retdata, p.returncode
646 654 # for all others, abort
647 655 raise util.Abort('git %s error %d in %s' %
648 656 (command, p.returncode, self._relpath))
649 657
650 658 return retdata, p.returncode
651 659
652 660 def _gitstate(self):
653 661 return self._gitcommand(['rev-parse', 'HEAD'])
654 662
655 663 def _githavelocally(self, revision):
656 664 out, code = self._gitdir(['cat-file', '-e', revision])
657 665 return code == 0
658 666
659 667 def _gitisancestor(self, r1, r2):
660 668 base = self._gitcommand(['merge-base', r1, r2])
661 669 return base == r1
662 670
663 671 def _gitbranchmap(self):
664 672 '''returns 3 things:
665 673 the current branch,
666 674 a map from git branch to revision
667 675 a map from revision to branches'''
668 676 branch2rev = {}
669 677 rev2branch = {}
670 678 current = None
671 679 out = self._gitcommand(['branch', '-a', '--no-color',
672 680 '--verbose', '--no-abbrev'])
673 681 for line in out.split('\n'):
674 682 if line[2:].startswith('(no branch)'):
675 683 continue
676 684 branch, revision = line[2:].split()[:2]
677 685 if revision == '->' or branch.endswith('/HEAD'):
678 686 continue # ignore remote/HEAD redirects
679 687 if '/' in branch and not branch.startswith('remotes/'):
680 688 # old git compatability
681 689 branch = 'remotes/' + branch
682 690 if line[0] == '*':
683 691 current = branch
684 692 branch2rev[branch] = revision
685 693 rev2branch.setdefault(revision, []).append(branch)
686 694 return current, branch2rev, rev2branch
687 695
688 696 def _gittracking(self, branches):
689 697 'return map of remote branch to local tracking branch'
690 698 # assumes no more than one local tracking branch for each remote
691 699 tracking = {}
692 700 for b in branches:
693 701 if b.startswith('remotes/'):
694 702 continue
695 703 remote = self._gitcommand(['config', 'branch.%s.remote' % b])
696 704 if remote:
697 705 ref = self._gitcommand(['config', 'branch.%s.merge' % b])
698 706 tracking['remotes/%s/%s' % (remote, ref.split('/')[-1])] = b
699 707 return tracking
700 708
701 709 def _fetch(self, source, revision):
702 710 if not os.path.exists('%s/.git' % self._path):
703 711 self._ui.status(_('cloning subrepo %s\n') % self._relpath)
704 712 self._gitnodir(['clone', source, self._path])
705 713 if self._githavelocally(revision):
706 714 return
707 715 self._ui.status(_('pulling subrepo %s\n') % self._relpath)
708 716 # first try from origin
709 717 self._gitcommand(['fetch'])
710 718 if self._githavelocally(revision):
711 719 return
712 720 # then try from known subrepo source
713 721 self._gitcommand(['fetch', source])
714 722 if not self._githavelocally(revision):
715 723 raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
716 724 (revision, self._path))
717 725
718 726 def dirty(self):
719 727 if self._state[1] != self._gitstate(): # version checked out changed?
720 728 return True
721 729 # check for staged changes or modified files; ignore untracked files
722 730 status = self._gitcommand(['status'])
723 731 return ('\n# Changed but not updated:' in status or
724 732 '\n# Changes to be committed:' in status)
725 733
726 734 def get(self, state):
727 735 source, revision, kind = state
728 736 self._fetch(source, revision)
729 737 # if the repo was set to be bare, unbare it
730 738 if self._gitcommand(['config', '--bool', 'core.bare']) == 'true':
731 739 self._gitcommand(['config', 'core.bare', 'false'])
732 740 if self._gitstate() == revision:
733 741 self._gitcommand(['reset', '--hard', 'HEAD'])
734 742 return
735 743 elif self._gitstate() == revision:
736 744 return
737 745 current, branch2rev, rev2branch = self._gitbranchmap()
738 746
739 747 def rawcheckout():
740 748 # no branch to checkout, check it out with no branch
741 749 self._ui.warn(_('checking out detached HEAD in subrepo %s\n') %
742 750 self._relpath)
743 751 self._ui.warn(_('check out a git branch if you intend '
744 752 'to make changes\n'))
745 753 self._gitcommand(['checkout', '-q', revision])
746 754
747 755 if revision not in rev2branch:
748 756 rawcheckout()
749 757 return
750 758 branches = rev2branch[revision]
751 759 firstlocalbranch = None
752 760 for b in branches:
753 761 if b == 'master':
754 762 # master trumps all other branches
755 763 self._gitcommand(['checkout', 'master'])
756 764 return
757 765 if not firstlocalbranch and not b.startswith('remotes/'):
758 766 firstlocalbranch = b
759 767 if firstlocalbranch:
760 768 self._gitcommand(['checkout', firstlocalbranch])
761 769 return
762 770
763 771 tracking = self._gittracking(branch2rev.keys())
764 772 # choose a remote branch already tracked if possible
765 773 remote = branches[0]
766 774 if remote not in tracking:
767 775 for b in branches:
768 776 if b in tracking:
769 777 remote = b
770 778 break
771 779
772 780 if remote not in tracking:
773 781 # create a new local tracking branch
774 782 local = remote.split('/')[-1]
775 783 self._gitcommand(['checkout', '-b', local, remote])
776 784 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
777 785 # When updating to a tracked remote branch,
778 786 # if the local tracking branch is downstream of it,
779 787 # a normal `git pull` would have performed a "fast-forward merge"
780 788 # which is equivalent to updating the local branch to the remote.
781 789 # Since we are only looking at branching at update, we need to
782 790 # detect this situation and perform this action lazily.
783 791 if tracking[remote] != current:
784 792 self._gitcommand(['checkout', tracking[remote]])
785 793 self._gitcommand(['merge', '--ff', remote])
786 794 else:
787 795 # a real merge would be required, just checkout the revision
788 796 rawcheckout()
789 797
790 798 def commit(self, text, user, date):
791 799 cmd = ['commit', '-a', '-m', text]
792 800 env = os.environ.copy()
793 801 if user:
794 802 cmd += ['--author', user]
795 803 if date:
796 804 # git's date parser silently ignores when seconds < 1e9
797 805 # convert to ISO8601
798 806 env['GIT_AUTHOR_DATE'] = util.datestr(date,
799 807 '%Y-%m-%dT%H:%M:%S %1%2')
800 808 self._gitcommand(cmd, env=env)
801 809 # make sure commit works otherwise HEAD might not exist under certain
802 810 # circumstances
803 811 return self._gitstate()
804 812
805 813 def merge(self, state):
806 814 source, revision, kind = state
807 815 self._fetch(source, revision)
808 816 base = self._gitcommand(['merge-base', revision, self._state[1]])
809 817 if base == revision:
810 818 self.get(state) # fast forward merge
811 819 elif base != self._state[1]:
812 820 self._gitcommand(['merge', '--no-commit', revision])
813 821
814 822 def push(self, force):
815 823 # if a branch in origin contains the revision, nothing to do
816 824 current, branch2rev, rev2branch = self._gitbranchmap()
817 825 if self._state[1] in rev2branch:
818 826 for b in rev2branch[self._state[1]]:
819 827 if b.startswith('remotes/origin/'):
820 828 return True
821 829 for b, revision in branch2rev.iteritems():
822 830 if b.startswith('remotes/origin/'):
823 831 if self._gitisancestor(self._state[1], revision):
824 832 return True
825 833 # otherwise, try to push the currently checked out branch
826 834 cmd = ['push']
827 835 if force:
828 836 cmd.append('--force')
829 837 if current:
830 838 # determine if the current branch is even useful
831 839 if not self._gitisancestor(self._state[1], current):
832 840 self._ui.warn(_('unrelated git branch checked out '
833 841 'in subrepo %s\n') % self._relpath)
834 842 return False
835 843 self._ui.status(_('pushing branch %s of subrepo %s\n') %
836 844 (current, self._relpath))
837 845 self._gitcommand(cmd + ['origin', current])
838 846 return True
839 847 else:
840 848 self._ui.warn(_('no branch checked out in subrepo %s\n'
841 849 'cannot push revision %s') %
842 850 (self._relpath, self._state[1]))
843 851 return False
844 852
845 853 def remove(self):
846 854 if self.dirty():
847 855 self._ui.warn(_('not removing repo %s because '
848 856 'it has changes.\n') % self._path)
849 857 return
850 858 # we can't fully delete the repository as it may contain
851 859 # local-only history
852 860 self._ui.note(_('removing subrepo %s\n') % self._path)
853 861 self._gitcommand(['config', 'core.bare', 'true'])
854 862 for f in os.listdir(self._path):
855 863 if f == '.git':
856 864 continue
857 865 path = os.path.join(self._path, f)
858 866 if os.path.isdir(path) and not os.path.islink(path):
859 867 shutil.rmtree(path)
860 868 else:
861 869 os.remove(path)
862 870
863 def archive(self, archiver, prefix):
871 def archive(self, ui, archiver, prefix):
864 872 source, revision = self._state
865 873 self._fetch(source, revision)
866 874
867 875 # Parse git's native archive command.
868 876 # This should be much faster than manually traversing the trees
869 877 # and objects with many subprocess calls.
870 878 tarstream = self._gitcommand(['archive', revision], stream=True)
871 879 tar = tarfile.open(fileobj=tarstream, mode='r|')
872 for info in tar:
880 relpath = subrelpath(self)
881 ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
882 for i, info in enumerate(tar):
873 883 archiver.addfile(os.path.join(prefix, self._relpath, info.name),
874 884 info.mode, info.issym(),
875 885 tar.extractfile(info).read())
886 ui.progress(_('archiving (%s)') % relpath, i + 1,
887 unit=_('files'))
888 ui.progress(_('archiving (%s)') % relpath, None)
889
876 890
877 891 types = {
878 892 'hg': hgsubrepo,
879 893 'svn': svnsubrepo,
880 894 'git': gitsubrepo,
881 895 }
@@ -1,348 +1,414 b''
1 1 Create test repository:
2 2
3 3 $ hg init repo
4 4 $ cd repo
5 5 $ echo x1 > x.txt
6 6
7 7 $ hg init foo
8 8 $ cd foo
9 9 $ echo y1 > y.txt
10 10
11 11 $ hg init bar
12 12 $ cd bar
13 13 $ echo z1 > z.txt
14 14
15 15 $ cd ..
16 16 $ echo 'bar = bar' > .hgsub
17 17
18 18 $ cd ..
19 19 $ echo 'foo = foo' > .hgsub
20 20
21 21 Add files --- .hgsub files must go first to trigger subrepos:
22 22
23 23 $ hg add -S .hgsub
24 24 $ hg add -S foo/.hgsub
25 25 $ hg add -S foo/bar
26 26 adding foo/bar/z.txt
27 27 $ hg add -S
28 28 adding x.txt
29 29 adding foo/y.txt
30 30
31 31 Test recursive status without committing anything:
32 32
33 33 $ hg status -S
34 34 A .hgsub
35 35 A foo/.hgsub
36 36 A foo/bar/z.txt
37 37 A foo/y.txt
38 38 A x.txt
39 39
40 40 Test recursive diff without committing anything:
41 41
42 42 $ hg diff --nodates -S foo
43 43 diff -r 000000000000 foo/.hgsub
44 44 --- /dev/null
45 45 +++ b/foo/.hgsub
46 46 @@ -0,0 +1,1 @@
47 47 +bar = bar
48 48 diff -r 000000000000 foo/y.txt
49 49 --- /dev/null
50 50 +++ b/foo/y.txt
51 51 @@ -0,0 +1,1 @@
52 52 +y1
53 53 diff -r 000000000000 foo/bar/z.txt
54 54 --- /dev/null
55 55 +++ b/foo/bar/z.txt
56 56 @@ -0,0 +1,1 @@
57 57 +z1
58 58
59 59 Commits:
60 60
61 61 $ hg commit -m 0-0-0
62 62 committing subrepository foo
63 63 committing subrepository foo/bar
64 64
65 65 $ cd foo
66 66 $ echo y2 >> y.txt
67 67 $ hg commit -m 0-1-0
68 68
69 69 $ cd bar
70 70 $ echo z2 >> z.txt
71 71 $ hg commit -m 0-1-1
72 72
73 73 $ cd ..
74 74 $ hg commit -m 0-2-1
75 75 committing subrepository bar
76 76
77 77 $ cd ..
78 78 $ hg commit -m 1-2-1
79 79 committing subrepository foo
80 80
81 81 Change working directory:
82 82
83 83 $ echo y3 >> foo/y.txt
84 84 $ echo z3 >> foo/bar/z.txt
85 85 $ hg status -S
86 86 M foo/bar/z.txt
87 87 M foo/y.txt
88 88 $ hg diff --nodates -S
89 89 diff -r d254738c5f5e foo/y.txt
90 90 --- a/foo/y.txt
91 91 +++ b/foo/y.txt
92 92 @@ -1,2 +1,3 @@
93 93 y1
94 94 y2
95 95 +y3
96 96 diff -r 9647f22de499 foo/bar/z.txt
97 97 --- a/foo/bar/z.txt
98 98 +++ b/foo/bar/z.txt
99 99 @@ -1,2 +1,3 @@
100 100 z1
101 101 z2
102 102 +z3
103 103
104 104 Status call crossing repository boundaries:
105 105
106 106 $ hg status -S foo/bar/z.txt
107 107 M foo/bar/z.txt
108 108 $ hg status -S -I 'foo/?.txt'
109 109 M foo/y.txt
110 110 $ hg status -S -I '**/?.txt'
111 111 M foo/bar/z.txt
112 112 M foo/y.txt
113 113 $ hg diff --nodates -S -I '**/?.txt'
114 114 diff -r d254738c5f5e foo/y.txt
115 115 --- a/foo/y.txt
116 116 +++ b/foo/y.txt
117 117 @@ -1,2 +1,3 @@
118 118 y1
119 119 y2
120 120 +y3
121 121 diff -r 9647f22de499 foo/bar/z.txt
122 122 --- a/foo/bar/z.txt
123 123 +++ b/foo/bar/z.txt
124 124 @@ -1,2 +1,3 @@
125 125 z1
126 126 z2
127 127 +z3
128 128
129 129 Status from within a subdirectory:
130 130
131 131 $ mkdir dir
132 132 $ cd dir
133 133 $ echo a1 > a.txt
134 134 $ hg status -S
135 135 M foo/bar/z.txt
136 136 M foo/y.txt
137 137 ? dir/a.txt
138 138 $ hg diff --nodates -S
139 139 diff -r d254738c5f5e foo/y.txt
140 140 --- a/foo/y.txt
141 141 +++ b/foo/y.txt
142 142 @@ -1,2 +1,3 @@
143 143 y1
144 144 y2
145 145 +y3
146 146 diff -r 9647f22de499 foo/bar/z.txt
147 147 --- a/foo/bar/z.txt
148 148 +++ b/foo/bar/z.txt
149 149 @@ -1,2 +1,3 @@
150 150 z1
151 151 z2
152 152 +z3
153 153
154 154 Status with relative path:
155 155
156 156 $ hg status -S ..
157 157 M ../foo/bar/z.txt
158 158 M ../foo/y.txt
159 159 ? a.txt
160 160 $ hg diff --nodates -S ..
161 161 diff -r d254738c5f5e foo/y.txt
162 162 --- a/foo/y.txt
163 163 +++ b/foo/y.txt
164 164 @@ -1,2 +1,3 @@
165 165 y1
166 166 y2
167 167 +y3
168 168 diff -r 9647f22de499 foo/bar/z.txt
169 169 --- a/foo/bar/z.txt
170 170 +++ b/foo/bar/z.txt
171 171 @@ -1,2 +1,3 @@
172 172 z1
173 173 z2
174 174 +z3
175 175 $ cd ..
176 176
177 177 Cleanup and final commit:
178 178
179 179 $ rm -r dir
180 180 $ hg commit -m 2-3-2
181 181 committing subrepository foo
182 182 committing subrepository foo/bar
183 183
184 184 Log with the relationships between repo and its subrepo:
185 185
186 186 $ hg log --template '{rev}:{node|short} {desc}\n'
187 187 2:1326fa26d0c0 2-3-2
188 188 1:4b3c9ff4f66b 1-2-1
189 189 0:23376cbba0d8 0-0-0
190 190
191 191 $ hg -R foo log --template '{rev}:{node|short} {desc}\n'
192 192 3:65903cebad86 2-3-2
193 193 2:d254738c5f5e 0-2-1
194 194 1:8629ce7dcc39 0-1-0
195 195 0:af048e97ade2 0-0-0
196 196
197 197 $ hg -R foo/bar log --template '{rev}:{node|short} {desc}\n'
198 198 2:31ecbdafd357 2-3-2
199 199 1:9647f22de499 0-1-1
200 200 0:4904098473f9 0-0-0
201 201
202 202 Status between revisions:
203 203
204 204 $ hg status -S
205 205 $ hg status -S --rev 0:1
206 206 M .hgsubstate
207 207 M foo/.hgsubstate
208 208 M foo/bar/z.txt
209 209 M foo/y.txt
210 210 $ hg diff --nodates -S -I '**/?.txt' --rev 0:1
211 211 diff -r af048e97ade2 -r d254738c5f5e foo/y.txt
212 212 --- a/foo/y.txt
213 213 +++ b/foo/y.txt
214 214 @@ -1,1 +1,2 @@
215 215 y1
216 216 +y2
217 217 diff -r 4904098473f9 -r 9647f22de499 foo/bar/z.txt
218 218 --- a/foo/bar/z.txt
219 219 +++ b/foo/bar/z.txt
220 220 @@ -1,1 +1,2 @@
221 221 z1
222 222 +z2
223 223
224 Test archiving to a directory tree:
224 Enable progress extension for archive tests:
225
226 $ cp $HGRCPATH $HGRCPATH.no-progress
227 $ cat >> $HGRCPATH <<EOF
228 > [extensions]
229 > progress =
230 > [progress]
231 > assume-tty = 1
232 > delay = 0
233 > refresh = 0
234 > width = 60
235 > EOF
236
237 Test archiving to a directory tree (the doubled lines in the output
238 only show up in the test output, not in real usage):
225 239
226 $ hg archive --subrepos ../archive
240 $ hg archive --subrepos ../archive 2>&1 | $TESTDIR/filtercr.py
241
242 archiving [ ] 0/3
243 archiving [ ] 0/3
244 archiving [=============> ] 1/3
245 archiving [=============> ] 1/3
246 archiving [===========================> ] 2/3
247 archiving [===========================> ] 2/3
248 archiving [==========================================>] 3/3
249 archiving [==========================================>] 3/3
250
251 archiving (foo) [ ] 0/3
252 archiving (foo) [ ] 0/3
253 archiving (foo) [===========> ] 1/3
254 archiving (foo) [===========> ] 1/3
255 archiving (foo) [=======================> ] 2/3
256 archiving (foo) [=======================> ] 2/3
257 archiving (foo) [====================================>] 3/3
258 archiving (foo) [====================================>] 3/3
259
260 archiving (foo/bar) [ ] 0/1
261 archiving (foo/bar) [ ] 0/1
262 archiving (foo/bar) [================================>] 1/1
263 archiving (foo/bar) [================================>] 1/1
264 \r (esc)
227 265 $ find ../archive | sort
228 266 ../archive
229 267 ../archive/.hg_archival.txt
230 268 ../archive/.hgsub
231 269 ../archive/.hgsubstate
232 270 ../archive/foo
233 271 ../archive/foo/.hgsub
234 272 ../archive/foo/.hgsubstate
235 273 ../archive/foo/bar
236 274 ../archive/foo/bar/z.txt
237 275 ../archive/foo/y.txt
238 276 ../archive/x.txt
239 277
240 278 Test archiving to zip file (unzip output is unstable):
241 279
242 $ hg archive --subrepos ../archive.zip
280 $ hg archive --subrepos ../archive.zip 2>&1 | $TESTDIR/filtercr.py
281
282 archiving [ ] 0/3
283 archiving [ ] 0/3
284 archiving [=============> ] 1/3
285 archiving [=============> ] 1/3
286 archiving [===========================> ] 2/3
287 archiving [===========================> ] 2/3
288 archiving [==========================================>] 3/3
289 archiving [==========================================>] 3/3
290
291 archiving (foo) [ ] 0/3
292 archiving (foo) [ ] 0/3
293 archiving (foo) [===========> ] 1/3
294 archiving (foo) [===========> ] 1/3
295 archiving (foo) [=======================> ] 2/3
296 archiving (foo) [=======================> ] 2/3
297 archiving (foo) [====================================>] 3/3
298 archiving (foo) [====================================>] 3/3
299
300 archiving (foo/bar) [ ] 0/1
301 archiving (foo/bar) [ ] 0/1
302 archiving (foo/bar) [================================>] 1/1
303 archiving (foo/bar) [================================>] 1/1
304 \r (esc)
305
306 Disable progress extension and cleanup:
307
308 $ mv $HGRCPATH.no-progress $HGRCPATH
243 309
244 310 Clone and test outgoing:
245 311
246 312 $ cd ..
247 313 $ hg clone repo repo2
248 314 updating to branch default
249 315 pulling subrepo foo from $TESTTMP/repo/foo
250 316 requesting all changes
251 317 adding changesets
252 318 adding manifests
253 319 adding file changes
254 320 added 4 changesets with 7 changes to 3 files
255 321 pulling subrepo foo/bar from $TESTTMP/repo/foo/bar
256 322 requesting all changes
257 323 adding changesets
258 324 adding manifests
259 325 adding file changes
260 326 added 3 changesets with 3 changes to 1 files
261 327 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
262 328 $ cd repo2
263 329 $ hg outgoing -S
264 330 comparing with $TESTTMP/repo
265 331 searching for changes
266 332 no changes found
267 333 comparing with $TESTTMP/repo/foo
268 334 searching for changes
269 335 no changes found
270 336 comparing with $TESTTMP/repo/foo/bar
271 337 searching for changes
272 338 no changes found
273 339 [1]
274 340
275 341 Make nested change:
276 342
277 343 $ echo y4 >> foo/y.txt
278 344 $ hg diff --nodates -S
279 345 diff -r 65903cebad86 foo/y.txt
280 346 --- a/foo/y.txt
281 347 +++ b/foo/y.txt
282 348 @@ -1,3 +1,4 @@
283 349 y1
284 350 y2
285 351 y3
286 352 +y4
287 353 $ hg commit -m 3-4-2
288 354 committing subrepository foo
289 355 $ hg outgoing -S
290 356 comparing with $TESTTMP/repo
291 357 searching for changes
292 358 changeset: 3:2655b8ecc4ee
293 359 tag: tip
294 360 user: test
295 361 date: Thu Jan 01 00:00:00 1970 +0000
296 362 summary: 3-4-2
297 363
298 364 comparing with $TESTTMP/repo/foo
299 365 searching for changes
300 366 changeset: 4:e96193d6cb36
301 367 tag: tip
302 368 user: test
303 369 date: Thu Jan 01 00:00:00 1970 +0000
304 370 summary: 3-4-2
305 371
306 372 comparing with $TESTTMP/repo/foo/bar
307 373 searching for changes
308 374 no changes found
309 375
310 376
311 377 Switch to original repo and setup default path:
312 378
313 379 $ cd ../repo
314 380 $ echo '[paths]' >> .hg/hgrc
315 381 $ echo 'default = ../repo2' >> .hg/hgrc
316 382
317 383 Test incoming:
318 384
319 385 $ hg incoming -S
320 386 comparing with $TESTTMP/repo2
321 387 searching for changes
322 388 changeset: 3:2655b8ecc4ee
323 389 tag: tip
324 390 user: test
325 391 date: Thu Jan 01 00:00:00 1970 +0000
326 392 summary: 3-4-2
327 393
328 394 comparing with $TESTTMP/repo2/foo
329 395 searching for changes
330 396 changeset: 4:e96193d6cb36
331 397 tag: tip
332 398 user: test
333 399 date: Thu Jan 01 00:00:00 1970 +0000
334 400 summary: 3-4-2
335 401
336 402 comparing with $TESTTMP/repo2/foo/bar
337 403 searching for changes
338 404 no changes found
339 405
340 406 $ hg incoming -S --bundle incoming.hg
341 407 abort: cannot combine --bundle and --subrepos
342 408 [255]
343 409
344 410 Test missing subrepo:
345 411
346 412 $ rm -r foo
347 413 $ hg status -S
348 414 warning: error "unknown revision '65903cebad86f1a84bd4f1134f62fa7dcb7a1c98'" in subrepository "foo"
General Comments 0
You need to be logged in to leave comments. Login now