##// END OF EJS Templates
subrepo: abort in hgsubrepo._get if the destination is obstructed...
Martin Geisler -
r15287:b3e19c35 stable
parent child Browse files
Show More
@@ -1,1117 +1,1117 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, posixpath
9 9 import stat, subprocess, tarfile
10 10 from i18n import _
11 11 import config, scmutil, util, node, error, cmdutil, bookmarks
12 12 hg = None
13 13 propertycache = util.propertycache
14 14
15 15 nullstate = ('', '', 'empty')
16 16
17 17 def state(ctx, ui):
18 18 """return a state dict, mapping subrepo paths configured in .hgsub
19 19 to tuple: (source from .hgsub, revision from .hgsubstate, kind
20 20 (key in types dict))
21 21 """
22 22 p = config.config()
23 23 def read(f, sections=None, remap=None):
24 24 if f in ctx:
25 25 try:
26 26 data = ctx[f].data()
27 27 except IOError, err:
28 28 if err.errno != errno.ENOENT:
29 29 raise
30 30 # handle missing subrepo spec files as removed
31 31 ui.warn(_("warning: subrepo spec file %s not found\n") % f)
32 32 return
33 33 p.parse(f, data, sections, remap, read)
34 34 else:
35 35 raise util.Abort(_("subrepo spec file %s not found") % f)
36 36
37 37 if '.hgsub' in ctx:
38 38 read('.hgsub')
39 39
40 40 for path, src in ui.configitems('subpaths'):
41 41 p.set('subpaths', path, src, ui.configsource('subpaths', path))
42 42
43 43 rev = {}
44 44 if '.hgsubstate' in ctx:
45 45 try:
46 46 for l in ctx['.hgsubstate'].data().splitlines():
47 47 revision, path = l.split(" ", 1)
48 48 rev[path] = revision
49 49 except IOError, err:
50 50 if err.errno != errno.ENOENT:
51 51 raise
52 52
53 53 def remap(src):
54 54 for pattern, repl in p.items('subpaths'):
55 55 # Turn r'C:\foo\bar' into r'C:\\foo\\bar' since re.sub
56 56 # does a string decode.
57 57 repl = repl.encode('string-escape')
58 58 # However, we still want to allow back references to go
59 59 # through unharmed, so we turn r'\\1' into r'\1'. Again,
60 60 # extra escapes are needed because re.sub string decodes.
61 61 repl = re.sub(r'\\\\([0-9]+)', r'\\\1', repl)
62 62 try:
63 63 src = re.sub(pattern, repl, src, 1)
64 64 except re.error, e:
65 65 raise util.Abort(_("bad subrepository pattern in %s: %s")
66 66 % (p.source('subpaths', pattern), e))
67 67 return src
68 68
69 69 state = {}
70 70 for path, src in p[''].items():
71 71 kind = 'hg'
72 72 if src.startswith('['):
73 73 if ']' not in src:
74 74 raise util.Abort(_('missing ] in subrepo source'))
75 75 kind, src = src.split(']', 1)
76 76 kind = kind[1:]
77 77 src = src.lstrip() # strip any extra whitespace after ']'
78 78
79 79 if not util.url(src).isabs():
80 80 parent = _abssource(ctx._repo, abort=False)
81 81 if parent:
82 82 parent = util.url(parent)
83 83 parent.path = posixpath.join(parent.path or '', src)
84 84 parent.path = posixpath.normpath(parent.path)
85 85 joined = str(parent)
86 86 # Remap the full joined path and use it if it changes,
87 87 # else remap the original source.
88 88 remapped = remap(joined)
89 89 if remapped == joined:
90 90 src = remap(src)
91 91 else:
92 92 src = remapped
93 93
94 94 src = remap(src)
95 95 state[path] = (src.strip(), rev.get(path, ''), kind)
96 96
97 97 return state
98 98
99 99 def writestate(repo, state):
100 100 """rewrite .hgsubstate in (outer) repo with these subrepo states"""
101 101 lines = ['%s %s\n' % (state[s][1], s) for s in sorted(state)]
102 102 repo.wwrite('.hgsubstate', ''.join(lines), '')
103 103
104 104 def submerge(repo, wctx, mctx, actx, overwrite):
105 105 """delegated from merge.applyupdates: merging of .hgsubstate file
106 106 in working context, merging context and ancestor context"""
107 107 if mctx == actx: # backwards?
108 108 actx = wctx.p1()
109 109 s1 = wctx.substate
110 110 s2 = mctx.substate
111 111 sa = actx.substate
112 112 sm = {}
113 113
114 114 repo.ui.debug("subrepo merge %s %s %s\n" % (wctx, mctx, actx))
115 115
116 116 def debug(s, msg, r=""):
117 117 if r:
118 118 r = "%s:%s:%s" % r
119 119 repo.ui.debug(" subrepo %s: %s %s\n" % (s, msg, r))
120 120
121 121 for s, l in s1.items():
122 122 a = sa.get(s, nullstate)
123 123 ld = l # local state with possible dirty flag for compares
124 124 if wctx.sub(s).dirty():
125 125 ld = (l[0], l[1] + "+")
126 126 if wctx == actx: # overwrite
127 127 a = ld
128 128
129 129 if s in s2:
130 130 r = s2[s]
131 131 if ld == r or r == a: # no change or local is newer
132 132 sm[s] = l
133 133 continue
134 134 elif ld == a: # other side changed
135 135 debug(s, "other changed, get", r)
136 136 wctx.sub(s).get(r, overwrite)
137 137 sm[s] = r
138 138 elif ld[0] != r[0]: # sources differ
139 139 if repo.ui.promptchoice(
140 140 _(' subrepository sources for %s differ\n'
141 141 'use (l)ocal source (%s) or (r)emote source (%s)?')
142 142 % (s, l[0], r[0]),
143 143 (_('&Local'), _('&Remote')), 0):
144 144 debug(s, "prompt changed, get", r)
145 145 wctx.sub(s).get(r, overwrite)
146 146 sm[s] = r
147 147 elif ld[1] == a[1]: # local side is unchanged
148 148 debug(s, "other side changed, get", r)
149 149 wctx.sub(s).get(r, overwrite)
150 150 sm[s] = r
151 151 else:
152 152 debug(s, "both sides changed, merge with", r)
153 153 wctx.sub(s).merge(r)
154 154 sm[s] = l
155 155 elif ld == a: # remote removed, local unchanged
156 156 debug(s, "remote removed, remove")
157 157 wctx.sub(s).remove()
158 158 elif a == nullstate: # not present in remote or ancestor
159 159 debug(s, "local added, keep")
160 160 sm[s] = l
161 161 continue
162 162 else:
163 163 if repo.ui.promptchoice(
164 164 _(' local changed subrepository %s which remote removed\n'
165 165 'use (c)hanged version or (d)elete?') % s,
166 166 (_('&Changed'), _('&Delete')), 0):
167 167 debug(s, "prompt remove")
168 168 wctx.sub(s).remove()
169 169
170 170 for s, r in sorted(s2.items()):
171 171 if s in s1:
172 172 continue
173 173 elif s not in sa:
174 174 debug(s, "remote added, get", r)
175 175 mctx.sub(s).get(r)
176 176 sm[s] = r
177 177 elif r != sa[s]:
178 178 if repo.ui.promptchoice(
179 179 _(' remote changed subrepository %s which local removed\n'
180 180 'use (c)hanged version or (d)elete?') % s,
181 181 (_('&Changed'), _('&Delete')), 0) == 0:
182 182 debug(s, "prompt recreate", r)
183 183 wctx.sub(s).get(r)
184 184 sm[s] = r
185 185
186 186 # record merged .hgsubstate
187 187 writestate(repo, sm)
188 188
189 189 def _updateprompt(ui, sub, dirty, local, remote):
190 190 if dirty:
191 191 msg = (_(' subrepository sources for %s differ\n'
192 192 'use (l)ocal source (%s) or (r)emote source (%s)?\n')
193 193 % (subrelpath(sub), local, remote))
194 194 else:
195 195 msg = (_(' subrepository sources for %s differ (in checked out version)\n'
196 196 'use (l)ocal source (%s) or (r)emote source (%s)?\n')
197 197 % (subrelpath(sub), local, remote))
198 198 return ui.promptchoice(msg, (_('&Local'), _('&Remote')), 0)
199 199
200 200 def reporelpath(repo):
201 201 """return path to this (sub)repo as seen from outermost repo"""
202 202 parent = repo
203 203 while util.safehasattr(parent, '_subparent'):
204 204 parent = parent._subparent
205 205 p = parent.root.rstrip(os.sep)
206 206 return repo.root[len(p) + 1:]
207 207
208 208 def subrelpath(sub):
209 209 """return path to this subrepo as seen from outermost repo"""
210 210 if util.safehasattr(sub, '_relpath'):
211 211 return sub._relpath
212 212 if not util.safehasattr(sub, '_repo'):
213 213 return sub._path
214 214 return reporelpath(sub._repo)
215 215
216 216 def _abssource(repo, push=False, abort=True):
217 217 """return pull/push path of repo - either based on parent repo .hgsub info
218 218 or on the top repo config. Abort or return None if no source found."""
219 219 if util.safehasattr(repo, '_subparent'):
220 220 source = util.url(repo._subsource)
221 221 if source.isabs():
222 222 return str(source)
223 223 source.path = posixpath.normpath(source.path)
224 224 parent = _abssource(repo._subparent, push, abort=False)
225 225 if parent:
226 226 parent = util.url(parent)
227 227 parent.path = posixpath.join(parent.path or '', source.path)
228 228 parent.path = posixpath.normpath(parent.path)
229 229 return str(parent)
230 230 else: # recursion reached top repo
231 231 if util.safehasattr(repo, '_subtoppath'):
232 232 return repo._subtoppath
233 233 if push and repo.ui.config('paths', 'default-push'):
234 234 return repo.ui.config('paths', 'default-push')
235 235 if repo.ui.config('paths', 'default'):
236 236 return repo.ui.config('paths', 'default')
237 237 if abort:
238 238 raise util.Abort(_("default path for subrepository %s not found") %
239 239 reporelpath(repo))
240 240
241 241 def itersubrepos(ctx1, ctx2):
242 242 """find subrepos in ctx1 or ctx2"""
243 243 # Create a (subpath, ctx) mapping where we prefer subpaths from
244 244 # ctx1. The subpaths from ctx2 are important when the .hgsub file
245 245 # has been modified (in ctx2) but not yet committed (in ctx1).
246 246 subpaths = dict.fromkeys(ctx2.substate, ctx2)
247 247 subpaths.update(dict.fromkeys(ctx1.substate, ctx1))
248 248 for subpath, ctx in sorted(subpaths.iteritems()):
249 249 yield subpath, ctx.sub(subpath)
250 250
251 251 def subrepo(ctx, path):
252 252 """return instance of the right subrepo class for subrepo in path"""
253 253 # subrepo inherently violates our import layering rules
254 254 # because it wants to make repo objects from deep inside the stack
255 255 # so we manually delay the circular imports to not break
256 256 # scripts that don't use our demand-loading
257 257 global hg
258 258 import hg as h
259 259 hg = h
260 260
261 261 scmutil.pathauditor(ctx._repo.root)(path)
262 262 state = ctx.substate.get(path, nullstate)
263 263 if state[2] not in types:
264 264 raise util.Abort(_('unknown subrepo type %s') % state[2])
265 265 return types[state[2]](ctx, path, state[:2])
266 266
267 267 # subrepo classes need to implement the following abstract class:
268 268
269 269 class abstractsubrepo(object):
270 270
271 271 def dirty(self, ignoreupdate=False):
272 272 """returns true if the dirstate of the subrepo is dirty or does not
273 273 match current stored state. If ignoreupdate is true, only check
274 274 whether the subrepo has uncommitted changes in its dirstate.
275 275 """
276 276 raise NotImplementedError
277 277
278 278 def checknested(self, path):
279 279 """check if path is a subrepository within this repository"""
280 280 return False
281 281
282 282 def commit(self, text, user, date):
283 283 """commit the current changes to the subrepo with the given
284 284 log message. Use given user and date if possible. Return the
285 285 new state of the subrepo.
286 286 """
287 287 raise NotImplementedError
288 288
289 289 def remove(self):
290 290 """remove the subrepo
291 291
292 292 (should verify the dirstate is not dirty first)
293 293 """
294 294 raise NotImplementedError
295 295
296 296 def get(self, state, overwrite=False):
297 297 """run whatever commands are needed to put the subrepo into
298 298 this state
299 299 """
300 300 raise NotImplementedError
301 301
302 302 def merge(self, state):
303 303 """merge currently-saved state with the new state."""
304 304 raise NotImplementedError
305 305
306 306 def push(self, force):
307 307 """perform whatever action is analogous to 'hg push'
308 308
309 309 This may be a no-op on some systems.
310 310 """
311 311 raise NotImplementedError
312 312
313 313 def add(self, ui, match, dryrun, prefix):
314 314 return []
315 315
316 316 def status(self, rev2, **opts):
317 317 return [], [], [], [], [], [], []
318 318
319 319 def diff(self, diffopts, node2, match, prefix, **opts):
320 320 pass
321 321
322 322 def outgoing(self, ui, dest, opts):
323 323 return 1
324 324
325 325 def incoming(self, ui, source, opts):
326 326 return 1
327 327
328 328 def files(self):
329 329 """return filename iterator"""
330 330 raise NotImplementedError
331 331
332 332 def filedata(self, name):
333 333 """return file data"""
334 334 raise NotImplementedError
335 335
336 336 def fileflags(self, name):
337 337 """return file flags"""
338 338 return ''
339 339
340 340 def archive(self, ui, archiver, prefix):
341 341 files = self.files()
342 342 total = len(files)
343 343 relpath = subrelpath(self)
344 344 ui.progress(_('archiving (%s)') % relpath, 0,
345 345 unit=_('files'), total=total)
346 346 for i, name in enumerate(files):
347 347 flags = self.fileflags(name)
348 348 mode = 'x' in flags and 0755 or 0644
349 349 symlink = 'l' in flags
350 350 archiver.addfile(os.path.join(prefix, self._path, name),
351 351 mode, symlink, self.filedata(name))
352 352 ui.progress(_('archiving (%s)') % relpath, i + 1,
353 353 unit=_('files'), total=total)
354 354 ui.progress(_('archiving (%s)') % relpath, None)
355 355
356 356
357 357 class hgsubrepo(abstractsubrepo):
358 358 def __init__(self, ctx, path, state):
359 359 self._path = path
360 360 self._state = state
361 361 r = ctx._repo
362 362 root = r.wjoin(path)
363 363 create = False
364 364 if not os.path.exists(os.path.join(root, '.hg')):
365 365 create = True
366 366 util.makedirs(root)
367 367 self._repo = hg.repository(r.ui, root, create=create)
368 368 self._initrepo(r, state[0], create)
369 369
370 370 def _initrepo(self, parentrepo, source, create):
371 371 self._repo._subparent = parentrepo
372 372 self._repo._subsource = source
373 373
374 374 if create:
375 375 fp = self._repo.opener("hgrc", "w", text=True)
376 376 fp.write('[paths]\n')
377 377
378 378 def addpathconfig(key, value):
379 379 if value:
380 380 fp.write('%s = %s\n' % (key, value))
381 381 self._repo.ui.setconfig('paths', key, value)
382 382
383 383 defpath = _abssource(self._repo, abort=False)
384 384 defpushpath = _abssource(self._repo, True, abort=False)
385 385 addpathconfig('default', defpath)
386 386 if defpath != defpushpath:
387 387 addpathconfig('default-push', defpushpath)
388 388 fp.close()
389 389
390 390 def add(self, ui, match, dryrun, prefix):
391 391 return cmdutil.add(ui, self._repo, match, dryrun, True,
392 392 os.path.join(prefix, self._path))
393 393
394 394 def status(self, rev2, **opts):
395 395 try:
396 396 rev1 = self._state[1]
397 397 ctx1 = self._repo[rev1]
398 398 ctx2 = self._repo[rev2]
399 399 return self._repo.status(ctx1, ctx2, **opts)
400 400 except error.RepoLookupError, inst:
401 401 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
402 402 % (inst, subrelpath(self)))
403 403 return [], [], [], [], [], [], []
404 404
405 405 def diff(self, diffopts, node2, match, prefix, **opts):
406 406 try:
407 407 node1 = node.bin(self._state[1])
408 408 # We currently expect node2 to come from substate and be
409 409 # in hex format
410 410 if node2 is not None:
411 411 node2 = node.bin(node2)
412 412 cmdutil.diffordiffstat(self._repo.ui, self._repo, diffopts,
413 413 node1, node2, match,
414 414 prefix=os.path.join(prefix, self._path),
415 415 listsubrepos=True, **opts)
416 416 except error.RepoLookupError, inst:
417 417 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
418 418 % (inst, subrelpath(self)))
419 419
420 420 def archive(self, ui, archiver, prefix):
421 421 self._get(self._state + ('hg',))
422 422 abstractsubrepo.archive(self, ui, archiver, prefix)
423 423
424 424 rev = self._state[1]
425 425 ctx = self._repo[rev]
426 426 for subpath in ctx.substate:
427 427 s = subrepo(ctx, subpath)
428 428 s.archive(ui, archiver, os.path.join(prefix, self._path))
429 429
430 430 def dirty(self, ignoreupdate=False):
431 431 r = self._state[1]
432 432 if r == '' and not ignoreupdate: # no state recorded
433 433 return True
434 434 w = self._repo[None]
435 435 if r != w.p1().hex() and not ignoreupdate:
436 436 # different version checked out
437 437 return True
438 438 return w.dirty() # working directory changed
439 439
440 440 def checknested(self, path):
441 441 return self._repo._checknested(self._repo.wjoin(path))
442 442
443 443 def commit(self, text, user, date):
444 444 # don't bother committing in the subrepo if it's only been
445 445 # updated
446 446 if not self.dirty(True):
447 447 return self._repo['.'].hex()
448 448 self._repo.ui.debug("committing subrepo %s\n" % subrelpath(self))
449 449 n = self._repo.commit(text, user, date)
450 450 if not n:
451 451 return self._repo['.'].hex() # different version checked out
452 452 return node.hex(n)
453 453
454 454 def remove(self):
455 455 # we can't fully delete the repository as it may contain
456 456 # local-only history
457 457 self._repo.ui.note(_('removing subrepo %s\n') % subrelpath(self))
458 458 hg.clean(self._repo, node.nullid, False)
459 459
460 460 def _get(self, state):
461 461 source, revision, kind = state
462 462 if revision not in self._repo:
463 463 self._repo._subsource = source
464 464 srcurl = _abssource(self._repo)
465 465 other = hg.peer(self._repo.ui, {}, srcurl)
466 466 if len(self._repo) == 0:
467 467 self._repo.ui.status(_('cloning subrepo %s from %s\n')
468 468 % (subrelpath(self), srcurl))
469 469 parentrepo = self._repo._subparent
470 shutil.rmtree(self._repo.root)
470 shutil.rmtree(self._repo.path)
471 471 other, self._repo = hg.clone(self._repo._subparent.ui, {}, other,
472 472 self._repo.root, update=False)
473 473 self._initrepo(parentrepo, source, create=True)
474 474 else:
475 475 self._repo.ui.status(_('pulling subrepo %s from %s\n')
476 476 % (subrelpath(self), srcurl))
477 477 self._repo.pull(other)
478 478 bookmarks.updatefromremote(self._repo.ui, self._repo, other)
479 479
480 480 def get(self, state, overwrite=False):
481 481 self._get(state)
482 482 source, revision, kind = state
483 483 self._repo.ui.debug("getting subrepo %s\n" % self._path)
484 484 hg.clean(self._repo, revision, False)
485 485
486 486 def merge(self, state):
487 487 self._get(state)
488 488 cur = self._repo['.']
489 489 dst = self._repo[state[1]]
490 490 anc = dst.ancestor(cur)
491 491
492 492 def mergefunc():
493 493 if anc == cur:
494 494 self._repo.ui.debug("updating subrepo %s\n" % subrelpath(self))
495 495 hg.update(self._repo, state[1])
496 496 elif anc == dst:
497 497 self._repo.ui.debug("skipping subrepo %s\n" % subrelpath(self))
498 498 else:
499 499 self._repo.ui.debug("merging subrepo %s\n" % subrelpath(self))
500 500 hg.merge(self._repo, state[1], remind=False)
501 501
502 502 wctx = self._repo[None]
503 503 if self.dirty():
504 504 if anc != dst:
505 505 if _updateprompt(self._repo.ui, self, wctx.dirty(), cur, dst):
506 506 mergefunc()
507 507 else:
508 508 mergefunc()
509 509 else:
510 510 mergefunc()
511 511
512 512 def push(self, force):
513 513 # push subrepos depth-first for coherent ordering
514 514 c = self._repo['']
515 515 subs = c.substate # only repos that are committed
516 516 for s in sorted(subs):
517 517 if not c.sub(s).push(force):
518 518 return False
519 519
520 520 dsturl = _abssource(self._repo, True)
521 521 self._repo.ui.status(_('pushing subrepo %s to %s\n') %
522 522 (subrelpath(self), dsturl))
523 523 other = hg.peer(self._repo.ui, {}, dsturl)
524 524 return self._repo.push(other, force)
525 525
526 526 def outgoing(self, ui, dest, opts):
527 527 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
528 528
529 529 def incoming(self, ui, source, opts):
530 530 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
531 531
532 532 def files(self):
533 533 rev = self._state[1]
534 534 ctx = self._repo[rev]
535 535 return ctx.manifest()
536 536
537 537 def filedata(self, name):
538 538 rev = self._state[1]
539 539 return self._repo[rev][name].data()
540 540
541 541 def fileflags(self, name):
542 542 rev = self._state[1]
543 543 ctx = self._repo[rev]
544 544 return ctx.flags(name)
545 545
546 546
547 547 class svnsubrepo(abstractsubrepo):
548 548 def __init__(self, ctx, path, state):
549 549 self._path = path
550 550 self._state = state
551 551 self._ctx = ctx
552 552 self._ui = ctx._repo.ui
553 553 self._exe = util.findexe('svn')
554 554 if not self._exe:
555 555 raise util.Abort(_("'svn' executable not found for subrepo '%s'")
556 556 % self._path)
557 557
558 558 def _svncommand(self, commands, filename='', failok=False):
559 559 cmd = [self._exe]
560 560 extrakw = {}
561 561 if not self._ui.interactive():
562 562 # Making stdin be a pipe should prevent svn from behaving
563 563 # interactively even if we can't pass --non-interactive.
564 564 extrakw['stdin'] = subprocess.PIPE
565 565 # Starting in svn 1.5 --non-interactive is a global flag
566 566 # instead of being per-command, but we need to support 1.4 so
567 567 # we have to be intelligent about what commands take
568 568 # --non-interactive.
569 569 if commands[0] in ('update', 'checkout', 'commit'):
570 570 cmd.append('--non-interactive')
571 571 cmd.extend(commands)
572 572 if filename is not None:
573 573 path = os.path.join(self._ctx._repo.origroot, self._path, filename)
574 574 cmd.append(path)
575 575 env = dict(os.environ)
576 576 # Avoid localized output, preserve current locale for everything else.
577 577 env['LC_MESSAGES'] = 'C'
578 578 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
579 579 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
580 580 universal_newlines=True, env=env, **extrakw)
581 581 stdout, stderr = p.communicate()
582 582 stderr = stderr.strip()
583 583 if not failok:
584 584 if p.returncode:
585 585 raise util.Abort(stderr or 'exited with code %d' % p.returncode)
586 586 if stderr:
587 587 self._ui.warn(stderr + '\n')
588 588 return stdout, stderr
589 589
590 590 @propertycache
591 591 def _svnversion(self):
592 592 output, err = self._svncommand(['--version'], filename=None)
593 593 m = re.search(r'^svn,\s+version\s+(\d+)\.(\d+)', output)
594 594 if not m:
595 595 raise util.Abort(_('cannot retrieve svn tool version'))
596 596 return (int(m.group(1)), int(m.group(2)))
597 597
598 598 def _wcrevs(self):
599 599 # Get the working directory revision as well as the last
600 600 # commit revision so we can compare the subrepo state with
601 601 # both. We used to store the working directory one.
602 602 output, err = self._svncommand(['info', '--xml'])
603 603 doc = xml.dom.minidom.parseString(output)
604 604 entries = doc.getElementsByTagName('entry')
605 605 lastrev, rev = '0', '0'
606 606 if entries:
607 607 rev = str(entries[0].getAttribute('revision')) or '0'
608 608 commits = entries[0].getElementsByTagName('commit')
609 609 if commits:
610 610 lastrev = str(commits[0].getAttribute('revision')) or '0'
611 611 return (lastrev, rev)
612 612
613 613 def _wcrev(self):
614 614 return self._wcrevs()[0]
615 615
616 616 def _wcchanged(self):
617 617 """Return (changes, extchanges) where changes is True
618 618 if the working directory was changed, and extchanges is
619 619 True if any of these changes concern an external entry.
620 620 """
621 621 output, err = self._svncommand(['status', '--xml'])
622 622 externals, changes = [], []
623 623 doc = xml.dom.minidom.parseString(output)
624 624 for e in doc.getElementsByTagName('entry'):
625 625 s = e.getElementsByTagName('wc-status')
626 626 if not s:
627 627 continue
628 628 item = s[0].getAttribute('item')
629 629 props = s[0].getAttribute('props')
630 630 path = e.getAttribute('path')
631 631 if item == 'external':
632 632 externals.append(path)
633 633 if (item not in ('', 'normal', 'unversioned', 'external')
634 634 or props not in ('', 'none', 'normal')):
635 635 changes.append(path)
636 636 for path in changes:
637 637 for ext in externals:
638 638 if path == ext or path.startswith(ext + os.sep):
639 639 return True, True
640 640 return bool(changes), False
641 641
642 642 def dirty(self, ignoreupdate=False):
643 643 if not self._wcchanged()[0]:
644 644 if self._state[1] in self._wcrevs() or ignoreupdate:
645 645 return False
646 646 return True
647 647
648 648 def commit(self, text, user, date):
649 649 # user and date are out of our hands since svn is centralized
650 650 changed, extchanged = self._wcchanged()
651 651 if not changed:
652 652 return self._wcrev()
653 653 if extchanged:
654 654 # Do not try to commit externals
655 655 raise util.Abort(_('cannot commit svn externals'))
656 656 commitinfo, err = self._svncommand(['commit', '-m', text])
657 657 self._ui.status(commitinfo)
658 658 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
659 659 if not newrev:
660 660 raise util.Abort(commitinfo.splitlines()[-1])
661 661 newrev = newrev.groups()[0]
662 662 self._ui.status(self._svncommand(['update', '-r', newrev])[0])
663 663 return newrev
664 664
665 665 def remove(self):
666 666 if self.dirty():
667 667 self._ui.warn(_('not removing repo %s because '
668 668 'it has changes.\n' % self._path))
669 669 return
670 670 self._ui.note(_('removing subrepo %s\n') % self._path)
671 671
672 672 def onerror(function, path, excinfo):
673 673 if function is not os.remove:
674 674 raise
675 675 # read-only files cannot be unlinked under Windows
676 676 s = os.stat(path)
677 677 if (s.st_mode & stat.S_IWRITE) != 0:
678 678 raise
679 679 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
680 680 os.remove(path)
681 681
682 682 path = self._ctx._repo.wjoin(self._path)
683 683 shutil.rmtree(path, onerror=onerror)
684 684 try:
685 685 os.removedirs(os.path.dirname(path))
686 686 except OSError:
687 687 pass
688 688
689 689 def get(self, state, overwrite=False):
690 690 if overwrite:
691 691 self._svncommand(['revert', '--recursive'])
692 692 args = ['checkout']
693 693 if self._svnversion >= (1, 5):
694 694 args.append('--force')
695 695 # The revision must be specified at the end of the URL to properly
696 696 # update to a directory which has since been deleted and recreated.
697 697 args.append('%s@%s' % (state[0], state[1]))
698 698 status, err = self._svncommand(args, failok=True)
699 699 if not re.search('Checked out revision [0-9]+.', status):
700 700 if ('is already a working copy for a different URL' in err
701 701 and (self._wcchanged() == (False, False))):
702 702 # obstructed but clean working copy, so just blow it away.
703 703 self.remove()
704 704 self.get(state, overwrite=False)
705 705 return
706 706 raise util.Abort((status or err).splitlines()[-1])
707 707 self._ui.status(status)
708 708
709 709 def merge(self, state):
710 710 old = self._state[1]
711 711 new = state[1]
712 712 if new != self._wcrev():
713 713 dirty = old == self._wcrev() or self._wcchanged()[0]
714 714 if _updateprompt(self._ui, self, dirty, self._wcrev(), new):
715 715 self.get(state, False)
716 716
717 717 def push(self, force):
718 718 # push is a no-op for SVN
719 719 return True
720 720
721 721 def files(self):
722 722 output = self._svncommand(['list'])
723 723 # This works because svn forbids \n in filenames.
724 724 return output.splitlines()
725 725
726 726 def filedata(self, name):
727 727 return self._svncommand(['cat'], name)
728 728
729 729
730 730 class gitsubrepo(abstractsubrepo):
731 731 def __init__(self, ctx, path, state):
732 732 # TODO add git version check.
733 733 self._state = state
734 734 self._ctx = ctx
735 735 self._path = path
736 736 self._relpath = os.path.join(reporelpath(ctx._repo), path)
737 737 self._abspath = ctx._repo.wjoin(path)
738 738 self._subparent = ctx._repo
739 739 self._ui = ctx._repo.ui
740 740
741 741 def _gitcommand(self, commands, env=None, stream=False):
742 742 return self._gitdir(commands, env=env, stream=stream)[0]
743 743
744 744 def _gitdir(self, commands, env=None, stream=False):
745 745 return self._gitnodir(commands, env=env, stream=stream,
746 746 cwd=self._abspath)
747 747
748 748 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
749 749 """Calls the git command
750 750
751 751 The methods tries to call the git command. versions previor to 1.6.0
752 752 are not supported and very probably fail.
753 753 """
754 754 self._ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
755 755 # unless ui.quiet is set, print git's stderr,
756 756 # which is mostly progress and useful info
757 757 errpipe = None
758 758 if self._ui.quiet:
759 759 errpipe = open(os.devnull, 'w')
760 760 p = subprocess.Popen(['git'] + commands, bufsize=-1, cwd=cwd, env=env,
761 761 close_fds=util.closefds,
762 762 stdout=subprocess.PIPE, stderr=errpipe)
763 763 if stream:
764 764 return p.stdout, None
765 765
766 766 retdata = p.stdout.read().strip()
767 767 # wait for the child to exit to avoid race condition.
768 768 p.wait()
769 769
770 770 if p.returncode != 0 and p.returncode != 1:
771 771 # there are certain error codes that are ok
772 772 command = commands[0]
773 773 if command in ('cat-file', 'symbolic-ref'):
774 774 return retdata, p.returncode
775 775 # for all others, abort
776 776 raise util.Abort('git %s error %d in %s' %
777 777 (command, p.returncode, self._relpath))
778 778
779 779 return retdata, p.returncode
780 780
781 781 def _gitmissing(self):
782 782 return not os.path.exists(os.path.join(self._abspath, '.git'))
783 783
784 784 def _gitstate(self):
785 785 return self._gitcommand(['rev-parse', 'HEAD'])
786 786
787 787 def _gitcurrentbranch(self):
788 788 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
789 789 if err:
790 790 current = None
791 791 return current
792 792
793 793 def _gitremote(self, remote):
794 794 out = self._gitcommand(['remote', 'show', '-n', remote])
795 795 line = out.split('\n')[1]
796 796 i = line.index('URL: ') + len('URL: ')
797 797 return line[i:]
798 798
799 799 def _githavelocally(self, revision):
800 800 out, code = self._gitdir(['cat-file', '-e', revision])
801 801 return code == 0
802 802
803 803 def _gitisancestor(self, r1, r2):
804 804 base = self._gitcommand(['merge-base', r1, r2])
805 805 return base == r1
806 806
807 807 def _gitisbare(self):
808 808 return self._gitcommand(['config', '--bool', 'core.bare']) == 'true'
809 809
810 810 def _gitbranchmap(self):
811 811 '''returns 2 things:
812 812 a map from git branch to revision
813 813 a map from revision to branches'''
814 814 branch2rev = {}
815 815 rev2branch = {}
816 816
817 817 out = self._gitcommand(['for-each-ref', '--format',
818 818 '%(objectname) %(refname)'])
819 819 for line in out.split('\n'):
820 820 revision, ref = line.split(' ')
821 821 if (not ref.startswith('refs/heads/') and
822 822 not ref.startswith('refs/remotes/')):
823 823 continue
824 824 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
825 825 continue # ignore remote/HEAD redirects
826 826 branch2rev[ref] = revision
827 827 rev2branch.setdefault(revision, []).append(ref)
828 828 return branch2rev, rev2branch
829 829
830 830 def _gittracking(self, branches):
831 831 'return map of remote branch to local tracking branch'
832 832 # assumes no more than one local tracking branch for each remote
833 833 tracking = {}
834 834 for b in branches:
835 835 if b.startswith('refs/remotes/'):
836 836 continue
837 837 bname = b.split('/', 2)[2]
838 838 remote = self._gitcommand(['config', 'branch.%s.remote' % bname])
839 839 if remote:
840 840 ref = self._gitcommand(['config', 'branch.%s.merge' % bname])
841 841 tracking['refs/remotes/%s/%s' %
842 842 (remote, ref.split('/', 2)[2])] = b
843 843 return tracking
844 844
845 845 def _abssource(self, source):
846 846 if '://' not in source:
847 847 # recognize the scp syntax as an absolute source
848 848 colon = source.find(':')
849 849 if colon != -1 and '/' not in source[:colon]:
850 850 return source
851 851 self._subsource = source
852 852 return _abssource(self)
853 853
854 854 def _fetch(self, source, revision):
855 855 if self._gitmissing():
856 856 source = self._abssource(source)
857 857 self._ui.status(_('cloning subrepo %s from %s\n') %
858 858 (self._relpath, source))
859 859 self._gitnodir(['clone', source, self._abspath])
860 860 if self._githavelocally(revision):
861 861 return
862 862 self._ui.status(_('pulling subrepo %s from %s\n') %
863 863 (self._relpath, self._gitremote('origin')))
864 864 # try only origin: the originally cloned repo
865 865 self._gitcommand(['fetch'])
866 866 if not self._githavelocally(revision):
867 867 raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
868 868 (revision, self._relpath))
869 869
870 870 def dirty(self, ignoreupdate=False):
871 871 if self._gitmissing():
872 872 return self._state[1] != ''
873 873 if self._gitisbare():
874 874 return True
875 875 if not ignoreupdate and self._state[1] != self._gitstate():
876 876 # different version checked out
877 877 return True
878 878 # check for staged changes or modified files; ignore untracked files
879 879 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
880 880 return code == 1
881 881
882 882 def get(self, state, overwrite=False):
883 883 source, revision, kind = state
884 884 if not revision:
885 885 self.remove()
886 886 return
887 887 self._fetch(source, revision)
888 888 # if the repo was set to be bare, unbare it
889 889 if self._gitisbare():
890 890 self._gitcommand(['config', 'core.bare', 'false'])
891 891 if self._gitstate() == revision:
892 892 self._gitcommand(['reset', '--hard', 'HEAD'])
893 893 return
894 894 elif self._gitstate() == revision:
895 895 if overwrite:
896 896 # first reset the index to unmark new files for commit, because
897 897 # reset --hard will otherwise throw away files added for commit,
898 898 # not just unmark them.
899 899 self._gitcommand(['reset', 'HEAD'])
900 900 self._gitcommand(['reset', '--hard', 'HEAD'])
901 901 return
902 902 branch2rev, rev2branch = self._gitbranchmap()
903 903
904 904 def checkout(args):
905 905 cmd = ['checkout']
906 906 if overwrite:
907 907 # first reset the index to unmark new files for commit, because
908 908 # the -f option will otherwise throw away files added for
909 909 # commit, not just unmark them.
910 910 self._gitcommand(['reset', 'HEAD'])
911 911 cmd.append('-f')
912 912 self._gitcommand(cmd + args)
913 913
914 914 def rawcheckout():
915 915 # no branch to checkout, check it out with no branch
916 916 self._ui.warn(_('checking out detached HEAD in subrepo %s\n') %
917 917 self._relpath)
918 918 self._ui.warn(_('check out a git branch if you intend '
919 919 'to make changes\n'))
920 920 checkout(['-q', revision])
921 921
922 922 if revision not in rev2branch:
923 923 rawcheckout()
924 924 return
925 925 branches = rev2branch[revision]
926 926 firstlocalbranch = None
927 927 for b in branches:
928 928 if b == 'refs/heads/master':
929 929 # master trumps all other branches
930 930 checkout(['refs/heads/master'])
931 931 return
932 932 if not firstlocalbranch and not b.startswith('refs/remotes/'):
933 933 firstlocalbranch = b
934 934 if firstlocalbranch:
935 935 checkout([firstlocalbranch])
936 936 return
937 937
938 938 tracking = self._gittracking(branch2rev.keys())
939 939 # choose a remote branch already tracked if possible
940 940 remote = branches[0]
941 941 if remote not in tracking:
942 942 for b in branches:
943 943 if b in tracking:
944 944 remote = b
945 945 break
946 946
947 947 if remote not in tracking:
948 948 # create a new local tracking branch
949 949 local = remote.split('/', 2)[2]
950 950 checkout(['-b', local, remote])
951 951 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
952 952 # When updating to a tracked remote branch,
953 953 # if the local tracking branch is downstream of it,
954 954 # a normal `git pull` would have performed a "fast-forward merge"
955 955 # which is equivalent to updating the local branch to the remote.
956 956 # Since we are only looking at branching at update, we need to
957 957 # detect this situation and perform this action lazily.
958 958 if tracking[remote] != self._gitcurrentbranch():
959 959 checkout([tracking[remote]])
960 960 self._gitcommand(['merge', '--ff', remote])
961 961 else:
962 962 # a real merge would be required, just checkout the revision
963 963 rawcheckout()
964 964
965 965 def commit(self, text, user, date):
966 966 if self._gitmissing():
967 967 raise util.Abort(_("subrepo %s is missing") % self._relpath)
968 968 cmd = ['commit', '-a', '-m', text]
969 969 env = os.environ.copy()
970 970 if user:
971 971 cmd += ['--author', user]
972 972 if date:
973 973 # git's date parser silently ignores when seconds < 1e9
974 974 # convert to ISO8601
975 975 env['GIT_AUTHOR_DATE'] = util.datestr(date,
976 976 '%Y-%m-%dT%H:%M:%S %1%2')
977 977 self._gitcommand(cmd, env=env)
978 978 # make sure commit works otherwise HEAD might not exist under certain
979 979 # circumstances
980 980 return self._gitstate()
981 981
982 982 def merge(self, state):
983 983 source, revision, kind = state
984 984 self._fetch(source, revision)
985 985 base = self._gitcommand(['merge-base', revision, self._state[1]])
986 986 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
987 987
988 988 def mergefunc():
989 989 if base == revision:
990 990 self.get(state) # fast forward merge
991 991 elif base != self._state[1]:
992 992 self._gitcommand(['merge', '--no-commit', revision])
993 993
994 994 if self.dirty():
995 995 if self._gitstate() != revision:
996 996 dirty = self._gitstate() == self._state[1] or code != 0
997 997 if _updateprompt(self._ui, self, dirty,
998 998 self._state[1][:7], revision[:7]):
999 999 mergefunc()
1000 1000 else:
1001 1001 mergefunc()
1002 1002
1003 1003 def push(self, force):
1004 1004 if not self._state[1]:
1005 1005 return True
1006 1006 if self._gitmissing():
1007 1007 raise util.Abort(_("subrepo %s is missing") % self._relpath)
1008 1008 # if a branch in origin contains the revision, nothing to do
1009 1009 branch2rev, rev2branch = self._gitbranchmap()
1010 1010 if self._state[1] in rev2branch:
1011 1011 for b in rev2branch[self._state[1]]:
1012 1012 if b.startswith('refs/remotes/origin/'):
1013 1013 return True
1014 1014 for b, revision in branch2rev.iteritems():
1015 1015 if b.startswith('refs/remotes/origin/'):
1016 1016 if self._gitisancestor(self._state[1], revision):
1017 1017 return True
1018 1018 # otherwise, try to push the currently checked out branch
1019 1019 cmd = ['push']
1020 1020 if force:
1021 1021 cmd.append('--force')
1022 1022
1023 1023 current = self._gitcurrentbranch()
1024 1024 if current:
1025 1025 # determine if the current branch is even useful
1026 1026 if not self._gitisancestor(self._state[1], current):
1027 1027 self._ui.warn(_('unrelated git branch checked out '
1028 1028 'in subrepo %s\n') % self._relpath)
1029 1029 return False
1030 1030 self._ui.status(_('pushing branch %s of subrepo %s\n') %
1031 1031 (current.split('/', 2)[2], self._relpath))
1032 1032 self._gitcommand(cmd + ['origin', current])
1033 1033 return True
1034 1034 else:
1035 1035 self._ui.warn(_('no branch checked out in subrepo %s\n'
1036 1036 'cannot push revision %s') %
1037 1037 (self._relpath, self._state[1]))
1038 1038 return False
1039 1039
1040 1040 def remove(self):
1041 1041 if self._gitmissing():
1042 1042 return
1043 1043 if self.dirty():
1044 1044 self._ui.warn(_('not removing repo %s because '
1045 1045 'it has changes.\n') % self._relpath)
1046 1046 return
1047 1047 # we can't fully delete the repository as it may contain
1048 1048 # local-only history
1049 1049 self._ui.note(_('removing subrepo %s\n') % self._relpath)
1050 1050 self._gitcommand(['config', 'core.bare', 'true'])
1051 1051 for f in os.listdir(self._abspath):
1052 1052 if f == '.git':
1053 1053 continue
1054 1054 path = os.path.join(self._abspath, f)
1055 1055 if os.path.isdir(path) and not os.path.islink(path):
1056 1056 shutil.rmtree(path)
1057 1057 else:
1058 1058 os.remove(path)
1059 1059
1060 1060 def archive(self, ui, archiver, prefix):
1061 1061 source, revision = self._state
1062 1062 if not revision:
1063 1063 return
1064 1064 self._fetch(source, revision)
1065 1065
1066 1066 # Parse git's native archive command.
1067 1067 # This should be much faster than manually traversing the trees
1068 1068 # and objects with many subprocess calls.
1069 1069 tarstream = self._gitcommand(['archive', revision], stream=True)
1070 1070 tar = tarfile.open(fileobj=tarstream, mode='r|')
1071 1071 relpath = subrelpath(self)
1072 1072 ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
1073 1073 for i, info in enumerate(tar):
1074 1074 if info.isdir():
1075 1075 continue
1076 1076 if info.issym():
1077 1077 data = info.linkname
1078 1078 else:
1079 1079 data = tar.extractfile(info).read()
1080 1080 archiver.addfile(os.path.join(prefix, self._path, info.name),
1081 1081 info.mode, info.issym(), data)
1082 1082 ui.progress(_('archiving (%s)') % relpath, i + 1,
1083 1083 unit=_('files'))
1084 1084 ui.progress(_('archiving (%s)') % relpath, None)
1085 1085
1086 1086
1087 1087 def status(self, rev2, **opts):
1088 1088 rev1 = self._state[1]
1089 1089 if self._gitmissing() or not rev1:
1090 1090 # if the repo is missing, return no results
1091 1091 return [], [], [], [], [], [], []
1092 1092 modified, added, removed = [], [], []
1093 1093 if rev2:
1094 1094 command = ['diff-tree', rev1, rev2]
1095 1095 else:
1096 1096 command = ['diff-index', rev1]
1097 1097 out = self._gitcommand(command)
1098 1098 for line in out.split('\n'):
1099 1099 tab = line.find('\t')
1100 1100 if tab == -1:
1101 1101 continue
1102 1102 status, f = line[tab - 1], line[tab + 1:]
1103 1103 if status == 'M':
1104 1104 modified.append(f)
1105 1105 elif status == 'A':
1106 1106 added.append(f)
1107 1107 elif status == 'D':
1108 1108 removed.append(f)
1109 1109
1110 1110 deleted = unknown = ignored = clean = []
1111 1111 return modified, added, removed, deleted, unknown, ignored, clean
1112 1112
1113 1113 types = {
1114 1114 'hg': hgsubrepo,
1115 1115 'svn': svnsubrepo,
1116 1116 'git': gitsubrepo,
1117 1117 }
@@ -1,458 +1,470 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 224 Enable progress extension for archive tests:
225 225
226 226 $ cp $HGRCPATH $HGRCPATH.no-progress
227 227 $ cat >> $HGRCPATH <<EOF
228 228 > [extensions]
229 229 > progress =
230 230 > [progress]
231 231 > assume-tty = 1
232 232 > delay = 0
233 233 > format = topic bar number
234 234 > refresh = 0
235 235 > width = 60
236 236 > EOF
237 237
238 238 Test archiving to a directory tree (the doubled lines in the output
239 239 only show up in the test output, not in real usage):
240 240
241 241 $ hg archive --subrepos ../archive 2>&1 | $TESTDIR/filtercr.py
242 242
243 243 archiving [ ] 0/3
244 244 archiving [ ] 0/3
245 245 archiving [=============> ] 1/3
246 246 archiving [=============> ] 1/3
247 247 archiving [===========================> ] 2/3
248 248 archiving [===========================> ] 2/3
249 249 archiving [==========================================>] 3/3
250 250 archiving [==========================================>] 3/3
251 251
252 252 archiving (foo) [ ] 0/3
253 253 archiving (foo) [ ] 0/3
254 254 archiving (foo) [===========> ] 1/3
255 255 archiving (foo) [===========> ] 1/3
256 256 archiving (foo) [=======================> ] 2/3
257 257 archiving (foo) [=======================> ] 2/3
258 258 archiving (foo) [====================================>] 3/3
259 259 archiving (foo) [====================================>] 3/3
260 260
261 261 archiving (foo/bar) [ ] 0/1
262 262 archiving (foo/bar) [ ] 0/1
263 263 archiving (foo/bar) [================================>] 1/1
264 264 archiving (foo/bar) [================================>] 1/1
265 265 \r (esc)
266 266 $ find ../archive | sort
267 267 ../archive
268 268 ../archive/.hg_archival.txt
269 269 ../archive/.hgsub
270 270 ../archive/.hgsubstate
271 271 ../archive/foo
272 272 ../archive/foo/.hgsub
273 273 ../archive/foo/.hgsubstate
274 274 ../archive/foo/bar
275 275 ../archive/foo/bar/z.txt
276 276 ../archive/foo/y.txt
277 277 ../archive/x.txt
278 278
279 279 Test archiving to zip file (unzip output is unstable):
280 280
281 281 $ hg archive --subrepos ../archive.zip 2>&1 | $TESTDIR/filtercr.py
282 282
283 283 archiving [ ] 0/3
284 284 archiving [ ] 0/3
285 285 archiving [=============> ] 1/3
286 286 archiving [=============> ] 1/3
287 287 archiving [===========================> ] 2/3
288 288 archiving [===========================> ] 2/3
289 289 archiving [==========================================>] 3/3
290 290 archiving [==========================================>] 3/3
291 291
292 292 archiving (foo) [ ] 0/3
293 293 archiving (foo) [ ] 0/3
294 294 archiving (foo) [===========> ] 1/3
295 295 archiving (foo) [===========> ] 1/3
296 296 archiving (foo) [=======================> ] 2/3
297 297 archiving (foo) [=======================> ] 2/3
298 298 archiving (foo) [====================================>] 3/3
299 299 archiving (foo) [====================================>] 3/3
300 300
301 301 archiving (foo/bar) [ ] 0/1
302 302 archiving (foo/bar) [ ] 0/1
303 303 archiving (foo/bar) [================================>] 1/1
304 304 archiving (foo/bar) [================================>] 1/1
305 305 \r (esc)
306 306
307 307 Test archiving a revision that references a subrepo that is not yet
308 308 cloned:
309 309
310 310 $ hg clone -U . ../empty
311 311 $ cd ../empty
312 312 $ hg archive --subrepos -r tip ../archive.tar.gz 2>&1 | $TESTDIR/filtercr.py
313 313
314 314 archiving [ ] 0/3
315 315 archiving [ ] 0/3
316 316 archiving [=============> ] 1/3
317 317 archiving [=============> ] 1/3
318 318 archiving [===========================> ] 2/3
319 319 archiving [===========================> ] 2/3
320 320 archiving [==========================================>] 3/3
321 321 archiving [==========================================>] 3/3
322 322
323 323 archiving (foo) [ ] 0/3
324 324 archiving (foo) [ ] 0/3
325 325 archiving (foo) [===========> ] 1/3
326 326 archiving (foo) [===========> ] 1/3
327 327 archiving (foo) [=======================> ] 2/3
328 328 archiving (foo) [=======================> ] 2/3
329 329 archiving (foo) [====================================>] 3/3
330 330 archiving (foo) [====================================>] 3/3
331 331
332 332 archiving (foo/bar) [ ] 0/1
333 333 archiving (foo/bar) [ ] 0/1
334 334 archiving (foo/bar) [================================>] 1/1
335 335 archiving (foo/bar) [================================>] 1/1
336 336
337 337 cloning subrepo foo from $TESTTMP/repo/foo
338 338 cloning subrepo foo/bar from $TESTTMP/repo/foo/bar
339 339
340 340 The newly cloned subrepos contain no working copy:
341 341
342 342 $ hg -R foo summary
343 343 parent: -1:000000000000 (no revision checked out)
344 344 branch: default
345 345 commit: (clean)
346 346 update: 4 new changesets (update)
347 347
348 348 Disable progress extension and cleanup:
349 349
350 350 $ mv $HGRCPATH.no-progress $HGRCPATH
351 351
352 Test archiving when there is a directory in the way for a subrepo
353 created by archive:
354
355 $ hg clone -U . ../almost-empty
356 $ cd ../almost-empty
357 $ mkdir foo
358 $ echo f > foo/f
359 $ hg archive --subrepos -r tip archive
360 cloning subrepo foo from $TESTTMP/empty/foo
361 abort: destination '$TESTTMP/almost-empty/foo' is not empty
362 [255]
363
352 364 Clone and test outgoing:
353 365
354 366 $ cd ..
355 367 $ hg clone repo repo2
356 368 updating to branch default
357 369 cloning subrepo foo from $TESTTMP/repo/foo
358 370 cloning subrepo foo/bar from $TESTTMP/repo/foo/bar
359 371 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
360 372 $ cd repo2
361 373 $ hg outgoing -S
362 374 comparing with $TESTTMP/repo
363 375 searching for changes
364 376 no changes found
365 377 comparing with $TESTTMP/repo/foo
366 378 searching for changes
367 379 no changes found
368 380 comparing with $TESTTMP/repo/foo/bar
369 381 searching for changes
370 382 no changes found
371 383 [1]
372 384
373 385 Make nested change:
374 386
375 387 $ echo y4 >> foo/y.txt
376 388 $ hg diff --nodates -S
377 389 diff -r 65903cebad86 foo/y.txt
378 390 --- a/foo/y.txt
379 391 +++ b/foo/y.txt
380 392 @@ -1,3 +1,4 @@
381 393 y1
382 394 y2
383 395 y3
384 396 +y4
385 397 $ hg commit -m 3-4-2
386 398 committing subrepository foo
387 399 $ hg outgoing -S
388 400 comparing with $TESTTMP/repo
389 401 searching for changes
390 402 changeset: 3:2655b8ecc4ee
391 403 tag: tip
392 404 user: test
393 405 date: Thu Jan 01 00:00:00 1970 +0000
394 406 summary: 3-4-2
395 407
396 408 comparing with $TESTTMP/repo/foo
397 409 searching for changes
398 410 changeset: 4:e96193d6cb36
399 411 tag: tip
400 412 user: test
401 413 date: Thu Jan 01 00:00:00 1970 +0000
402 414 summary: 3-4-2
403 415
404 416 comparing with $TESTTMP/repo/foo/bar
405 417 searching for changes
406 418 no changes found
407 419
408 420
409 421 Switch to original repo and setup default path:
410 422
411 423 $ cd ../repo
412 424 $ echo '[paths]' >> .hg/hgrc
413 425 $ echo 'default = ../repo2' >> .hg/hgrc
414 426
415 427 Test incoming:
416 428
417 429 $ hg incoming -S
418 430 comparing with $TESTTMP/repo2
419 431 searching for changes
420 432 changeset: 3:2655b8ecc4ee
421 433 tag: tip
422 434 user: test
423 435 date: Thu Jan 01 00:00:00 1970 +0000
424 436 summary: 3-4-2
425 437
426 438 comparing with $TESTTMP/repo2/foo
427 439 searching for changes
428 440 changeset: 4:e96193d6cb36
429 441 tag: tip
430 442 user: test
431 443 date: Thu Jan 01 00:00:00 1970 +0000
432 444 summary: 3-4-2
433 445
434 446 comparing with $TESTTMP/repo2/foo/bar
435 447 searching for changes
436 448 no changes found
437 449
438 450 $ hg incoming -S --bundle incoming.hg
439 451 abort: cannot combine --bundle and --subrepos
440 452 [255]
441 453
442 454 Test missing subrepo:
443 455
444 456 $ rm -r foo
445 457 $ hg status -S
446 458 warning: error "unknown revision '65903cebad86f1a84bd4f1134f62fa7dcb7a1c98'" in subrepository "foo"
447 459
448 460 Issue2619: IndexError: list index out of range on hg add with subrepos
449 461 The subrepo must sorts after the explicit filename.
450 462
451 463 $ cd ..
452 464 $ hg init test
453 465 $ cd test
454 466 $ hg init x
455 467 $ echo "x = x" >> .hgsub
456 468 $ hg add .hgsub
457 469 $ touch a x/a
458 470 $ hg add a x/a
General Comments 0
You need to be logged in to leave comments. Login now