##// END OF EJS Templates
add: support adding explicit files in subrepos...
David M. Carr -
r15410:9e99d2bb default
parent child Browse files
Show More
@@ -1,1264 +1,1268
1 1 # cmdutil.py - help for command processing in mercurial
2 2 #
3 3 # Copyright 2005-2007 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 from node import hex, nullid, nullrev, short
9 9 from i18n import _
10 10 import os, sys, errno, re, tempfile
11 11 import util, scmutil, templater, patch, error, templatekw, revlog, copies
12 12 import match as matchmod
13 13 import subrepo
14 14
15 15 def parsealiases(cmd):
16 16 return cmd.lstrip("^").split("|")
17 17
18 18 def findpossible(cmd, table, strict=False):
19 19 """
20 20 Return cmd -> (aliases, command table entry)
21 21 for each matching command.
22 22 Return debug commands (or their aliases) only if no normal command matches.
23 23 """
24 24 choice = {}
25 25 debugchoice = {}
26 26 for e in table.keys():
27 27 aliases = parsealiases(e)
28 28 found = None
29 29 if cmd in aliases:
30 30 found = cmd
31 31 elif not strict:
32 32 for a in aliases:
33 33 if a.startswith(cmd):
34 34 found = a
35 35 break
36 36 if found is not None:
37 37 if aliases[0].startswith("debug") or found.startswith("debug"):
38 38 debugchoice[found] = (aliases, table[e])
39 39 else:
40 40 choice[found] = (aliases, table[e])
41 41
42 42 if not choice and debugchoice:
43 43 choice = debugchoice
44 44
45 45 return choice
46 46
47 47 def findcmd(cmd, table, strict=True):
48 48 """Return (aliases, command table entry) for command string."""
49 49 choice = findpossible(cmd, table, strict)
50 50
51 51 if cmd in choice:
52 52 return choice[cmd]
53 53
54 54 if len(choice) > 1:
55 55 clist = choice.keys()
56 56 clist.sort()
57 57 raise error.AmbiguousCommand(cmd, clist)
58 58
59 59 if choice:
60 60 return choice.values()[0]
61 61
62 62 raise error.UnknownCommand(cmd)
63 63
64 64 def findrepo(p):
65 65 while not os.path.isdir(os.path.join(p, ".hg")):
66 66 oldp, p = p, os.path.dirname(p)
67 67 if p == oldp:
68 68 return None
69 69
70 70 return p
71 71
72 72 def bailifchanged(repo):
73 73 if repo.dirstate.p2() != nullid:
74 74 raise util.Abort(_('outstanding uncommitted merge'))
75 75 modified, added, removed, deleted = repo.status()[:4]
76 76 if modified or added or removed or deleted:
77 77 raise util.Abort(_("outstanding uncommitted changes"))
78 78 ctx = repo[None]
79 79 for s in ctx.substate:
80 80 if ctx.sub(s).dirty():
81 81 raise util.Abort(_("uncommitted changes in subrepo %s") % s)
82 82
83 83 def logmessage(ui, opts):
84 84 """ get the log message according to -m and -l option """
85 85 message = opts.get('message')
86 86 logfile = opts.get('logfile')
87 87
88 88 if message and logfile:
89 89 raise util.Abort(_('options --message and --logfile are mutually '
90 90 'exclusive'))
91 91 if not message and logfile:
92 92 try:
93 93 if logfile == '-':
94 94 message = ui.fin.read()
95 95 else:
96 96 message = '\n'.join(util.readfile(logfile).splitlines())
97 97 except IOError, inst:
98 98 raise util.Abort(_("can't read commit message '%s': %s") %
99 99 (logfile, inst.strerror))
100 100 return message
101 101
102 102 def loglimit(opts):
103 103 """get the log limit according to option -l/--limit"""
104 104 limit = opts.get('limit')
105 105 if limit:
106 106 try:
107 107 limit = int(limit)
108 108 except ValueError:
109 109 raise util.Abort(_('limit must be a positive integer'))
110 110 if limit <= 0:
111 111 raise util.Abort(_('limit must be positive'))
112 112 else:
113 113 limit = None
114 114 return limit
115 115
116 116 def makefilename(repo, pat, node, desc=None,
117 117 total=None, seqno=None, revwidth=None, pathname=None):
118 118 node_expander = {
119 119 'H': lambda: hex(node),
120 120 'R': lambda: str(repo.changelog.rev(node)),
121 121 'h': lambda: short(node),
122 122 'm': lambda: re.sub('[^\w]', '_', str(desc))
123 123 }
124 124 expander = {
125 125 '%': lambda: '%',
126 126 'b': lambda: os.path.basename(repo.root),
127 127 }
128 128
129 129 try:
130 130 if node:
131 131 expander.update(node_expander)
132 132 if node:
133 133 expander['r'] = (lambda:
134 134 str(repo.changelog.rev(node)).zfill(revwidth or 0))
135 135 if total is not None:
136 136 expander['N'] = lambda: str(total)
137 137 if seqno is not None:
138 138 expander['n'] = lambda: str(seqno)
139 139 if total is not None and seqno is not None:
140 140 expander['n'] = lambda: str(seqno).zfill(len(str(total)))
141 141 if pathname is not None:
142 142 expander['s'] = lambda: os.path.basename(pathname)
143 143 expander['d'] = lambda: os.path.dirname(pathname) or '.'
144 144 expander['p'] = lambda: pathname
145 145
146 146 newname = []
147 147 patlen = len(pat)
148 148 i = 0
149 149 while i < patlen:
150 150 c = pat[i]
151 151 if c == '%':
152 152 i += 1
153 153 c = pat[i]
154 154 c = expander[c]()
155 155 newname.append(c)
156 156 i += 1
157 157 return ''.join(newname)
158 158 except KeyError, inst:
159 159 raise util.Abort(_("invalid format spec '%%%s' in output filename") %
160 160 inst.args[0])
161 161
162 162 def makefileobj(repo, pat, node=None, desc=None, total=None,
163 163 seqno=None, revwidth=None, mode='wb', pathname=None):
164 164
165 165 writable = mode not in ('r', 'rb')
166 166
167 167 if not pat or pat == '-':
168 168 fp = writable and repo.ui.fout or repo.ui.fin
169 169 if util.safehasattr(fp, 'fileno'):
170 170 return os.fdopen(os.dup(fp.fileno()), mode)
171 171 else:
172 172 # if this fp can't be duped properly, return
173 173 # a dummy object that can be closed
174 174 class wrappedfileobj(object):
175 175 noop = lambda x: None
176 176 def __init__(self, f):
177 177 self.f = f
178 178 def __getattr__(self, attr):
179 179 if attr == 'close':
180 180 return self.noop
181 181 else:
182 182 return getattr(self.f, attr)
183 183
184 184 return wrappedfileobj(fp)
185 185 if util.safehasattr(pat, 'write') and writable:
186 186 return pat
187 187 if util.safehasattr(pat, 'read') and 'r' in mode:
188 188 return pat
189 189 return open(makefilename(repo, pat, node, desc, total, seqno, revwidth,
190 190 pathname),
191 191 mode)
192 192
193 193 def openrevlog(repo, cmd, file_, opts):
194 194 """opens the changelog, manifest, a filelog or a given revlog"""
195 195 cl = opts['changelog']
196 196 mf = opts['manifest']
197 197 msg = None
198 198 if cl and mf:
199 199 msg = _('cannot specify --changelog and --manifest at the same time')
200 200 elif cl or mf:
201 201 if file_:
202 202 msg = _('cannot specify filename with --changelog or --manifest')
203 203 elif not repo:
204 204 msg = _('cannot specify --changelog or --manifest '
205 205 'without a repository')
206 206 if msg:
207 207 raise util.Abort(msg)
208 208
209 209 r = None
210 210 if repo:
211 211 if cl:
212 212 r = repo.changelog
213 213 elif mf:
214 214 r = repo.manifest
215 215 elif file_:
216 216 filelog = repo.file(file_)
217 217 if len(filelog):
218 218 r = filelog
219 219 if not r:
220 220 if not file_:
221 221 raise error.CommandError(cmd, _('invalid arguments'))
222 222 if not os.path.isfile(file_):
223 223 raise util.Abort(_("revlog '%s' not found") % file_)
224 224 r = revlog.revlog(scmutil.opener(os.getcwd(), audit=False),
225 225 file_[:-2] + ".i")
226 226 return r
227 227
228 228 def copy(ui, repo, pats, opts, rename=False):
229 229 # called with the repo lock held
230 230 #
231 231 # hgsep => pathname that uses "/" to separate directories
232 232 # ossep => pathname that uses os.sep to separate directories
233 233 cwd = repo.getcwd()
234 234 targets = {}
235 235 after = opts.get("after")
236 236 dryrun = opts.get("dry_run")
237 237 wctx = repo[None]
238 238
239 239 def walkpat(pat):
240 240 srcs = []
241 241 badstates = after and '?' or '?r'
242 242 m = scmutil.match(repo[None], [pat], opts, globbed=True)
243 243 for abs in repo.walk(m):
244 244 state = repo.dirstate[abs]
245 245 rel = m.rel(abs)
246 246 exact = m.exact(abs)
247 247 if state in badstates:
248 248 if exact and state == '?':
249 249 ui.warn(_('%s: not copying - file is not managed\n') % rel)
250 250 if exact and state == 'r':
251 251 ui.warn(_('%s: not copying - file has been marked for'
252 252 ' remove\n') % rel)
253 253 continue
254 254 # abs: hgsep
255 255 # rel: ossep
256 256 srcs.append((abs, rel, exact))
257 257 return srcs
258 258
259 259 # abssrc: hgsep
260 260 # relsrc: ossep
261 261 # otarget: ossep
262 262 def copyfile(abssrc, relsrc, otarget, exact):
263 263 abstarget = scmutil.canonpath(repo.root, cwd, otarget)
264 264 reltarget = repo.pathto(abstarget, cwd)
265 265 target = repo.wjoin(abstarget)
266 266 src = repo.wjoin(abssrc)
267 267 state = repo.dirstate[abstarget]
268 268
269 269 scmutil.checkportable(ui, abstarget)
270 270
271 271 # check for collisions
272 272 prevsrc = targets.get(abstarget)
273 273 if prevsrc is not None:
274 274 ui.warn(_('%s: not overwriting - %s collides with %s\n') %
275 275 (reltarget, repo.pathto(abssrc, cwd),
276 276 repo.pathto(prevsrc, cwd)))
277 277 return
278 278
279 279 # check for overwrites
280 280 exists = os.path.lexists(target)
281 281 if not after and exists or after and state in 'mn':
282 282 if not opts['force']:
283 283 ui.warn(_('%s: not overwriting - file exists\n') %
284 284 reltarget)
285 285 return
286 286
287 287 if after:
288 288 if not exists:
289 289 if rename:
290 290 ui.warn(_('%s: not recording move - %s does not exist\n') %
291 291 (relsrc, reltarget))
292 292 else:
293 293 ui.warn(_('%s: not recording copy - %s does not exist\n') %
294 294 (relsrc, reltarget))
295 295 return
296 296 elif not dryrun:
297 297 try:
298 298 if exists:
299 299 os.unlink(target)
300 300 targetdir = os.path.dirname(target) or '.'
301 301 if not os.path.isdir(targetdir):
302 302 os.makedirs(targetdir)
303 303 util.copyfile(src, target)
304 304 srcexists = True
305 305 except IOError, inst:
306 306 if inst.errno == errno.ENOENT:
307 307 ui.warn(_('%s: deleted in working copy\n') % relsrc)
308 308 srcexists = False
309 309 else:
310 310 ui.warn(_('%s: cannot copy - %s\n') %
311 311 (relsrc, inst.strerror))
312 312 return True # report a failure
313 313
314 314 if ui.verbose or not exact:
315 315 if rename:
316 316 ui.status(_('moving %s to %s\n') % (relsrc, reltarget))
317 317 else:
318 318 ui.status(_('copying %s to %s\n') % (relsrc, reltarget))
319 319
320 320 targets[abstarget] = abssrc
321 321
322 322 # fix up dirstate
323 323 scmutil.dirstatecopy(ui, repo, wctx, abssrc, abstarget,
324 324 dryrun=dryrun, cwd=cwd)
325 325 if rename and not dryrun:
326 326 if not after and srcexists:
327 327 util.unlinkpath(repo.wjoin(abssrc))
328 328 wctx.forget([abssrc])
329 329
330 330 # pat: ossep
331 331 # dest ossep
332 332 # srcs: list of (hgsep, hgsep, ossep, bool)
333 333 # return: function that takes hgsep and returns ossep
334 334 def targetpathfn(pat, dest, srcs):
335 335 if os.path.isdir(pat):
336 336 abspfx = scmutil.canonpath(repo.root, cwd, pat)
337 337 abspfx = util.localpath(abspfx)
338 338 if destdirexists:
339 339 striplen = len(os.path.split(abspfx)[0])
340 340 else:
341 341 striplen = len(abspfx)
342 342 if striplen:
343 343 striplen += len(os.sep)
344 344 res = lambda p: os.path.join(dest, util.localpath(p)[striplen:])
345 345 elif destdirexists:
346 346 res = lambda p: os.path.join(dest,
347 347 os.path.basename(util.localpath(p)))
348 348 else:
349 349 res = lambda p: dest
350 350 return res
351 351
352 352 # pat: ossep
353 353 # dest ossep
354 354 # srcs: list of (hgsep, hgsep, ossep, bool)
355 355 # return: function that takes hgsep and returns ossep
356 356 def targetpathafterfn(pat, dest, srcs):
357 357 if matchmod.patkind(pat):
358 358 # a mercurial pattern
359 359 res = lambda p: os.path.join(dest,
360 360 os.path.basename(util.localpath(p)))
361 361 else:
362 362 abspfx = scmutil.canonpath(repo.root, cwd, pat)
363 363 if len(abspfx) < len(srcs[0][0]):
364 364 # A directory. Either the target path contains the last
365 365 # component of the source path or it does not.
366 366 def evalpath(striplen):
367 367 score = 0
368 368 for s in srcs:
369 369 t = os.path.join(dest, util.localpath(s[0])[striplen:])
370 370 if os.path.lexists(t):
371 371 score += 1
372 372 return score
373 373
374 374 abspfx = util.localpath(abspfx)
375 375 striplen = len(abspfx)
376 376 if striplen:
377 377 striplen += len(os.sep)
378 378 if os.path.isdir(os.path.join(dest, os.path.split(abspfx)[1])):
379 379 score = evalpath(striplen)
380 380 striplen1 = len(os.path.split(abspfx)[0])
381 381 if striplen1:
382 382 striplen1 += len(os.sep)
383 383 if evalpath(striplen1) > score:
384 384 striplen = striplen1
385 385 res = lambda p: os.path.join(dest,
386 386 util.localpath(p)[striplen:])
387 387 else:
388 388 # a file
389 389 if destdirexists:
390 390 res = lambda p: os.path.join(dest,
391 391 os.path.basename(util.localpath(p)))
392 392 else:
393 393 res = lambda p: dest
394 394 return res
395 395
396 396
397 397 pats = scmutil.expandpats(pats)
398 398 if not pats:
399 399 raise util.Abort(_('no source or destination specified'))
400 400 if len(pats) == 1:
401 401 raise util.Abort(_('no destination specified'))
402 402 dest = pats.pop()
403 403 destdirexists = os.path.isdir(dest) and not os.path.islink(dest)
404 404 if not destdirexists:
405 405 if len(pats) > 1 or matchmod.patkind(pats[0]):
406 406 raise util.Abort(_('with multiple sources, destination must be an '
407 407 'existing directory'))
408 408 if util.endswithsep(dest):
409 409 raise util.Abort(_('destination %s is not a directory') % dest)
410 410
411 411 tfn = targetpathfn
412 412 if after:
413 413 tfn = targetpathafterfn
414 414 copylist = []
415 415 for pat in pats:
416 416 srcs = walkpat(pat)
417 417 if not srcs:
418 418 continue
419 419 copylist.append((tfn(pat, dest, srcs), srcs))
420 420 if not copylist:
421 421 raise util.Abort(_('no files to copy'))
422 422
423 423 errors = 0
424 424 for targetpath, srcs in copylist:
425 425 for abssrc, relsrc, exact in srcs:
426 426 if copyfile(abssrc, relsrc, targetpath(abssrc), exact):
427 427 errors += 1
428 428
429 429 if errors:
430 430 ui.warn(_('(consider using --after)\n'))
431 431
432 432 return errors != 0
433 433
434 434 def service(opts, parentfn=None, initfn=None, runfn=None, logfile=None,
435 435 runargs=None, appendpid=False):
436 436 '''Run a command as a service.'''
437 437
438 438 if opts['daemon'] and not opts['daemon_pipefds']:
439 439 # Signal child process startup with file removal
440 440 lockfd, lockpath = tempfile.mkstemp(prefix='hg-service-')
441 441 os.close(lockfd)
442 442 try:
443 443 if not runargs:
444 444 runargs = util.hgcmd() + sys.argv[1:]
445 445 runargs.append('--daemon-pipefds=%s' % lockpath)
446 446 # Don't pass --cwd to the child process, because we've already
447 447 # changed directory.
448 448 for i in xrange(1, len(runargs)):
449 449 if runargs[i].startswith('--cwd='):
450 450 del runargs[i]
451 451 break
452 452 elif runargs[i].startswith('--cwd'):
453 453 del runargs[i:i + 2]
454 454 break
455 455 def condfn():
456 456 return not os.path.exists(lockpath)
457 457 pid = util.rundetached(runargs, condfn)
458 458 if pid < 0:
459 459 raise util.Abort(_('child process failed to start'))
460 460 finally:
461 461 try:
462 462 os.unlink(lockpath)
463 463 except OSError, e:
464 464 if e.errno != errno.ENOENT:
465 465 raise
466 466 if parentfn:
467 467 return parentfn(pid)
468 468 else:
469 469 return
470 470
471 471 if initfn:
472 472 initfn()
473 473
474 474 if opts['pid_file']:
475 475 mode = appendpid and 'a' or 'w'
476 476 fp = open(opts['pid_file'], mode)
477 477 fp.write(str(os.getpid()) + '\n')
478 478 fp.close()
479 479
480 480 if opts['daemon_pipefds']:
481 481 lockpath = opts['daemon_pipefds']
482 482 try:
483 483 os.setsid()
484 484 except AttributeError:
485 485 pass
486 486 os.unlink(lockpath)
487 487 util.hidewindow()
488 488 sys.stdout.flush()
489 489 sys.stderr.flush()
490 490
491 491 nullfd = os.open(util.nulldev, os.O_RDWR)
492 492 logfilefd = nullfd
493 493 if logfile:
494 494 logfilefd = os.open(logfile, os.O_RDWR | os.O_CREAT | os.O_APPEND)
495 495 os.dup2(nullfd, 0)
496 496 os.dup2(logfilefd, 1)
497 497 os.dup2(logfilefd, 2)
498 498 if nullfd not in (0, 1, 2):
499 499 os.close(nullfd)
500 500 if logfile and logfilefd not in (0, 1, 2):
501 501 os.close(logfilefd)
502 502
503 503 if runfn:
504 504 return runfn()
505 505
506 506 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
507 507 opts=None):
508 508 '''export changesets as hg patches.'''
509 509
510 510 total = len(revs)
511 511 revwidth = max([len(str(rev)) for rev in revs])
512 512
513 513 def single(rev, seqno, fp):
514 514 ctx = repo[rev]
515 515 node = ctx.node()
516 516 parents = [p.node() for p in ctx.parents() if p]
517 517 branch = ctx.branch()
518 518 if switch_parent:
519 519 parents.reverse()
520 520 prev = (parents and parents[0]) or nullid
521 521
522 522 shouldclose = False
523 523 if not fp:
524 524 desc_lines = ctx.description().rstrip().split('\n')
525 525 desc = desc_lines[0] #Commit always has a first line.
526 526 fp = makefileobj(repo, template, node, desc=desc, total=total,
527 527 seqno=seqno, revwidth=revwidth, mode='ab')
528 528 if fp != template:
529 529 shouldclose = True
530 530 if fp != sys.stdout and util.safehasattr(fp, 'name'):
531 531 repo.ui.note("%s\n" % fp.name)
532 532
533 533 fp.write("# HG changeset patch\n")
534 534 fp.write("# User %s\n" % ctx.user())
535 535 fp.write("# Date %d %d\n" % ctx.date())
536 536 if branch and branch != 'default':
537 537 fp.write("# Branch %s\n" % branch)
538 538 fp.write("# Node ID %s\n" % hex(node))
539 539 fp.write("# Parent %s\n" % hex(prev))
540 540 if len(parents) > 1:
541 541 fp.write("# Parent %s\n" % hex(parents[1]))
542 542 fp.write(ctx.description().rstrip())
543 543 fp.write("\n\n")
544 544
545 545 for chunk in patch.diff(repo, prev, node, opts=opts):
546 546 fp.write(chunk)
547 547
548 548 if shouldclose:
549 549 fp.close()
550 550
551 551 for seqno, rev in enumerate(revs):
552 552 single(rev, seqno + 1, fp)
553 553
554 554 def diffordiffstat(ui, repo, diffopts, node1, node2, match,
555 555 changes=None, stat=False, fp=None, prefix='',
556 556 listsubrepos=False):
557 557 '''show diff or diffstat.'''
558 558 if fp is None:
559 559 write = ui.write
560 560 else:
561 561 def write(s, **kw):
562 562 fp.write(s)
563 563
564 564 if stat:
565 565 diffopts = diffopts.copy(context=0)
566 566 width = 80
567 567 if not ui.plain():
568 568 width = ui.termwidth()
569 569 chunks = patch.diff(repo, node1, node2, match, changes, diffopts,
570 570 prefix=prefix)
571 571 for chunk, label in patch.diffstatui(util.iterlines(chunks),
572 572 width=width,
573 573 git=diffopts.git):
574 574 write(chunk, label=label)
575 575 else:
576 576 for chunk, label in patch.diffui(repo, node1, node2, match,
577 577 changes, diffopts, prefix=prefix):
578 578 write(chunk, label=label)
579 579
580 580 if listsubrepos:
581 581 ctx1 = repo[node1]
582 582 ctx2 = repo[node2]
583 583 for subpath, sub in subrepo.itersubrepos(ctx1, ctx2):
584 584 if node2 is not None:
585 585 node2 = ctx2.substate[subpath][1]
586 586 submatch = matchmod.narrowmatcher(subpath, match)
587 587 sub.diff(diffopts, node2, submatch, changes=changes,
588 588 stat=stat, fp=fp, prefix=prefix)
589 589
590 590 class changeset_printer(object):
591 591 '''show changeset information when templating not requested.'''
592 592
593 593 def __init__(self, ui, repo, patch, diffopts, buffered):
594 594 self.ui = ui
595 595 self.repo = repo
596 596 self.buffered = buffered
597 597 self.patch = patch
598 598 self.diffopts = diffopts
599 599 self.header = {}
600 600 self.hunk = {}
601 601 self.lastheader = None
602 602 self.footer = None
603 603
604 604 def flush(self, rev):
605 605 if rev in self.header:
606 606 h = self.header[rev]
607 607 if h != self.lastheader:
608 608 self.lastheader = h
609 609 self.ui.write(h)
610 610 del self.header[rev]
611 611 if rev in self.hunk:
612 612 self.ui.write(self.hunk[rev])
613 613 del self.hunk[rev]
614 614 return 1
615 615 return 0
616 616
617 617 def close(self):
618 618 if self.footer:
619 619 self.ui.write(self.footer)
620 620
621 621 def show(self, ctx, copies=None, matchfn=None, **props):
622 622 if self.buffered:
623 623 self.ui.pushbuffer()
624 624 self._show(ctx, copies, matchfn, props)
625 625 self.hunk[ctx.rev()] = self.ui.popbuffer(labeled=True)
626 626 else:
627 627 self._show(ctx, copies, matchfn, props)
628 628
629 629 def _show(self, ctx, copies, matchfn, props):
630 630 '''show a single changeset or file revision'''
631 631 changenode = ctx.node()
632 632 rev = ctx.rev()
633 633
634 634 if self.ui.quiet:
635 635 self.ui.write("%d:%s\n" % (rev, short(changenode)),
636 636 label='log.node')
637 637 return
638 638
639 639 log = self.repo.changelog
640 640 date = util.datestr(ctx.date())
641 641
642 642 hexfunc = self.ui.debugflag and hex or short
643 643
644 644 parents = [(p, hexfunc(log.node(p)))
645 645 for p in self._meaningful_parentrevs(log, rev)]
646 646
647 647 self.ui.write(_("changeset: %d:%s\n") % (rev, hexfunc(changenode)),
648 648 label='log.changeset')
649 649
650 650 branch = ctx.branch()
651 651 # don't show the default branch name
652 652 if branch != 'default':
653 653 self.ui.write(_("branch: %s\n") % branch,
654 654 label='log.branch')
655 655 for bookmark in self.repo.nodebookmarks(changenode):
656 656 self.ui.write(_("bookmark: %s\n") % bookmark,
657 657 label='log.bookmark')
658 658 for tag in self.repo.nodetags(changenode):
659 659 self.ui.write(_("tag: %s\n") % tag,
660 660 label='log.tag')
661 661 for parent in parents:
662 662 self.ui.write(_("parent: %d:%s\n") % parent,
663 663 label='log.parent')
664 664
665 665 if self.ui.debugflag:
666 666 mnode = ctx.manifestnode()
667 667 self.ui.write(_("manifest: %d:%s\n") %
668 668 (self.repo.manifest.rev(mnode), hex(mnode)),
669 669 label='ui.debug log.manifest')
670 670 self.ui.write(_("user: %s\n") % ctx.user(),
671 671 label='log.user')
672 672 self.ui.write(_("date: %s\n") % date,
673 673 label='log.date')
674 674
675 675 if self.ui.debugflag:
676 676 files = self.repo.status(log.parents(changenode)[0], changenode)[:3]
677 677 for key, value in zip([_("files:"), _("files+:"), _("files-:")],
678 678 files):
679 679 if value:
680 680 self.ui.write("%-12s %s\n" % (key, " ".join(value)),
681 681 label='ui.debug log.files')
682 682 elif ctx.files() and self.ui.verbose:
683 683 self.ui.write(_("files: %s\n") % " ".join(ctx.files()),
684 684 label='ui.note log.files')
685 685 if copies and self.ui.verbose:
686 686 copies = ['%s (%s)' % c for c in copies]
687 687 self.ui.write(_("copies: %s\n") % ' '.join(copies),
688 688 label='ui.note log.copies')
689 689
690 690 extra = ctx.extra()
691 691 if extra and self.ui.debugflag:
692 692 for key, value in sorted(extra.items()):
693 693 self.ui.write(_("extra: %s=%s\n")
694 694 % (key, value.encode('string_escape')),
695 695 label='ui.debug log.extra')
696 696
697 697 description = ctx.description().strip()
698 698 if description:
699 699 if self.ui.verbose:
700 700 self.ui.write(_("description:\n"),
701 701 label='ui.note log.description')
702 702 self.ui.write(description,
703 703 label='ui.note log.description')
704 704 self.ui.write("\n\n")
705 705 else:
706 706 self.ui.write(_("summary: %s\n") %
707 707 description.splitlines()[0],
708 708 label='log.summary')
709 709 self.ui.write("\n")
710 710
711 711 self.showpatch(changenode, matchfn)
712 712
713 713 def showpatch(self, node, matchfn):
714 714 if not matchfn:
715 715 matchfn = self.patch
716 716 if matchfn:
717 717 stat = self.diffopts.get('stat')
718 718 diff = self.diffopts.get('patch')
719 719 diffopts = patch.diffopts(self.ui, self.diffopts)
720 720 prev = self.repo.changelog.parents(node)[0]
721 721 if stat:
722 722 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
723 723 match=matchfn, stat=True)
724 724 if diff:
725 725 if stat:
726 726 self.ui.write("\n")
727 727 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
728 728 match=matchfn, stat=False)
729 729 self.ui.write("\n")
730 730
731 731 def _meaningful_parentrevs(self, log, rev):
732 732 """Return list of meaningful (or all if debug) parentrevs for rev.
733 733
734 734 For merges (two non-nullrev revisions) both parents are meaningful.
735 735 Otherwise the first parent revision is considered meaningful if it
736 736 is not the preceding revision.
737 737 """
738 738 parents = log.parentrevs(rev)
739 739 if not self.ui.debugflag and parents[1] == nullrev:
740 740 if parents[0] >= rev - 1:
741 741 parents = []
742 742 else:
743 743 parents = [parents[0]]
744 744 return parents
745 745
746 746
747 747 class changeset_templater(changeset_printer):
748 748 '''format changeset information.'''
749 749
750 750 def __init__(self, ui, repo, patch, diffopts, mapfile, buffered):
751 751 changeset_printer.__init__(self, ui, repo, patch, diffopts, buffered)
752 752 formatnode = ui.debugflag and (lambda x: x) or (lambda x: x[:12])
753 753 defaulttempl = {
754 754 'parent': '{rev}:{node|formatnode} ',
755 755 'manifest': '{rev}:{node|formatnode}',
756 756 'file_copy': '{name} ({source})',
757 757 'extra': '{key}={value|stringescape}'
758 758 }
759 759 # filecopy is preserved for compatibility reasons
760 760 defaulttempl['filecopy'] = defaulttempl['file_copy']
761 761 self.t = templater.templater(mapfile, {'formatnode': formatnode},
762 762 cache=defaulttempl)
763 763 self.cache = {}
764 764
765 765 def use_template(self, t):
766 766 '''set template string to use'''
767 767 self.t.cache['changeset'] = t
768 768
769 769 def _meaningful_parentrevs(self, ctx):
770 770 """Return list of meaningful (or all if debug) parentrevs for rev.
771 771 """
772 772 parents = ctx.parents()
773 773 if len(parents) > 1:
774 774 return parents
775 775 if self.ui.debugflag:
776 776 return [parents[0], self.repo['null']]
777 777 if parents[0].rev() >= ctx.rev() - 1:
778 778 return []
779 779 return parents
780 780
781 781 def _show(self, ctx, copies, matchfn, props):
782 782 '''show a single changeset or file revision'''
783 783
784 784 showlist = templatekw.showlist
785 785
786 786 # showparents() behaviour depends on ui trace level which
787 787 # causes unexpected behaviours at templating level and makes
788 788 # it harder to extract it in a standalone function. Its
789 789 # behaviour cannot be changed so leave it here for now.
790 790 def showparents(**args):
791 791 ctx = args['ctx']
792 792 parents = [[('rev', p.rev()), ('node', p.hex())]
793 793 for p in self._meaningful_parentrevs(ctx)]
794 794 return showlist('parent', parents, **args)
795 795
796 796 props = props.copy()
797 797 props.update(templatekw.keywords)
798 798 props['parents'] = showparents
799 799 props['templ'] = self.t
800 800 props['ctx'] = ctx
801 801 props['repo'] = self.repo
802 802 props['revcache'] = {'copies': copies}
803 803 props['cache'] = self.cache
804 804
805 805 # find correct templates for current mode
806 806
807 807 tmplmodes = [
808 808 (True, None),
809 809 (self.ui.verbose, 'verbose'),
810 810 (self.ui.quiet, 'quiet'),
811 811 (self.ui.debugflag, 'debug'),
812 812 ]
813 813
814 814 types = {'header': '', 'footer':'', 'changeset': 'changeset'}
815 815 for mode, postfix in tmplmodes:
816 816 for type in types:
817 817 cur = postfix and ('%s_%s' % (type, postfix)) or type
818 818 if mode and cur in self.t:
819 819 types[type] = cur
820 820
821 821 try:
822 822
823 823 # write header
824 824 if types['header']:
825 825 h = templater.stringify(self.t(types['header'], **props))
826 826 if self.buffered:
827 827 self.header[ctx.rev()] = h
828 828 else:
829 829 if self.lastheader != h:
830 830 self.lastheader = h
831 831 self.ui.write(h)
832 832
833 833 # write changeset metadata, then patch if requested
834 834 key = types['changeset']
835 835 self.ui.write(templater.stringify(self.t(key, **props)))
836 836 self.showpatch(ctx.node(), matchfn)
837 837
838 838 if types['footer']:
839 839 if not self.footer:
840 840 self.footer = templater.stringify(self.t(types['footer'],
841 841 **props))
842 842
843 843 except KeyError, inst:
844 844 msg = _("%s: no key named '%s'")
845 845 raise util.Abort(msg % (self.t.mapfile, inst.args[0]))
846 846 except SyntaxError, inst:
847 847 raise util.Abort('%s: %s' % (self.t.mapfile, inst.args[0]))
848 848
849 849 def show_changeset(ui, repo, opts, buffered=False):
850 850 """show one changeset using template or regular display.
851 851
852 852 Display format will be the first non-empty hit of:
853 853 1. option 'template'
854 854 2. option 'style'
855 855 3. [ui] setting 'logtemplate'
856 856 4. [ui] setting 'style'
857 857 If all of these values are either the unset or the empty string,
858 858 regular display via changeset_printer() is done.
859 859 """
860 860 # options
861 861 patch = False
862 862 if opts.get('patch') or opts.get('stat'):
863 863 patch = scmutil.matchall(repo)
864 864
865 865 tmpl = opts.get('template')
866 866 style = None
867 867 if tmpl:
868 868 tmpl = templater.parsestring(tmpl, quoted=False)
869 869 else:
870 870 style = opts.get('style')
871 871
872 872 # ui settings
873 873 if not (tmpl or style):
874 874 tmpl = ui.config('ui', 'logtemplate')
875 875 if tmpl:
876 876 tmpl = templater.parsestring(tmpl)
877 877 else:
878 878 style = util.expandpath(ui.config('ui', 'style', ''))
879 879
880 880 if not (tmpl or style):
881 881 return changeset_printer(ui, repo, patch, opts, buffered)
882 882
883 883 mapfile = None
884 884 if style and not tmpl:
885 885 mapfile = style
886 886 if not os.path.split(mapfile)[0]:
887 887 mapname = (templater.templatepath('map-cmdline.' + mapfile)
888 888 or templater.templatepath(mapfile))
889 889 if mapname:
890 890 mapfile = mapname
891 891
892 892 try:
893 893 t = changeset_templater(ui, repo, patch, opts, mapfile, buffered)
894 894 except SyntaxError, inst:
895 895 raise util.Abort(inst.args[0])
896 896 if tmpl:
897 897 t.use_template(tmpl)
898 898 return t
899 899
900 900 def finddate(ui, repo, date):
901 901 """Find the tipmost changeset that matches the given date spec"""
902 902
903 903 df = util.matchdate(date)
904 904 m = scmutil.matchall(repo)
905 905 results = {}
906 906
907 907 def prep(ctx, fns):
908 908 d = ctx.date()
909 909 if df(d[0]):
910 910 results[ctx.rev()] = d
911 911
912 912 for ctx in walkchangerevs(repo, m, {'rev': None}, prep):
913 913 rev = ctx.rev()
914 914 if rev in results:
915 915 ui.status(_("Found revision %s from %s\n") %
916 916 (rev, util.datestr(results[rev])))
917 917 return str(rev)
918 918
919 919 raise util.Abort(_("revision matching date not found"))
920 920
921 921 def walkchangerevs(repo, match, opts, prepare):
922 922 '''Iterate over files and the revs in which they changed.
923 923
924 924 Callers most commonly need to iterate backwards over the history
925 925 in which they are interested. Doing so has awful (quadratic-looking)
926 926 performance, so we use iterators in a "windowed" way.
927 927
928 928 We walk a window of revisions in the desired order. Within the
929 929 window, we first walk forwards to gather data, then in the desired
930 930 order (usually backwards) to display it.
931 931
932 932 This function returns an iterator yielding contexts. Before
933 933 yielding each context, the iterator will first call the prepare
934 934 function on each context in the window in forward order.'''
935 935
936 936 def increasing_windows(start, end, windowsize=8, sizelimit=512):
937 937 if start < end:
938 938 while start < end:
939 939 yield start, min(windowsize, end - start)
940 940 start += windowsize
941 941 if windowsize < sizelimit:
942 942 windowsize *= 2
943 943 else:
944 944 while start > end:
945 945 yield start, min(windowsize, start - end - 1)
946 946 start -= windowsize
947 947 if windowsize < sizelimit:
948 948 windowsize *= 2
949 949
950 950 follow = opts.get('follow') or opts.get('follow_first')
951 951
952 952 if not len(repo):
953 953 return []
954 954
955 955 if follow:
956 956 defrange = '%s:0' % repo['.'].rev()
957 957 else:
958 958 defrange = '-1:0'
959 959 revs = scmutil.revrange(repo, opts['rev'] or [defrange])
960 960 if not revs:
961 961 return []
962 962 wanted = set()
963 963 slowpath = match.anypats() or (match.files() and opts.get('removed'))
964 964 fncache = {}
965 965 change = util.cachefunc(repo.changectx)
966 966
967 967 # First step is to fill wanted, the set of revisions that we want to yield.
968 968 # When it does not induce extra cost, we also fill fncache for revisions in
969 969 # wanted: a cache of filenames that were changed (ctx.files()) and that
970 970 # match the file filtering conditions.
971 971
972 972 if not slowpath and not match.files():
973 973 # No files, no patterns. Display all revs.
974 974 wanted = set(revs)
975 975 copies = []
976 976
977 977 if not slowpath:
978 978 # We only have to read through the filelog to find wanted revisions
979 979
980 980 minrev, maxrev = min(revs), max(revs)
981 981 def filerevgen(filelog, last):
982 982 """
983 983 Only files, no patterns. Check the history of each file.
984 984
985 985 Examines filelog entries within minrev, maxrev linkrev range
986 986 Returns an iterator yielding (linkrev, parentlinkrevs, copied)
987 987 tuples in backwards order
988 988 """
989 989 cl_count = len(repo)
990 990 revs = []
991 991 for j in xrange(0, last + 1):
992 992 linkrev = filelog.linkrev(j)
993 993 if linkrev < minrev:
994 994 continue
995 995 # only yield rev for which we have the changelog, it can
996 996 # happen while doing "hg log" during a pull or commit
997 997 if linkrev >= cl_count:
998 998 break
999 999
1000 1000 parentlinkrevs = []
1001 1001 for p in filelog.parentrevs(j):
1002 1002 if p != nullrev:
1003 1003 parentlinkrevs.append(filelog.linkrev(p))
1004 1004 n = filelog.node(j)
1005 1005 revs.append((linkrev, parentlinkrevs,
1006 1006 follow and filelog.renamed(n)))
1007 1007
1008 1008 return reversed(revs)
1009 1009 def iterfiles():
1010 1010 for filename in match.files():
1011 1011 yield filename, None
1012 1012 for filename_node in copies:
1013 1013 yield filename_node
1014 1014 for file_, node in iterfiles():
1015 1015 filelog = repo.file(file_)
1016 1016 if not len(filelog):
1017 1017 if node is None:
1018 1018 # A zero count may be a directory or deleted file, so
1019 1019 # try to find matching entries on the slow path.
1020 1020 if follow:
1021 1021 raise util.Abort(
1022 1022 _('cannot follow nonexistent file: "%s"') % file_)
1023 1023 slowpath = True
1024 1024 break
1025 1025 else:
1026 1026 continue
1027 1027
1028 1028 if node is None:
1029 1029 last = len(filelog) - 1
1030 1030 else:
1031 1031 last = filelog.rev(node)
1032 1032
1033 1033
1034 1034 # keep track of all ancestors of the file
1035 1035 ancestors = set([filelog.linkrev(last)])
1036 1036
1037 1037 # iterate from latest to oldest revision
1038 1038 for rev, flparentlinkrevs, copied in filerevgen(filelog, last):
1039 1039 if not follow:
1040 1040 if rev > maxrev:
1041 1041 continue
1042 1042 else:
1043 1043 # Note that last might not be the first interesting
1044 1044 # rev to us:
1045 1045 # if the file has been changed after maxrev, we'll
1046 1046 # have linkrev(last) > maxrev, and we still need
1047 1047 # to explore the file graph
1048 1048 if rev not in ancestors:
1049 1049 continue
1050 1050 # XXX insert 1327 fix here
1051 1051 if flparentlinkrevs:
1052 1052 ancestors.update(flparentlinkrevs)
1053 1053
1054 1054 fncache.setdefault(rev, []).append(file_)
1055 1055 wanted.add(rev)
1056 1056 if copied:
1057 1057 copies.append(copied)
1058 1058 if slowpath:
1059 1059 # We have to read the changelog to match filenames against
1060 1060 # changed files
1061 1061
1062 1062 if follow:
1063 1063 raise util.Abort(_('can only follow copies/renames for explicit '
1064 1064 'filenames'))
1065 1065
1066 1066 # The slow path checks files modified in every changeset.
1067 1067 for i in sorted(revs):
1068 1068 ctx = change(i)
1069 1069 matches = filter(match, ctx.files())
1070 1070 if matches:
1071 1071 fncache[i] = matches
1072 1072 wanted.add(i)
1073 1073
1074 1074 class followfilter(object):
1075 1075 def __init__(self, onlyfirst=False):
1076 1076 self.startrev = nullrev
1077 1077 self.roots = set()
1078 1078 self.onlyfirst = onlyfirst
1079 1079
1080 1080 def match(self, rev):
1081 1081 def realparents(rev):
1082 1082 if self.onlyfirst:
1083 1083 return repo.changelog.parentrevs(rev)[0:1]
1084 1084 else:
1085 1085 return filter(lambda x: x != nullrev,
1086 1086 repo.changelog.parentrevs(rev))
1087 1087
1088 1088 if self.startrev == nullrev:
1089 1089 self.startrev = rev
1090 1090 return True
1091 1091
1092 1092 if rev > self.startrev:
1093 1093 # forward: all descendants
1094 1094 if not self.roots:
1095 1095 self.roots.add(self.startrev)
1096 1096 for parent in realparents(rev):
1097 1097 if parent in self.roots:
1098 1098 self.roots.add(rev)
1099 1099 return True
1100 1100 else:
1101 1101 # backwards: all parents
1102 1102 if not self.roots:
1103 1103 self.roots.update(realparents(self.startrev))
1104 1104 if rev in self.roots:
1105 1105 self.roots.remove(rev)
1106 1106 self.roots.update(realparents(rev))
1107 1107 return True
1108 1108
1109 1109 return False
1110 1110
1111 1111 # it might be worthwhile to do this in the iterator if the rev range
1112 1112 # is descending and the prune args are all within that range
1113 1113 for rev in opts.get('prune', ()):
1114 1114 rev = repo.changelog.rev(repo.lookup(rev))
1115 1115 ff = followfilter()
1116 1116 stop = min(revs[0], revs[-1])
1117 1117 for x in xrange(rev, stop - 1, -1):
1118 1118 if ff.match(x):
1119 1119 wanted.discard(x)
1120 1120
1121 1121 # Now that wanted is correctly initialized, we can iterate over the
1122 1122 # revision range, yielding only revisions in wanted.
1123 1123 def iterate():
1124 1124 if follow and not match.files():
1125 1125 ff = followfilter(onlyfirst=opts.get('follow_first'))
1126 1126 def want(rev):
1127 1127 return ff.match(rev) and rev in wanted
1128 1128 else:
1129 1129 def want(rev):
1130 1130 return rev in wanted
1131 1131
1132 1132 for i, window in increasing_windows(0, len(revs)):
1133 1133 nrevs = [rev for rev in revs[i:i + window] if want(rev)]
1134 1134 for rev in sorted(nrevs):
1135 1135 fns = fncache.get(rev)
1136 1136 ctx = change(rev)
1137 1137 if not fns:
1138 1138 def fns_generator():
1139 1139 for f in ctx.files():
1140 1140 if match(f):
1141 1141 yield f
1142 1142 fns = fns_generator()
1143 1143 prepare(ctx, fns)
1144 1144 for rev in nrevs:
1145 1145 yield change(rev)
1146 1146 return iterate()
1147 1147
1148 1148 def add(ui, repo, match, dryrun, listsubrepos, prefix):
1149 1149 join = lambda f: os.path.join(prefix, f)
1150 1150 bad = []
1151 1151 oldbad = match.bad
1152 1152 match.bad = lambda x, y: bad.append(x) or oldbad(x, y)
1153 1153 names = []
1154 1154 wctx = repo[None]
1155 1155 cca = None
1156 1156 abort, warn = scmutil.checkportabilityalert(ui)
1157 1157 if abort or warn:
1158 1158 cca = scmutil.casecollisionauditor(ui, abort, wctx)
1159 1159 for f in repo.walk(match):
1160 1160 exact = match.exact(f)
1161 1161 if exact or f not in repo.dirstate:
1162 1162 if cca:
1163 1163 cca(f)
1164 1164 names.append(f)
1165 1165 if ui.verbose or not exact:
1166 1166 ui.status(_('adding %s\n') % match.rel(join(f)))
1167 1167
1168 if listsubrepos:
1169 for subpath in wctx.substate:
1170 sub = wctx.sub(subpath)
1171 try:
1172 submatch = matchmod.narrowmatcher(subpath, match)
1168 for subpath in wctx.substate:
1169 sub = wctx.sub(subpath)
1170 try:
1171 submatch = matchmod.narrowmatcher(subpath, match)
1172 if listsubrepos:
1173 1173 bad.extend(sub.add(ui, submatch, dryrun, prefix))
1174 except error.LookupError:
1175 ui.status(_("skipping missing subrepository: %s\n")
1176 % join(subpath))
1174 else:
1175 for f in sub.walk(submatch):
1176 if submatch.exact(f):
1177 bad.extend(sub.add(ui, submatch, dryrun, prefix))
1178 except error.LookupError:
1179 ui.status(_("skipping missing subrepository: %s\n")
1180 % join(subpath))
1177 1181
1178 1182 if not dryrun:
1179 1183 rejected = wctx.add(names, prefix)
1180 1184 bad.extend(f for f in rejected if f in match.files())
1181 1185 return bad
1182 1186
1183 1187 def duplicatecopies(repo, rev, p1, p2):
1184 1188 "Reproduce copies found in the source revision in the dirstate for grafts"
1185 1189 # Here we simulate the copies and renames in the source changeset
1186 1190 cop, diver = copies.copies(repo, repo[rev], repo[p1], repo[p2], True)
1187 1191 m1 = repo[rev].manifest()
1188 1192 m2 = repo[p1].manifest()
1189 1193 for k, v in cop.iteritems():
1190 1194 if k in m1:
1191 1195 if v in m1 or v in m2:
1192 1196 repo.dirstate.copy(v, k)
1193 1197 if v in m2 and v not in m1 and k in m2:
1194 1198 repo.dirstate.remove(v)
1195 1199
1196 1200 def commit(ui, repo, commitfunc, pats, opts):
1197 1201 '''commit the specified files or all outstanding changes'''
1198 1202 date = opts.get('date')
1199 1203 if date:
1200 1204 opts['date'] = util.parsedate(date)
1201 1205 message = logmessage(ui, opts)
1202 1206
1203 1207 # extract addremove carefully -- this function can be called from a command
1204 1208 # that doesn't support addremove
1205 1209 if opts.get('addremove'):
1206 1210 scmutil.addremove(repo, pats, opts)
1207 1211
1208 1212 return commitfunc(ui, repo, message,
1209 1213 scmutil.match(repo[None], pats, opts), opts)
1210 1214
1211 1215 def commiteditor(repo, ctx, subs):
1212 1216 if ctx.description():
1213 1217 return ctx.description()
1214 1218 return commitforceeditor(repo, ctx, subs)
1215 1219
1216 1220 def commitforceeditor(repo, ctx, subs):
1217 1221 edittext = []
1218 1222 modified, added, removed = ctx.modified(), ctx.added(), ctx.removed()
1219 1223 if ctx.description():
1220 1224 edittext.append(ctx.description())
1221 1225 edittext.append("")
1222 1226 edittext.append("") # Empty line between message and comments.
1223 1227 edittext.append(_("HG: Enter commit message."
1224 1228 " Lines beginning with 'HG:' are removed."))
1225 1229 edittext.append(_("HG: Leave message empty to abort commit."))
1226 1230 edittext.append("HG: --")
1227 1231 edittext.append(_("HG: user: %s") % ctx.user())
1228 1232 if ctx.p2():
1229 1233 edittext.append(_("HG: branch merge"))
1230 1234 if ctx.branch():
1231 1235 edittext.append(_("HG: branch '%s'") % ctx.branch())
1232 1236 edittext.extend([_("HG: subrepo %s") % s for s in subs])
1233 1237 edittext.extend([_("HG: added %s") % f for f in added])
1234 1238 edittext.extend([_("HG: changed %s") % f for f in modified])
1235 1239 edittext.extend([_("HG: removed %s") % f for f in removed])
1236 1240 if not added and not modified and not removed:
1237 1241 edittext.append(_("HG: no files changed"))
1238 1242 edittext.append("")
1239 1243 # run editor in the repository root
1240 1244 olddir = os.getcwd()
1241 1245 os.chdir(repo.root)
1242 1246 text = repo.ui.edit("\n".join(edittext), ctx.user())
1243 1247 text = re.sub("(?m)^HG:.*(\n|$)", "", text)
1244 1248 os.chdir(olddir)
1245 1249
1246 1250 if not text.strip():
1247 1251 raise util.Abort(_("empty commit message"))
1248 1252
1249 1253 return text
1250 1254
1251 1255 def command(table):
1252 1256 '''returns a function object bound to table which can be used as
1253 1257 a decorator for populating table as a command table'''
1254 1258
1255 1259 def cmd(name, options, synopsis=None):
1256 1260 def decorator(func):
1257 1261 if synopsis:
1258 1262 table[name] = func, options[:], synopsis
1259 1263 else:
1260 1264 table[name] = func, options[:]
1261 1265 return func
1262 1266 return decorator
1263 1267
1264 1268 return cmd
@@ -1,134 +1,136
1 1 Subrepositories let you nest external repositories or projects into a
2 2 parent Mercurial repository, and make commands operate on them as a
3 3 group.
4 4
5 5 Mercurial currently supports Mercurial, Git, and Subversion
6 6 subrepositories.
7 7
8 8 Subrepositories are made of three components:
9 9
10 10 1. Nested repository checkouts. They can appear anywhere in the
11 11 parent working directory.
12 12
13 13 2. Nested repository references. They are defined in ``.hgsub`` and
14 14 tell where the subrepository checkouts come from. Mercurial
15 15 subrepositories are referenced like:
16 16
17 17 path/to/nested = https://example.com/nested/repo/path
18 18
19 19 Git and Subversion subrepos are also supported:
20 20
21 21 path/to/nested = [git]git://example.com/nested/repo/path
22 22 path/to/nested = [svn]https://example.com/nested/trunk/path
23 23
24 24 where ``path/to/nested`` is the checkout location relatively to the
25 25 parent Mercurial root, and ``https://example.com/nested/repo/path``
26 26 is the source repository path. The source can also reference a
27 27 filesystem path.
28 28
29 29 Note that ``.hgsub`` does not exist by default in Mercurial
30 30 repositories, you have to create and add it to the parent
31 31 repository before using subrepositories.
32 32
33 33 3. Nested repository states. They are defined in ``.hgsubstate`` and
34 34 capture whatever information is required to restore the
35 35 subrepositories to the state they were committed in a parent
36 36 repository changeset. Mercurial automatically record the nested
37 37 repositories states when committing in the parent repository.
38 38
39 39 .. note::
40 40 The ``.hgsubstate`` file should not be edited manually.
41 41
42 42
43 43 Adding a Subrepository
44 44 ----------------------
45 45
46 46 If ``.hgsub`` does not exist, create it and add it to the parent
47 47 repository. Clone or checkout the external projects where you want it
48 48 to live in the parent repository. Edit ``.hgsub`` and add the
49 49 subrepository entry as described above. At this point, the
50 50 subrepository is tracked and the next commit will record its state in
51 51 ``.hgsubstate`` and bind it to the committed changeset.
52 52
53 53 Synchronizing a Subrepository
54 54 -----------------------------
55 55
56 56 Subrepos do not automatically track the latest changeset of their
57 57 sources. Instead, they are updated to the changeset that corresponds
58 58 with the changeset checked out in the top-level changeset. This is so
59 59 developers always get a consistent set of compatible code and
60 60 libraries when they update.
61 61
62 62 Thus, updating subrepos is a manual process. Simply check out target
63 63 subrepo at the desired revision, test in the top-level repo, then
64 64 commit in the parent repository to record the new combination.
65 65
66 66 Deleting a Subrepository
67 67 ------------------------
68 68
69 69 To remove a subrepository from the parent repository, delete its
70 70 reference from ``.hgsub``, then remove its files.
71 71
72 72 Interaction with Mercurial Commands
73 73 -----------------------------------
74 74
75 75 :add: add does not recurse in subrepos unless -S/--subrepos is
76 specified. Subversion subrepositories are currently silently
76 specified. However, if you specify the full path of a file in a
77 subrepo, it will be added even without -S/--subrepos specified.
78 Subversion subrepositories are currently silently
77 79 ignored.
78 80
79 81 :archive: archive does not recurse in subrepositories unless
80 82 -S/--subrepos is specified.
81 83
82 84 :commit: commit creates a consistent snapshot of the state of the
83 85 entire project and its subrepositories. It does this by first
84 86 attempting to commit all modified subrepositories, then recording
85 87 their state and finally committing it in the parent
86 88 repository. Mercurial can be made to abort if any subrepository
87 89 content is modified by setting "ui.commitsubrepos=no" in a
88 90 configuration file (see :hg:`help config`).
89 91
90 92 :diff: diff does not recurse in subrepos unless -S/--subrepos is
91 93 specified. Changes are displayed as usual, on the subrepositories
92 94 elements. Subversion subrepositories are currently silently
93 95 ignored.
94 96
95 97 :incoming: incoming does not recurse in subrepos unless -S/--subrepos
96 98 is specified. Subversion subrepositories are currently silently
97 99 ignored.
98 100
99 101 :outgoing: outgoing does not recurse in subrepos unless -S/--subrepos
100 102 is specified. Subversion subrepositories are currently silently
101 103 ignored.
102 104
103 105 :pull: pull is not recursive since it is not clear what to pull prior
104 106 to running :hg:`update`. Listing and retrieving all
105 107 subrepositories changes referenced by the parent repository pulled
106 108 changesets is expensive at best, impossible in the Subversion
107 109 case.
108 110
109 111 :push: Mercurial will automatically push all subrepositories first
110 112 when the parent repository is being pushed. This ensures new
111 113 subrepository changes are available when referenced by top-level
112 114 repositories.
113 115
114 116 :status: status does not recurse into subrepositories unless
115 117 -S/--subrepos is specified. Subrepository changes are displayed as
116 118 regular Mercurial changes on the subrepository
117 119 elements. Subversion subrepositories are currently silently
118 120 ignored.
119 121
120 122 :update: update restores the subrepos in the state they were
121 123 originally committed in target changeset. If the recorded
122 124 changeset is not available in the current subrepository, Mercurial
123 125 will pull it in first before updating. This means that updating
124 126 can require network access when using subrepositories.
125 127
126 128 Remapping Subrepositories Sources
127 129 ---------------------------------
128 130
129 131 A subrepository source location may change during a project life,
130 132 invalidating references stored in the parent repository history. To
131 133 fix this, rewriting rules can be defined in parent repository ``hgrc``
132 134 file or in Mercurial configuration. See the ``[subpaths]`` section in
133 135 hgrc(5) for more details.
134 136
@@ -1,1117 +1,1126
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 def walk(self, match):
357 '''
358 walk recursively through the directory tree, finding all files
359 matched by the match function
360 '''
361 pass
356 362
357 363 class hgsubrepo(abstractsubrepo):
358 364 def __init__(self, ctx, path, state):
359 365 self._path = path
360 366 self._state = state
361 367 r = ctx._repo
362 368 root = r.wjoin(path)
363 369 create = False
364 370 if not os.path.exists(os.path.join(root, '.hg')):
365 371 create = True
366 372 util.makedirs(root)
367 373 self._repo = hg.repository(r.ui, root, create=create)
368 374 self._initrepo(r, state[0], create)
369 375
370 376 def _initrepo(self, parentrepo, source, create):
371 377 self._repo._subparent = parentrepo
372 378 self._repo._subsource = source
373 379
374 380 if create:
375 381 fp = self._repo.opener("hgrc", "w", text=True)
376 382 fp.write('[paths]\n')
377 383
378 384 def addpathconfig(key, value):
379 385 if value:
380 386 fp.write('%s = %s\n' % (key, value))
381 387 self._repo.ui.setconfig('paths', key, value)
382 388
383 389 defpath = _abssource(self._repo, abort=False)
384 390 defpushpath = _abssource(self._repo, True, abort=False)
385 391 addpathconfig('default', defpath)
386 392 if defpath != defpushpath:
387 393 addpathconfig('default-push', defpushpath)
388 394 fp.close()
389 395
390 396 def add(self, ui, match, dryrun, prefix):
391 397 return cmdutil.add(ui, self._repo, match, dryrun, True,
392 398 os.path.join(prefix, self._path))
393 399
394 400 def status(self, rev2, **opts):
395 401 try:
396 402 rev1 = self._state[1]
397 403 ctx1 = self._repo[rev1]
398 404 ctx2 = self._repo[rev2]
399 405 return self._repo.status(ctx1, ctx2, **opts)
400 406 except error.RepoLookupError, inst:
401 407 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
402 408 % (inst, subrelpath(self)))
403 409 return [], [], [], [], [], [], []
404 410
405 411 def diff(self, diffopts, node2, match, prefix, **opts):
406 412 try:
407 413 node1 = node.bin(self._state[1])
408 414 # We currently expect node2 to come from substate and be
409 415 # in hex format
410 416 if node2 is not None:
411 417 node2 = node.bin(node2)
412 418 cmdutil.diffordiffstat(self._repo.ui, self._repo, diffopts,
413 419 node1, node2, match,
414 420 prefix=os.path.join(prefix, self._path),
415 421 listsubrepos=True, **opts)
416 422 except error.RepoLookupError, inst:
417 423 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
418 424 % (inst, subrelpath(self)))
419 425
420 426 def archive(self, ui, archiver, prefix):
421 427 self._get(self._state + ('hg',))
422 428 abstractsubrepo.archive(self, ui, archiver, prefix)
423 429
424 430 rev = self._state[1]
425 431 ctx = self._repo[rev]
426 432 for subpath in ctx.substate:
427 433 s = subrepo(ctx, subpath)
428 434 s.archive(ui, archiver, os.path.join(prefix, self._path))
429 435
430 436 def dirty(self, ignoreupdate=False):
431 437 r = self._state[1]
432 438 if r == '' and not ignoreupdate: # no state recorded
433 439 return True
434 440 w = self._repo[None]
435 441 if r != w.p1().hex() and not ignoreupdate:
436 442 # different version checked out
437 443 return True
438 444 return w.dirty() # working directory changed
439 445
440 446 def checknested(self, path):
441 447 return self._repo._checknested(self._repo.wjoin(path))
442 448
443 449 def commit(self, text, user, date):
444 450 # don't bother committing in the subrepo if it's only been
445 451 # updated
446 452 if not self.dirty(True):
447 453 return self._repo['.'].hex()
448 454 self._repo.ui.debug("committing subrepo %s\n" % subrelpath(self))
449 455 n = self._repo.commit(text, user, date)
450 456 if not n:
451 457 return self._repo['.'].hex() # different version checked out
452 458 return node.hex(n)
453 459
454 460 def remove(self):
455 461 # we can't fully delete the repository as it may contain
456 462 # local-only history
457 463 self._repo.ui.note(_('removing subrepo %s\n') % subrelpath(self))
458 464 hg.clean(self._repo, node.nullid, False)
459 465
460 466 def _get(self, state):
461 467 source, revision, kind = state
462 468 if revision not in self._repo:
463 469 self._repo._subsource = source
464 470 srcurl = _abssource(self._repo)
465 471 other = hg.peer(self._repo.ui, {}, srcurl)
466 472 if len(self._repo) == 0:
467 473 self._repo.ui.status(_('cloning subrepo %s from %s\n')
468 474 % (subrelpath(self), srcurl))
469 475 parentrepo = self._repo._subparent
470 476 shutil.rmtree(self._repo.path)
471 477 other, self._repo = hg.clone(self._repo._subparent.ui, {}, other,
472 478 self._repo.root, update=False)
473 479 self._initrepo(parentrepo, source, create=True)
474 480 else:
475 481 self._repo.ui.status(_('pulling subrepo %s from %s\n')
476 482 % (subrelpath(self), srcurl))
477 483 self._repo.pull(other)
478 484 bookmarks.updatefromremote(self._repo.ui, self._repo, other)
479 485
480 486 def get(self, state, overwrite=False):
481 487 self._get(state)
482 488 source, revision, kind = state
483 489 self._repo.ui.debug("getting subrepo %s\n" % self._path)
484 490 hg.clean(self._repo, revision, False)
485 491
486 492 def merge(self, state):
487 493 self._get(state)
488 494 cur = self._repo['.']
489 495 dst = self._repo[state[1]]
490 496 anc = dst.ancestor(cur)
491 497
492 498 def mergefunc():
493 499 if anc == cur:
494 500 self._repo.ui.debug("updating subrepo %s\n" % subrelpath(self))
495 501 hg.update(self._repo, state[1])
496 502 elif anc == dst:
497 503 self._repo.ui.debug("skipping subrepo %s\n" % subrelpath(self))
498 504 else:
499 505 self._repo.ui.debug("merging subrepo %s\n" % subrelpath(self))
500 506 hg.merge(self._repo, state[1], remind=False)
501 507
502 508 wctx = self._repo[None]
503 509 if self.dirty():
504 510 if anc != dst:
505 511 if _updateprompt(self._repo.ui, self, wctx.dirty(), cur, dst):
506 512 mergefunc()
507 513 else:
508 514 mergefunc()
509 515 else:
510 516 mergefunc()
511 517
512 518 def push(self, force):
513 519 # push subrepos depth-first for coherent ordering
514 520 c = self._repo['']
515 521 subs = c.substate # only repos that are committed
516 522 for s in sorted(subs):
517 523 if not c.sub(s).push(force):
518 524 return False
519 525
520 526 dsturl = _abssource(self._repo, True)
521 527 self._repo.ui.status(_('pushing subrepo %s to %s\n') %
522 528 (subrelpath(self), dsturl))
523 529 other = hg.peer(self._repo.ui, {}, dsturl)
524 530 return self._repo.push(other, force)
525 531
526 532 def outgoing(self, ui, dest, opts):
527 533 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
528 534
529 535 def incoming(self, ui, source, opts):
530 536 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
531 537
532 538 def files(self):
533 539 rev = self._state[1]
534 540 ctx = self._repo[rev]
535 541 return ctx.manifest()
536 542
537 543 def filedata(self, name):
538 544 rev = self._state[1]
539 545 return self._repo[rev][name].data()
540 546
541 547 def fileflags(self, name):
542 548 rev = self._state[1]
543 549 ctx = self._repo[rev]
544 550 return ctx.flags(name)
545 551
552 def walk(self, match):
553 ctx = self._repo[None]
554 return ctx.walk(match)
546 555
547 556 class svnsubrepo(abstractsubrepo):
548 557 def __init__(self, ctx, path, state):
549 558 self._path = path
550 559 self._state = state
551 560 self._ctx = ctx
552 561 self._ui = ctx._repo.ui
553 562 self._exe = util.findexe('svn')
554 563 if not self._exe:
555 564 raise util.Abort(_("'svn' executable not found for subrepo '%s'")
556 565 % self._path)
557 566
558 567 def _svncommand(self, commands, filename='', failok=False):
559 568 cmd = [self._exe]
560 569 extrakw = {}
561 570 if not self._ui.interactive():
562 571 # Making stdin be a pipe should prevent svn from behaving
563 572 # interactively even if we can't pass --non-interactive.
564 573 extrakw['stdin'] = subprocess.PIPE
565 574 # Starting in svn 1.5 --non-interactive is a global flag
566 575 # instead of being per-command, but we need to support 1.4 so
567 576 # we have to be intelligent about what commands take
568 577 # --non-interactive.
569 578 if commands[0] in ('update', 'checkout', 'commit'):
570 579 cmd.append('--non-interactive')
571 580 cmd.extend(commands)
572 581 if filename is not None:
573 582 path = os.path.join(self._ctx._repo.origroot, self._path, filename)
574 583 cmd.append(path)
575 584 env = dict(os.environ)
576 585 # Avoid localized output, preserve current locale for everything else.
577 586 env['LC_MESSAGES'] = 'C'
578 587 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
579 588 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
580 589 universal_newlines=True, env=env, **extrakw)
581 590 stdout, stderr = p.communicate()
582 591 stderr = stderr.strip()
583 592 if not failok:
584 593 if p.returncode:
585 594 raise util.Abort(stderr or 'exited with code %d' % p.returncode)
586 595 if stderr:
587 596 self._ui.warn(stderr + '\n')
588 597 return stdout, stderr
589 598
590 599 @propertycache
591 600 def _svnversion(self):
592 601 output, err = self._svncommand(['--version'], filename=None)
593 602 m = re.search(r'^svn,\s+version\s+(\d+)\.(\d+)', output)
594 603 if not m:
595 604 raise util.Abort(_('cannot retrieve svn tool version'))
596 605 return (int(m.group(1)), int(m.group(2)))
597 606
598 607 def _wcrevs(self):
599 608 # Get the working directory revision as well as the last
600 609 # commit revision so we can compare the subrepo state with
601 610 # both. We used to store the working directory one.
602 611 output, err = self._svncommand(['info', '--xml'])
603 612 doc = xml.dom.minidom.parseString(output)
604 613 entries = doc.getElementsByTagName('entry')
605 614 lastrev, rev = '0', '0'
606 615 if entries:
607 616 rev = str(entries[0].getAttribute('revision')) or '0'
608 617 commits = entries[0].getElementsByTagName('commit')
609 618 if commits:
610 619 lastrev = str(commits[0].getAttribute('revision')) or '0'
611 620 return (lastrev, rev)
612 621
613 622 def _wcrev(self):
614 623 return self._wcrevs()[0]
615 624
616 625 def _wcchanged(self):
617 626 """Return (changes, extchanges) where changes is True
618 627 if the working directory was changed, and extchanges is
619 628 True if any of these changes concern an external entry.
620 629 """
621 630 output, err = self._svncommand(['status', '--xml'])
622 631 externals, changes = [], []
623 632 doc = xml.dom.minidom.parseString(output)
624 633 for e in doc.getElementsByTagName('entry'):
625 634 s = e.getElementsByTagName('wc-status')
626 635 if not s:
627 636 continue
628 637 item = s[0].getAttribute('item')
629 638 props = s[0].getAttribute('props')
630 639 path = e.getAttribute('path')
631 640 if item == 'external':
632 641 externals.append(path)
633 642 if (item not in ('', 'normal', 'unversioned', 'external')
634 643 or props not in ('', 'none', 'normal')):
635 644 changes.append(path)
636 645 for path in changes:
637 646 for ext in externals:
638 647 if path == ext or path.startswith(ext + os.sep):
639 648 return True, True
640 649 return bool(changes), False
641 650
642 651 def dirty(self, ignoreupdate=False):
643 652 if not self._wcchanged()[0]:
644 653 if self._state[1] in self._wcrevs() or ignoreupdate:
645 654 return False
646 655 return True
647 656
648 657 def commit(self, text, user, date):
649 658 # user and date are out of our hands since svn is centralized
650 659 changed, extchanged = self._wcchanged()
651 660 if not changed:
652 661 return self._wcrev()
653 662 if extchanged:
654 663 # Do not try to commit externals
655 664 raise util.Abort(_('cannot commit svn externals'))
656 665 commitinfo, err = self._svncommand(['commit', '-m', text])
657 666 self._ui.status(commitinfo)
658 667 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
659 668 if not newrev:
660 669 raise util.Abort(commitinfo.splitlines()[-1])
661 670 newrev = newrev.groups()[0]
662 671 self._ui.status(self._svncommand(['update', '-r', newrev])[0])
663 672 return newrev
664 673
665 674 def remove(self):
666 675 if self.dirty():
667 676 self._ui.warn(_('not removing repo %s because '
668 677 'it has changes.\n' % self._path))
669 678 return
670 679 self._ui.note(_('removing subrepo %s\n') % self._path)
671 680
672 681 def onerror(function, path, excinfo):
673 682 if function is not os.remove:
674 683 raise
675 684 # read-only files cannot be unlinked under Windows
676 685 s = os.stat(path)
677 686 if (s.st_mode & stat.S_IWRITE) != 0:
678 687 raise
679 688 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
680 689 os.remove(path)
681 690
682 691 path = self._ctx._repo.wjoin(self._path)
683 692 shutil.rmtree(path, onerror=onerror)
684 693 try:
685 694 os.removedirs(os.path.dirname(path))
686 695 except OSError:
687 696 pass
688 697
689 698 def get(self, state, overwrite=False):
690 699 if overwrite:
691 700 self._svncommand(['revert', '--recursive'])
692 701 args = ['checkout']
693 702 if self._svnversion >= (1, 5):
694 703 args.append('--force')
695 704 # The revision must be specified at the end of the URL to properly
696 705 # update to a directory which has since been deleted and recreated.
697 706 args.append('%s@%s' % (state[0], state[1]))
698 707 status, err = self._svncommand(args, failok=True)
699 708 if not re.search('Checked out revision [0-9]+.', status):
700 709 if ('is already a working copy for a different URL' in err
701 710 and (self._wcchanged() == (False, False))):
702 711 # obstructed but clean working copy, so just blow it away.
703 712 self.remove()
704 713 self.get(state, overwrite=False)
705 714 return
706 715 raise util.Abort((status or err).splitlines()[-1])
707 716 self._ui.status(status)
708 717
709 718 def merge(self, state):
710 719 old = self._state[1]
711 720 new = state[1]
712 721 if new != self._wcrev():
713 722 dirty = old == self._wcrev() or self._wcchanged()[0]
714 723 if _updateprompt(self._ui, self, dirty, self._wcrev(), new):
715 724 self.get(state, False)
716 725
717 726 def push(self, force):
718 727 # push is a no-op for SVN
719 728 return True
720 729
721 730 def files(self):
722 731 output = self._svncommand(['list'])
723 732 # This works because svn forbids \n in filenames.
724 733 return output.splitlines()
725 734
726 735 def filedata(self, name):
727 736 return self._svncommand(['cat'], name)
728 737
729 738
730 739 class gitsubrepo(abstractsubrepo):
731 740 def __init__(self, ctx, path, state):
732 741 # TODO add git version check.
733 742 self._state = state
734 743 self._ctx = ctx
735 744 self._path = path
736 745 self._relpath = os.path.join(reporelpath(ctx._repo), path)
737 746 self._abspath = ctx._repo.wjoin(path)
738 747 self._subparent = ctx._repo
739 748 self._ui = ctx._repo.ui
740 749
741 750 def _gitcommand(self, commands, env=None, stream=False):
742 751 return self._gitdir(commands, env=env, stream=stream)[0]
743 752
744 753 def _gitdir(self, commands, env=None, stream=False):
745 754 return self._gitnodir(commands, env=env, stream=stream,
746 755 cwd=self._abspath)
747 756
748 757 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
749 758 """Calls the git command
750 759
751 760 The methods tries to call the git command. versions previor to 1.6.0
752 761 are not supported and very probably fail.
753 762 """
754 763 self._ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
755 764 # unless ui.quiet is set, print git's stderr,
756 765 # which is mostly progress and useful info
757 766 errpipe = None
758 767 if self._ui.quiet:
759 768 errpipe = open(os.devnull, 'w')
760 769 p = subprocess.Popen(['git'] + commands, bufsize=-1, cwd=cwd, env=env,
761 770 close_fds=util.closefds,
762 771 stdout=subprocess.PIPE, stderr=errpipe)
763 772 if stream:
764 773 return p.stdout, None
765 774
766 775 retdata = p.stdout.read().strip()
767 776 # wait for the child to exit to avoid race condition.
768 777 p.wait()
769 778
770 779 if p.returncode != 0 and p.returncode != 1:
771 780 # there are certain error codes that are ok
772 781 command = commands[0]
773 782 if command in ('cat-file', 'symbolic-ref'):
774 783 return retdata, p.returncode
775 784 # for all others, abort
776 785 raise util.Abort('git %s error %d in %s' %
777 786 (command, p.returncode, self._relpath))
778 787
779 788 return retdata, p.returncode
780 789
781 790 def _gitmissing(self):
782 791 return not os.path.exists(os.path.join(self._abspath, '.git'))
783 792
784 793 def _gitstate(self):
785 794 return self._gitcommand(['rev-parse', 'HEAD'])
786 795
787 796 def _gitcurrentbranch(self):
788 797 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
789 798 if err:
790 799 current = None
791 800 return current
792 801
793 802 def _gitremote(self, remote):
794 803 out = self._gitcommand(['remote', 'show', '-n', remote])
795 804 line = out.split('\n')[1]
796 805 i = line.index('URL: ') + len('URL: ')
797 806 return line[i:]
798 807
799 808 def _githavelocally(self, revision):
800 809 out, code = self._gitdir(['cat-file', '-e', revision])
801 810 return code == 0
802 811
803 812 def _gitisancestor(self, r1, r2):
804 813 base = self._gitcommand(['merge-base', r1, r2])
805 814 return base == r1
806 815
807 816 def _gitisbare(self):
808 817 return self._gitcommand(['config', '--bool', 'core.bare']) == 'true'
809 818
810 819 def _gitbranchmap(self):
811 820 '''returns 2 things:
812 821 a map from git branch to revision
813 822 a map from revision to branches'''
814 823 branch2rev = {}
815 824 rev2branch = {}
816 825
817 826 out = self._gitcommand(['for-each-ref', '--format',
818 827 '%(objectname) %(refname)'])
819 828 for line in out.split('\n'):
820 829 revision, ref = line.split(' ')
821 830 if (not ref.startswith('refs/heads/') and
822 831 not ref.startswith('refs/remotes/')):
823 832 continue
824 833 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
825 834 continue # ignore remote/HEAD redirects
826 835 branch2rev[ref] = revision
827 836 rev2branch.setdefault(revision, []).append(ref)
828 837 return branch2rev, rev2branch
829 838
830 839 def _gittracking(self, branches):
831 840 'return map of remote branch to local tracking branch'
832 841 # assumes no more than one local tracking branch for each remote
833 842 tracking = {}
834 843 for b in branches:
835 844 if b.startswith('refs/remotes/'):
836 845 continue
837 846 bname = b.split('/', 2)[2]
838 847 remote = self._gitcommand(['config', 'branch.%s.remote' % bname])
839 848 if remote:
840 849 ref = self._gitcommand(['config', 'branch.%s.merge' % bname])
841 850 tracking['refs/remotes/%s/%s' %
842 851 (remote, ref.split('/', 2)[2])] = b
843 852 return tracking
844 853
845 854 def _abssource(self, source):
846 855 if '://' not in source:
847 856 # recognize the scp syntax as an absolute source
848 857 colon = source.find(':')
849 858 if colon != -1 and '/' not in source[:colon]:
850 859 return source
851 860 self._subsource = source
852 861 return _abssource(self)
853 862
854 863 def _fetch(self, source, revision):
855 864 if self._gitmissing():
856 865 source = self._abssource(source)
857 866 self._ui.status(_('cloning subrepo %s from %s\n') %
858 867 (self._relpath, source))
859 868 self._gitnodir(['clone', source, self._abspath])
860 869 if self._githavelocally(revision):
861 870 return
862 871 self._ui.status(_('pulling subrepo %s from %s\n') %
863 872 (self._relpath, self._gitremote('origin')))
864 873 # try only origin: the originally cloned repo
865 874 self._gitcommand(['fetch'])
866 875 if not self._githavelocally(revision):
867 876 raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
868 877 (revision, self._relpath))
869 878
870 879 def dirty(self, ignoreupdate=False):
871 880 if self._gitmissing():
872 881 return self._state[1] != ''
873 882 if self._gitisbare():
874 883 return True
875 884 if not ignoreupdate and self._state[1] != self._gitstate():
876 885 # different version checked out
877 886 return True
878 887 # check for staged changes or modified files; ignore untracked files
879 888 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
880 889 return code == 1
881 890
882 891 def get(self, state, overwrite=False):
883 892 source, revision, kind = state
884 893 if not revision:
885 894 self.remove()
886 895 return
887 896 self._fetch(source, revision)
888 897 # if the repo was set to be bare, unbare it
889 898 if self._gitisbare():
890 899 self._gitcommand(['config', 'core.bare', 'false'])
891 900 if self._gitstate() == revision:
892 901 self._gitcommand(['reset', '--hard', 'HEAD'])
893 902 return
894 903 elif self._gitstate() == revision:
895 904 if overwrite:
896 905 # first reset the index to unmark new files for commit, because
897 906 # reset --hard will otherwise throw away files added for commit,
898 907 # not just unmark them.
899 908 self._gitcommand(['reset', 'HEAD'])
900 909 self._gitcommand(['reset', '--hard', 'HEAD'])
901 910 return
902 911 branch2rev, rev2branch = self._gitbranchmap()
903 912
904 913 def checkout(args):
905 914 cmd = ['checkout']
906 915 if overwrite:
907 916 # first reset the index to unmark new files for commit, because
908 917 # the -f option will otherwise throw away files added for
909 918 # commit, not just unmark them.
910 919 self._gitcommand(['reset', 'HEAD'])
911 920 cmd.append('-f')
912 921 self._gitcommand(cmd + args)
913 922
914 923 def rawcheckout():
915 924 # no branch to checkout, check it out with no branch
916 925 self._ui.warn(_('checking out detached HEAD in subrepo %s\n') %
917 926 self._relpath)
918 927 self._ui.warn(_('check out a git branch if you intend '
919 928 'to make changes\n'))
920 929 checkout(['-q', revision])
921 930
922 931 if revision not in rev2branch:
923 932 rawcheckout()
924 933 return
925 934 branches = rev2branch[revision]
926 935 firstlocalbranch = None
927 936 for b in branches:
928 937 if b == 'refs/heads/master':
929 938 # master trumps all other branches
930 939 checkout(['refs/heads/master'])
931 940 return
932 941 if not firstlocalbranch and not b.startswith('refs/remotes/'):
933 942 firstlocalbranch = b
934 943 if firstlocalbranch:
935 944 checkout([firstlocalbranch])
936 945 return
937 946
938 947 tracking = self._gittracking(branch2rev.keys())
939 948 # choose a remote branch already tracked if possible
940 949 remote = branches[0]
941 950 if remote not in tracking:
942 951 for b in branches:
943 952 if b in tracking:
944 953 remote = b
945 954 break
946 955
947 956 if remote not in tracking:
948 957 # create a new local tracking branch
949 958 local = remote.split('/', 2)[2]
950 959 checkout(['-b', local, remote])
951 960 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
952 961 # When updating to a tracked remote branch,
953 962 # if the local tracking branch is downstream of it,
954 963 # a normal `git pull` would have performed a "fast-forward merge"
955 964 # which is equivalent to updating the local branch to the remote.
956 965 # Since we are only looking at branching at update, we need to
957 966 # detect this situation and perform this action lazily.
958 967 if tracking[remote] != self._gitcurrentbranch():
959 968 checkout([tracking[remote]])
960 969 self._gitcommand(['merge', '--ff', remote])
961 970 else:
962 971 # a real merge would be required, just checkout the revision
963 972 rawcheckout()
964 973
965 974 def commit(self, text, user, date):
966 975 if self._gitmissing():
967 976 raise util.Abort(_("subrepo %s is missing") % self._relpath)
968 977 cmd = ['commit', '-a', '-m', text]
969 978 env = os.environ.copy()
970 979 if user:
971 980 cmd += ['--author', user]
972 981 if date:
973 982 # git's date parser silently ignores when seconds < 1e9
974 983 # convert to ISO8601
975 984 env['GIT_AUTHOR_DATE'] = util.datestr(date,
976 985 '%Y-%m-%dT%H:%M:%S %1%2')
977 986 self._gitcommand(cmd, env=env)
978 987 # make sure commit works otherwise HEAD might not exist under certain
979 988 # circumstances
980 989 return self._gitstate()
981 990
982 991 def merge(self, state):
983 992 source, revision, kind = state
984 993 self._fetch(source, revision)
985 994 base = self._gitcommand(['merge-base', revision, self._state[1]])
986 995 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
987 996
988 997 def mergefunc():
989 998 if base == revision:
990 999 self.get(state) # fast forward merge
991 1000 elif base != self._state[1]:
992 1001 self._gitcommand(['merge', '--no-commit', revision])
993 1002
994 1003 if self.dirty():
995 1004 if self._gitstate() != revision:
996 1005 dirty = self._gitstate() == self._state[1] or code != 0
997 1006 if _updateprompt(self._ui, self, dirty,
998 1007 self._state[1][:7], revision[:7]):
999 1008 mergefunc()
1000 1009 else:
1001 1010 mergefunc()
1002 1011
1003 1012 def push(self, force):
1004 1013 if not self._state[1]:
1005 1014 return True
1006 1015 if self._gitmissing():
1007 1016 raise util.Abort(_("subrepo %s is missing") % self._relpath)
1008 1017 # if a branch in origin contains the revision, nothing to do
1009 1018 branch2rev, rev2branch = self._gitbranchmap()
1010 1019 if self._state[1] in rev2branch:
1011 1020 for b in rev2branch[self._state[1]]:
1012 1021 if b.startswith('refs/remotes/origin/'):
1013 1022 return True
1014 1023 for b, revision in branch2rev.iteritems():
1015 1024 if b.startswith('refs/remotes/origin/'):
1016 1025 if self._gitisancestor(self._state[1], revision):
1017 1026 return True
1018 1027 # otherwise, try to push the currently checked out branch
1019 1028 cmd = ['push']
1020 1029 if force:
1021 1030 cmd.append('--force')
1022 1031
1023 1032 current = self._gitcurrentbranch()
1024 1033 if current:
1025 1034 # determine if the current branch is even useful
1026 1035 if not self._gitisancestor(self._state[1], current):
1027 1036 self._ui.warn(_('unrelated git branch checked out '
1028 1037 'in subrepo %s\n') % self._relpath)
1029 1038 return False
1030 1039 self._ui.status(_('pushing branch %s of subrepo %s\n') %
1031 1040 (current.split('/', 2)[2], self._relpath))
1032 1041 self._gitcommand(cmd + ['origin', current])
1033 1042 return True
1034 1043 else:
1035 1044 self._ui.warn(_('no branch checked out in subrepo %s\n'
1036 1045 'cannot push revision %s') %
1037 1046 (self._relpath, self._state[1]))
1038 1047 return False
1039 1048
1040 1049 def remove(self):
1041 1050 if self._gitmissing():
1042 1051 return
1043 1052 if self.dirty():
1044 1053 self._ui.warn(_('not removing repo %s because '
1045 1054 'it has changes.\n') % self._relpath)
1046 1055 return
1047 1056 # we can't fully delete the repository as it may contain
1048 1057 # local-only history
1049 1058 self._ui.note(_('removing subrepo %s\n') % self._relpath)
1050 1059 self._gitcommand(['config', 'core.bare', 'true'])
1051 1060 for f in os.listdir(self._abspath):
1052 1061 if f == '.git':
1053 1062 continue
1054 1063 path = os.path.join(self._abspath, f)
1055 1064 if os.path.isdir(path) and not os.path.islink(path):
1056 1065 shutil.rmtree(path)
1057 1066 else:
1058 1067 os.remove(path)
1059 1068
1060 1069 def archive(self, ui, archiver, prefix):
1061 1070 source, revision = self._state
1062 1071 if not revision:
1063 1072 return
1064 1073 self._fetch(source, revision)
1065 1074
1066 1075 # Parse git's native archive command.
1067 1076 # This should be much faster than manually traversing the trees
1068 1077 # and objects with many subprocess calls.
1069 1078 tarstream = self._gitcommand(['archive', revision], stream=True)
1070 1079 tar = tarfile.open(fileobj=tarstream, mode='r|')
1071 1080 relpath = subrelpath(self)
1072 1081 ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
1073 1082 for i, info in enumerate(tar):
1074 1083 if info.isdir():
1075 1084 continue
1076 1085 if info.issym():
1077 1086 data = info.linkname
1078 1087 else:
1079 1088 data = tar.extractfile(info).read()
1080 1089 archiver.addfile(os.path.join(prefix, self._path, info.name),
1081 1090 info.mode, info.issym(), data)
1082 1091 ui.progress(_('archiving (%s)') % relpath, i + 1,
1083 1092 unit=_('files'))
1084 1093 ui.progress(_('archiving (%s)') % relpath, None)
1085 1094
1086 1095
1087 1096 def status(self, rev2, **opts):
1088 1097 rev1 = self._state[1]
1089 1098 if self._gitmissing() or not rev1:
1090 1099 # if the repo is missing, return no results
1091 1100 return [], [], [], [], [], [], []
1092 1101 modified, added, removed = [], [], []
1093 1102 if rev2:
1094 1103 command = ['diff-tree', rev1, rev2]
1095 1104 else:
1096 1105 command = ['diff-index', rev1]
1097 1106 out = self._gitcommand(command)
1098 1107 for line in out.split('\n'):
1099 1108 tab = line.find('\t')
1100 1109 if tab == -1:
1101 1110 continue
1102 1111 status, f = line[tab - 1], line[tab + 1:]
1103 1112 if status == 'M':
1104 1113 modified.append(f)
1105 1114 elif status == 'A':
1106 1115 added.append(f)
1107 1116 elif status == 'D':
1108 1117 removed.append(f)
1109 1118
1110 1119 deleted = unknown = ignored = clean = []
1111 1120 return modified, added, removed, deleted, unknown, ignored, clean
1112 1121
1113 1122 types = {
1114 1123 'hg': hgsubrepo,
1115 1124 'svn': svnsubrepo,
1116 1125 'git': gitsubrepo,
1117 1126 }
@@ -1,1008 +1,1007
1 1 Let commit recurse into subrepos by default to match pre-2.0 behavior:
2 2
3 3 $ echo "[ui]" >> $HGRCPATH
4 4 $ echo "commitsubrepos = Yes" >> $HGRCPATH
5 5
6 6 $ rm -rf sub
7 7 $ mkdir sub
8 8 $ cd sub
9 9 $ hg init t
10 10 $ cd t
11 11
12 12 first revision, no sub
13 13
14 14 $ echo a > a
15 15 $ hg ci -Am0
16 16 adding a
17 17
18 18 add first sub
19 19
20 20 $ echo s = s > .hgsub
21 21 $ hg add .hgsub
22 22 $ hg init s
23 23 $ echo a > s/a
24 24
25 25 Issue2232: committing a subrepo without .hgsub
26 26
27 27 $ hg ci -mbad s
28 28 abort: can't commit subrepos without .hgsub
29 29 [255]
30 30
31 31 $ hg -R s ci -Ams0
32 32 adding a
33 33 $ hg sum
34 34 parent: 0:f7b1eb17ad24 tip
35 35 0
36 36 branch: default
37 37 commit: 1 added, 1 subrepos
38 38 update: (current)
39 39 $ hg ci -m1
40 40 committing subrepository s
41 41
42 42 Revert can't (yet) revert subrepos:
43 43
44 44 $ echo b > s/a
45 45 $ hg revert s
46 46 s: reverting subrepos is unsupported
47 47
48 48 Revert currently ignores subrepos by default
49 49
50 50 $ hg revert -a
51 51 $ hg revert -R s -a -C
52 52 reverting s/a
53 53
54 54 Issue2022: update -C
55 55
56 56 $ echo b > s/a
57 57 $ hg sum
58 58 parent: 1:7cf8cfea66e4 tip
59 59 1
60 60 branch: default
61 61 commit: 1 subrepos
62 62 update: (current)
63 63 $ hg co -C 1
64 64 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
65 65 $ hg sum
66 66 parent: 1:7cf8cfea66e4 tip
67 67 1
68 68 branch: default
69 69 commit: (clean)
70 70 update: (current)
71 71
72 72 commands that require a clean repo should respect subrepos
73 73
74 74 $ echo b >> s/a
75 75 $ hg backout tip
76 76 abort: uncommitted changes in subrepo s
77 77 [255]
78 78 $ hg revert -C -R s s/a
79 79
80 80 add sub sub
81 81
82 82 $ echo ss = ss > s/.hgsub
83 83 $ hg init s/ss
84 84 $ echo a > s/ss/a
85 85 $ hg -R s add s/.hgsub
86 86 $ hg -R s/ss add s/ss/a
87 87 $ hg sum
88 88 parent: 1:7cf8cfea66e4 tip
89 89 1
90 90 branch: default
91 91 commit: 1 subrepos
92 92 update: (current)
93 93 $ hg ci -m2
94 94 committing subrepository s
95 95 committing subrepository s/ss
96 96 $ hg sum
97 97 parent: 2:df30734270ae tip
98 98 2
99 99 branch: default
100 100 commit: (clean)
101 101 update: (current)
102 102
103 103 bump sub rev (and check it is ignored by ui.commitsubrepos)
104 104
105 105 $ echo b > s/a
106 106 $ hg -R s ci -ms1
107 107 $ hg --config ui.commitsubrepos=no ci -m3
108 108 committing subrepository s
109 109
110 110 leave sub dirty (and check ui.commitsubrepos=no aborts the commit)
111 111
112 112 $ echo c > s/a
113 113 $ hg --config ui.commitsubrepos=no ci -m4
114 114 abort: uncommitted changes in subrepo s
115 115 (use --subrepos for recursive commit)
116 116 [255]
117 117 $ hg ci -m4
118 118 committing subrepository s
119 119 $ hg tip -R s
120 120 changeset: 3:1c833a7a9e3a
121 121 tag: tip
122 122 user: test
123 123 date: Thu Jan 01 00:00:00 1970 +0000
124 124 summary: 4
125 125
126 126
127 127 check caching
128 128
129 129 $ hg co 0
130 130 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
131 131 $ hg debugsub
132 132
133 133 restore
134 134
135 135 $ hg co
136 136 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
137 137 $ hg debugsub
138 138 path s
139 139 source s
140 140 revision 1c833a7a9e3a4445c711aaf0f012379cd0d4034e
141 141
142 142 new branch for merge tests
143 143
144 144 $ hg co 1
145 145 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
146 146 $ echo t = t >> .hgsub
147 147 $ hg init t
148 148 $ echo t > t/t
149 149 $ hg -R t add t
150 150 adding t/t
151 151
152 152 5
153 153
154 154 $ hg ci -m5 # add sub
155 155 committing subrepository t
156 156 created new head
157 157 $ echo t2 > t/t
158 158
159 159 6
160 160
161 161 $ hg st -R s
162 162 $ hg ci -m6 # change sub
163 163 committing subrepository t
164 164 $ hg debugsub
165 165 path s
166 166 source s
167 167 revision e4ece1bf43360ddc8f6a96432201a37b7cd27ae4
168 168 path t
169 169 source t
170 170 revision 6747d179aa9a688023c4b0cad32e4c92bb7f34ad
171 171 $ echo t3 > t/t
172 172
173 173 7
174 174
175 175 $ hg ci -m7 # change sub again for conflict test
176 176 committing subrepository t
177 177 $ hg rm .hgsub
178 178
179 179 8
180 180
181 181 $ hg ci -m8 # remove sub
182 182
183 183 merge tests
184 184
185 185 $ hg co -C 3
186 186 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
187 187 $ hg merge 5 # test adding
188 188 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
189 189 (branch merge, don't forget to commit)
190 190 $ hg debugsub
191 191 path s
192 192 source s
193 193 revision fc627a69481fcbe5f1135069e8a3881c023e4cf5
194 194 path t
195 195 source t
196 196 revision 60ca1237c19474e7a3978b0dc1ca4e6f36d51382
197 197 $ hg ci -m9
198 198 created new head
199 199 $ hg merge 6 --debug # test change
200 200 searching for copies back to rev 2
201 201 resolving manifests
202 202 overwrite None partial False
203 203 ancestor 1f14a2e2d3ec local f0d2028bf86d+ remote 1831e14459c4
204 204 .hgsubstate: versions differ -> m
205 205 updating: .hgsubstate 1/1 files (100.00%)
206 206 subrepo merge f0d2028bf86d+ 1831e14459c4 1f14a2e2d3ec
207 207 subrepo t: other changed, get t:6747d179aa9a688023c4b0cad32e4c92bb7f34ad:hg
208 208 getting subrepo t
209 209 resolving manifests
210 210 overwrite True partial False
211 211 ancestor 60ca1237c194+ local 60ca1237c194+ remote 6747d179aa9a
212 212 t: remote is newer -> g
213 213 updating: t 1/1 files (100.00%)
214 214 getting t
215 215 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
216 216 (branch merge, don't forget to commit)
217 217 $ hg debugsub
218 218 path s
219 219 source s
220 220 revision fc627a69481fcbe5f1135069e8a3881c023e4cf5
221 221 path t
222 222 source t
223 223 revision 6747d179aa9a688023c4b0cad32e4c92bb7f34ad
224 224 $ echo conflict > t/t
225 225 $ hg ci -m10
226 226 committing subrepository t
227 227 $ HGMERGE=internal:merge hg merge --debug 7 # test conflict
228 228 searching for copies back to rev 2
229 229 resolving manifests
230 230 overwrite None partial False
231 231 ancestor 1831e14459c4 local e45c8b14af55+ remote f94576341bcf
232 232 .hgsubstate: versions differ -> m
233 233 updating: .hgsubstate 1/1 files (100.00%)
234 234 subrepo merge e45c8b14af55+ f94576341bcf 1831e14459c4
235 235 subrepo t: both sides changed, merge with t:7af322bc1198a32402fe903e0b7ebcfc5c9bf8f4:hg
236 236 merging subrepo t
237 237 searching for copies back to rev 2
238 238 resolving manifests
239 239 overwrite None partial False
240 240 ancestor 6747d179aa9a local 20a0db6fbf6c+ remote 7af322bc1198
241 241 t: versions differ -> m
242 242 preserving t for resolve of t
243 243 updating: t 1/1 files (100.00%)
244 244 picked tool 'internal:merge' for t (binary False symlink False)
245 245 merging t
246 246 my t@20a0db6fbf6c+ other t@7af322bc1198 ancestor t@6747d179aa9a
247 247 warning: conflicts during merge.
248 248 merging t failed!
249 249 0 files updated, 0 files merged, 0 files removed, 1 files unresolved
250 250 use 'hg resolve' to retry unresolved file merges or 'hg update -C .' to abandon
251 251 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
252 252 (branch merge, don't forget to commit)
253 253
254 254 should conflict
255 255
256 256 $ cat t/t
257 257 <<<<<<< local
258 258 conflict
259 259 =======
260 260 t3
261 261 >>>>>>> other
262 262
263 263 clone
264 264
265 265 $ cd ..
266 266 $ hg clone t tc
267 267 updating to branch default
268 268 cloning subrepo s from $TESTTMP/sub/t/s
269 269 cloning subrepo s/ss from $TESTTMP/sub/t/s/ss
270 270 cloning subrepo t from $TESTTMP/sub/t/t
271 271 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
272 272 $ cd tc
273 273 $ hg debugsub
274 274 path s
275 275 source s
276 276 revision fc627a69481fcbe5f1135069e8a3881c023e4cf5
277 277 path t
278 278 source t
279 279 revision 20a0db6fbf6c3d2836e6519a642ae929bfc67c0e
280 280
281 281 push
282 282
283 283 $ echo bah > t/t
284 284 $ hg ci -m11
285 285 committing subrepository t
286 286 $ hg push
287 287 pushing to $TESTTMP/sub/t
288 288 pushing subrepo s/ss to $TESTTMP/sub/t/s/ss
289 289 searching for changes
290 290 no changes found
291 291 pushing subrepo s to $TESTTMP/sub/t/s
292 292 searching for changes
293 293 no changes found
294 294 pushing subrepo t to $TESTTMP/sub/t/t
295 295 searching for changes
296 296 adding changesets
297 297 adding manifests
298 298 adding file changes
299 299 added 1 changesets with 1 changes to 1 files
300 300 searching for changes
301 301 adding changesets
302 302 adding manifests
303 303 adding file changes
304 304 added 1 changesets with 1 changes to 1 files
305 305
306 306 push -f
307 307
308 308 $ echo bah > s/a
309 309 $ hg ci -m12
310 310 committing subrepository s
311 311 $ hg push
312 312 pushing to $TESTTMP/sub/t
313 313 pushing subrepo s/ss to $TESTTMP/sub/t/s/ss
314 314 searching for changes
315 315 no changes found
316 316 pushing subrepo s to $TESTTMP/sub/t/s
317 317 searching for changes
318 318 abort: push creates new remote head 12a213df6fa9!
319 319 (did you forget to merge? use push -f to force)
320 320 [255]
321 321 $ hg push -f
322 322 pushing to $TESTTMP/sub/t
323 323 pushing subrepo s/ss to $TESTTMP/sub/t/s/ss
324 324 searching for changes
325 325 no changes found
326 326 pushing subrepo s to $TESTTMP/sub/t/s
327 327 searching for changes
328 328 adding changesets
329 329 adding manifests
330 330 adding file changes
331 331 added 1 changesets with 1 changes to 1 files (+1 heads)
332 332 pushing subrepo t to $TESTTMP/sub/t/t
333 333 searching for changes
334 334 no changes found
335 335 searching for changes
336 336 adding changesets
337 337 adding manifests
338 338 adding file changes
339 339 added 1 changesets with 1 changes to 1 files
340 340
341 341 update
342 342
343 343 $ cd ../t
344 344 $ hg up -C # discard our earlier merge
345 345 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
346 346 $ echo blah > t/t
347 347 $ hg ci -m13
348 348 committing subrepository t
349 349
350 350 pull
351 351
352 352 $ cd ../tc
353 353 $ hg pull
354 354 pulling from $TESTTMP/sub/t
355 355 searching for changes
356 356 adding changesets
357 357 adding manifests
358 358 adding file changes
359 359 added 1 changesets with 1 changes to 1 files
360 360 (run 'hg update' to get a working copy)
361 361
362 362 should pull t
363 363
364 364 $ hg up
365 365 pulling subrepo t from $TESTTMP/sub/t/t
366 366 searching for changes
367 367 adding changesets
368 368 adding manifests
369 369 adding file changes
370 370 added 1 changesets with 1 changes to 1 files
371 371 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
372 372 $ cat t/t
373 373 blah
374 374
375 375 bogus subrepo path aborts
376 376
377 377 $ echo 'bogus=[boguspath' >> .hgsub
378 378 $ hg ci -m 'bogus subrepo path'
379 379 abort: missing ] in subrepo source
380 380 [255]
381 381
382 382 Issue1986: merge aborts when trying to merge a subrepo that
383 383 shouldn't need merging
384 384
385 385 # subrepo layout
386 386 #
387 387 # o 5 br
388 388 # /|
389 389 # o | 4 default
390 390 # | |
391 391 # | o 3 br
392 392 # |/|
393 393 # o | 2 default
394 394 # | |
395 395 # | o 1 br
396 396 # |/
397 397 # o 0 default
398 398
399 399 $ cd ..
400 400 $ rm -rf sub
401 401 $ hg init main
402 402 $ cd main
403 403 $ hg init s
404 404 $ cd s
405 405 $ echo a > a
406 406 $ hg ci -Am1
407 407 adding a
408 408 $ hg branch br
409 409 marked working directory as branch br
410 410 $ echo a >> a
411 411 $ hg ci -m1
412 412 $ hg up default
413 413 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
414 414 $ echo b > b
415 415 $ hg ci -Am1
416 416 adding b
417 417 $ hg up br
418 418 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
419 419 $ hg merge tip
420 420 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
421 421 (branch merge, don't forget to commit)
422 422 $ hg ci -m1
423 423 $ hg up 2
424 424 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
425 425 $ echo c > c
426 426 $ hg ci -Am1
427 427 adding c
428 428 $ hg up 3
429 429 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
430 430 $ hg merge 4
431 431 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
432 432 (branch merge, don't forget to commit)
433 433 $ hg ci -m1
434 434
435 435 # main repo layout:
436 436 #
437 437 # * <-- try to merge default into br again
438 438 # .`|
439 439 # . o 5 br --> substate = 5
440 440 # . |
441 441 # o | 4 default --> substate = 4
442 442 # | |
443 443 # | o 3 br --> substate = 2
444 444 # |/|
445 445 # o | 2 default --> substate = 2
446 446 # | |
447 447 # | o 1 br --> substate = 3
448 448 # |/
449 449 # o 0 default --> substate = 2
450 450
451 451 $ cd ..
452 452 $ echo 's = s' > .hgsub
453 453 $ hg -R s up 2
454 454 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
455 455 $ hg ci -Am1
456 456 adding .hgsub
457 457 committing subrepository s
458 458 $ hg branch br
459 459 marked working directory as branch br
460 460 $ echo b > b
461 461 $ hg -R s up 3
462 462 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
463 463 $ hg ci -Am1
464 464 adding b
465 465 committing subrepository s
466 466 $ hg up default
467 467 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
468 468 $ echo c > c
469 469 $ hg ci -Am1
470 470 adding c
471 471 $ hg up 1
472 472 2 files updated, 0 files merged, 1 files removed, 0 files unresolved
473 473 $ hg merge 2
474 474 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
475 475 (branch merge, don't forget to commit)
476 476 $ hg ci -m1
477 477 $ hg up 2
478 478 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
479 479 $ hg -R s up 4
480 480 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
481 481 $ echo d > d
482 482 $ hg ci -Am1
483 483 adding d
484 484 committing subrepository s
485 485 $ hg up 3
486 486 2 files updated, 0 files merged, 1 files removed, 0 files unresolved
487 487 $ hg -R s up 5
488 488 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
489 489 $ echo e > e
490 490 $ hg ci -Am1
491 491 adding e
492 492 committing subrepository s
493 493
494 494 $ hg up 5
495 495 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
496 496 $ hg merge 4 # try to merge default into br again
497 497 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
498 498 (branch merge, don't forget to commit)
499 499 $ cd ..
500 500
501 501 test subrepo delete from .hgsubstate
502 502
503 503 $ hg init testdelete
504 504 $ mkdir testdelete/nested testdelete/nested2
505 505 $ hg init testdelete/nested
506 506 $ hg init testdelete/nested2
507 507 $ echo test > testdelete/nested/foo
508 508 $ echo test > testdelete/nested2/foo
509 509 $ hg -R testdelete/nested add
510 510 adding testdelete/nested/foo
511 511 $ hg -R testdelete/nested2 add
512 512 adding testdelete/nested2/foo
513 513 $ hg -R testdelete/nested ci -m test
514 514 $ hg -R testdelete/nested2 ci -m test
515 515 $ echo nested = nested > testdelete/.hgsub
516 516 $ echo nested2 = nested2 >> testdelete/.hgsub
517 517 $ hg -R testdelete add
518 518 adding testdelete/.hgsub
519 519 $ hg -R testdelete ci -m "nested 1 & 2 added"
520 520 committing subrepository nested
521 521 committing subrepository nested2
522 522 $ echo nested = nested > testdelete/.hgsub
523 523 $ hg -R testdelete ci -m "nested 2 deleted"
524 524 $ cat testdelete/.hgsubstate
525 525 bdf5c9a3103743d900b12ae0db3ffdcfd7b0d878 nested
526 526 $ hg -R testdelete remove testdelete/.hgsub
527 527 $ hg -R testdelete ci -m ".hgsub deleted"
528 528 $ cat testdelete/.hgsubstate
529 529 bdf5c9a3103743d900b12ae0db3ffdcfd7b0d878 nested
530 530
531 531 test repository cloning
532 532
533 533 $ mkdir mercurial mercurial2
534 534 $ hg init nested_absolute
535 535 $ echo test > nested_absolute/foo
536 536 $ hg -R nested_absolute add
537 537 adding nested_absolute/foo
538 538 $ hg -R nested_absolute ci -mtest
539 539 $ cd mercurial
540 540 $ hg init nested_relative
541 541 $ echo test2 > nested_relative/foo2
542 542 $ hg -R nested_relative add
543 543 adding nested_relative/foo2
544 544 $ hg -R nested_relative ci -mtest2
545 545 $ hg init main
546 546 $ echo "nested_relative = ../nested_relative" > main/.hgsub
547 547 $ echo "nested_absolute = `pwd`/nested_absolute" >> main/.hgsub
548 548 $ hg -R main add
549 549 adding main/.hgsub
550 550 $ hg -R main ci -m "add subrepos"
551 551 committing subrepository nested_absolute
552 552 committing subrepository nested_relative
553 553 $ cd ..
554 554 $ hg clone mercurial/main mercurial2/main
555 555 updating to branch default
556 556 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
557 557 $ cat mercurial2/main/nested_absolute/.hg/hgrc \
558 558 > mercurial2/main/nested_relative/.hg/hgrc
559 559 [paths]
560 560 default = $TESTTMP/sub/mercurial/nested_absolute
561 561 [paths]
562 562 default = $TESTTMP/sub/mercurial/nested_relative
563 563 $ rm -rf mercurial mercurial2
564 564
565 565 Issue1977: multirepo push should fail if subrepo push fails
566 566
567 567 $ hg init repo
568 568 $ hg init repo/s
569 569 $ echo a > repo/s/a
570 570 $ hg -R repo/s ci -Am0
571 571 adding a
572 572 $ echo s = s > repo/.hgsub
573 573 $ hg -R repo ci -Am1
574 574 adding .hgsub
575 575 committing subrepository s
576 576 $ hg clone repo repo2
577 577 updating to branch default
578 578 cloning subrepo s from $TESTTMP/sub/repo/s
579 579 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
580 580 $ hg -q -R repo2 pull -u
581 581 $ echo 1 > repo2/s/a
582 582 $ hg -R repo2/s ci -m2
583 583 $ hg -q -R repo2/s push
584 584 $ hg -R repo2/s up -C 0
585 585 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
586 586 $ echo 2 > repo2/s/a
587 587 $ hg -R repo2/s ci -m3
588 588 created new head
589 589 $ hg -R repo2 ci -m3
590 590 committing subrepository s
591 591 $ hg -q -R repo2 push
592 592 abort: push creates new remote head 9d66565e64e1!
593 593 (did you forget to merge? use push -f to force)
594 594 [255]
595 595 $ hg -R repo update
596 596 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
597 597 $ rm -rf repo2 repo
598 598
599 599
600 600 Issue1852 subrepos with relative paths always push/pull relative to default
601 601
602 602 Prepare a repo with subrepo
603 603
604 604 $ hg init issue1852a
605 605 $ cd issue1852a
606 606 $ hg init sub/repo
607 607 $ echo test > sub/repo/foo
608 608 $ hg -R sub/repo add sub/repo/foo
609 609 $ echo sub/repo = sub/repo > .hgsub
610 610 $ hg add .hgsub
611 611 $ hg ci -mtest
612 612 committing subrepository sub/repo
613 613 $ echo test >> sub/repo/foo
614 614 $ hg ci -mtest
615 615 committing subrepository sub/repo
616 616 $ cd ..
617 617
618 618 Create repo without default path, pull top repo, and see what happens on update
619 619
620 620 $ hg init issue1852b
621 621 $ hg -R issue1852b pull issue1852a
622 622 pulling from issue1852a
623 623 requesting all changes
624 624 adding changesets
625 625 adding manifests
626 626 adding file changes
627 627 added 2 changesets with 3 changes to 2 files
628 628 (run 'hg update' to get a working copy)
629 629 $ hg -R issue1852b update
630 630 abort: default path for subrepository sub/repo not found
631 631 [255]
632 632
633 633 Pull -u now doesn't help
634 634
635 635 $ hg -R issue1852b pull -u issue1852a
636 636 pulling from issue1852a
637 637 searching for changes
638 638 no changes found
639 639
640 640 Try the same, but with pull -u
641 641
642 642 $ hg init issue1852c
643 643 $ hg -R issue1852c pull -r0 -u issue1852a
644 644 pulling from issue1852a
645 645 adding changesets
646 646 adding manifests
647 647 adding file changes
648 648 added 1 changesets with 2 changes to 2 files
649 649 cloning subrepo sub/repo from issue1852a/sub/repo
650 650 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
651 651
652 652 Try to push from the other side
653 653
654 654 $ hg -R issue1852a push `pwd`/issue1852c
655 655 pushing to $TESTTMP/sub/issue1852c
656 656 pushing subrepo sub/repo to $TESTTMP/sub/issue1852c/sub/repo
657 657 searching for changes
658 658 no changes found
659 659 searching for changes
660 660 adding changesets
661 661 adding manifests
662 662 adding file changes
663 663 added 1 changesets with 1 changes to 1 files
664 664
665 665 Incoming and outgoing should not use the default path:
666 666
667 667 $ hg clone -q issue1852a issue1852d
668 668 $ hg -R issue1852d outgoing --subrepos issue1852c
669 669 comparing with issue1852c
670 670 searching for changes
671 671 no changes found
672 672 comparing with issue1852c/sub/repo
673 673 searching for changes
674 674 no changes found
675 675 [1]
676 676 $ hg -R issue1852d incoming --subrepos issue1852c
677 677 comparing with issue1852c
678 678 searching for changes
679 679 no changes found
680 680 comparing with issue1852c/sub/repo
681 681 searching for changes
682 682 no changes found
683 683 [1]
684 684
685 685 Check status of files when none of them belong to the first
686 686 subrepository:
687 687
688 688 $ hg init subrepo-status
689 689 $ cd subrepo-status
690 690 $ hg init subrepo-1
691 691 $ hg init subrepo-2
692 692 $ cd subrepo-2
693 693 $ touch file
694 694 $ hg add file
695 695 $ cd ..
696 696 $ echo subrepo-1 = subrepo-1 > .hgsub
697 697 $ echo subrepo-2 = subrepo-2 >> .hgsub
698 698 $ hg add .hgsub
699 699 $ hg ci -m 'Added subrepos'
700 700 committing subrepository subrepo-1
701 701 committing subrepository subrepo-2
702 702 $ hg st subrepo-2/file
703 703
704 704 Check hg update --clean
705 705 $ cd $TESTTMP/sub/t
706 706 $ rm -r t/t.orig
707 707 $ hg status -S --all
708 708 C .hgsub
709 709 C .hgsubstate
710 710 C a
711 711 C s/.hgsub
712 712 C s/.hgsubstate
713 713 C s/a
714 714 C s/ss/a
715 715 C t/t
716 716 $ echo c1 > s/a
717 717 $ cd s
718 718 $ echo c1 > b
719 719 $ echo c1 > c
720 720 $ hg add b
721 721 $ cd ..
722 722 $ hg status -S
723 723 M s/a
724 724 A s/b
725 725 ? s/c
726 726 $ hg update -C
727 727 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
728 728 $ hg status -S
729 729 ? s/b
730 730 ? s/c
731 731
732 732 Sticky subrepositories, no changes
733 733 $ cd $TESTTMP/sub/t
734 734 $ hg id
735 735 925c17564ef8 tip
736 736 $ hg -R s id
737 737 12a213df6fa9 tip
738 738 $ hg -R t id
739 739 52c0adc0515a tip
740 740 $ hg update 11
741 741 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
742 742 $ hg id
743 743 365661e5936a
744 744 $ hg -R s id
745 745 fc627a69481f
746 746 $ hg -R t id
747 747 e95bcfa18a35
748 748
749 749 Sticky subrepositorys, file changes
750 750 $ touch s/f1
751 751 $ touch t/f1
752 752 $ hg add -S s/f1
753 753 $ hg add -S t/f1
754 754 $ hg id
755 755 365661e5936a
756 756 $ hg -R s id
757 757 fc627a69481f+
758 758 $ hg -R t id
759 759 e95bcfa18a35+
760 760 $ hg update tip
761 761 subrepository sources for s differ
762 762 use (l)ocal source (fc627a69481f) or (r)emote source (12a213df6fa9)?
763 763 l
764 764 subrepository sources for t differ
765 765 use (l)ocal source (e95bcfa18a35) or (r)emote source (52c0adc0515a)?
766 766 l
767 767 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
768 768 $ hg id
769 769 925c17564ef8+ tip
770 770 $ hg -R s id
771 771 fc627a69481f+
772 772 $ hg -R t id
773 773 e95bcfa18a35+
774 774 $ hg update --clean tip
775 775 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
776 776
777 777 Sticky subrepository, revision updates
778 778 $ hg id
779 779 925c17564ef8 tip
780 780 $ hg -R s id
781 781 12a213df6fa9 tip
782 782 $ hg -R t id
783 783 52c0adc0515a tip
784 784 $ cd s
785 785 $ hg update -r -2
786 786 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
787 787 $ cd ../t
788 788 $ hg update -r 2
789 789 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
790 790 $ cd ..
791 791 $ hg update 10
792 792 subrepository sources for t differ (in checked out version)
793 793 use (l)ocal source (7af322bc1198) or (r)emote source (20a0db6fbf6c)?
794 794 l
795 795 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
796 796 $ hg id
797 797 e45c8b14af55+
798 798 $ hg -R s id
799 799 1c833a7a9e3a
800 800 $ hg -R t id
801 801 7af322bc1198
802 802
803 803 Sticky subrepository, file changes and revision updates
804 804 $ touch s/f1
805 805 $ touch t/f1
806 806 $ hg add -S s/f1
807 807 $ hg add -S t/f1
808 808 $ hg id
809 809 e45c8b14af55+
810 810 $ hg -R s id
811 811 1c833a7a9e3a+
812 812 $ hg -R t id
813 813 7af322bc1198+
814 814 $ hg update tip
815 815 subrepository sources for s differ
816 816 use (l)ocal source (1c833a7a9e3a) or (r)emote source (12a213df6fa9)?
817 817 l
818 818 subrepository sources for t differ
819 819 use (l)ocal source (7af322bc1198) or (r)emote source (52c0adc0515a)?
820 820 l
821 821 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
822 822 $ hg id
823 823 925c17564ef8 tip
824 824 $ hg -R s id
825 825 1c833a7a9e3a+
826 826 $ hg -R t id
827 827 7af322bc1198+
828 828
829 829 Sticky repository, update --clean
830 830 $ hg update --clean tip
831 831 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
832 832 $ hg id
833 833 925c17564ef8 tip
834 834 $ hg -R s id
835 835 12a213df6fa9 tip
836 836 $ hg -R t id
837 837 52c0adc0515a tip
838 838
839 839 Test subrepo already at intended revision:
840 840 $ cd s
841 841 $ hg update fc627a69481f
842 842 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
843 843 $ cd ..
844 844 $ hg update 11
845 845 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
846 846 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
847 847 $ hg id -n
848 848 11+
849 849 $ hg -R s id
850 850 fc627a69481f
851 851 $ hg -R t id
852 852 e95bcfa18a35
853 853
854 854 Test that removing .hgsubstate doesn't break anything:
855 855
856 856 $ hg rm -f .hgsubstate
857 857 $ hg ci -mrm
858 858 committing subrepository s
859 859 committing subrepository t
860 860 created new head
861 861 $ hg log -vr tip
862 862 changeset: 14:3941e0aa5236
863 863 tag: tip
864 864 parent: 11:365661e5936a
865 865 user: test
866 866 date: Thu Jan 01 00:00:00 1970 +0000
867 867 description:
868 868 rm
869 869
870 870
871 871
872 872 Test that removing .hgsub removes .hgsubstate:
873 873
874 874 $ hg rm .hgsub
875 875 $ hg ci -mrm2
876 876 $ hg log -vr tip
877 877 changeset: 15:8b31de9d13d1
878 878 tag: tip
879 879 user: test
880 880 date: Thu Jan 01 00:00:00 1970 +0000
881 881 files: .hgsub .hgsubstate
882 882 description:
883 883 rm2
884 884
885 885
886 886 Test behavior of add for explicit path in subrepo:
887 887 $ cd ..
888 888 $ hg init addtests
889 889 $ cd addtests
890 890 $ echo s = s > .hgsub
891 891 $ hg add .hgsub
892 892 $ hg init s
893 893 $ hg ci -m0
894 894 committing subrepository s
895 Adding with an explicit path in a subrepo currently fails silently
895 Adding with an explicit path in a subrepo adds the file
896 896 $ echo c1 > f1
897 897 $ echo c2 > s/f2
898 898 $ hg st -S
899 899 ? f1
900 900 ? s/f2
901 901 $ hg add s/f2
902 902 $ hg st -S
903 A s/f2
903 904 ? f1
904 ? s/f2
905 $ hg ci -R s -Am0
906 adding f2
905 $ hg ci -R s -m0
907 906 $ hg ci -Am1
908 907 adding f1
909 908 committing subrepository s
910 Adding with an explicit path in a subrepo with -S adds the file
909 Adding with an explicit path in a subrepo with -S has the same behavior
911 910 $ echo c3 > f3
912 911 $ echo c4 > s/f4
913 912 $ hg st -S
914 913 ? f3
915 914 ? s/f4
916 915 $ hg add -S s/f4
917 916 $ hg st -S
918 917 A s/f4
919 918 ? f3
920 919 $ hg ci -R s -m1
921 920 $ hg ci -Ama2
922 921 adding f3
923 922 committing subrepository s
924 923 Adding without a path or pattern silently ignores subrepos
925 924 $ echo c5 > f5
926 925 $ echo c6 > s/f6
927 926 $ echo c7 > s/f7
928 927 $ hg st -S
929 928 ? f5
930 929 ? s/f6
931 930 ? s/f7
932 931 $ hg add
933 932 adding f5
934 933 $ hg st -S
935 934 A f5
936 935 ? s/f6
937 936 ? s/f7
938 937 $ hg ci -R s -Am2
939 938 adding f6
940 939 adding f7
941 940 $ hg ci -m3
942 941 committing subrepository s
943 942 Adding without a path or pattern with -S also adds files in subrepos
944 943 $ echo c8 > f8
945 944 $ echo c9 > s/f9
946 945 $ echo c10 > s/f10
947 946 $ hg st -S
948 947 ? f8
949 948 ? s/f10
950 949 ? s/f9
951 950 $ hg add -S
952 951 adding f8
953 952 adding s/f10
954 953 adding s/f9
955 954 $ hg st -S
956 955 A f8
957 956 A s/f10
958 957 A s/f9
959 958 $ hg ci -R s -m3
960 959 $ hg ci -m4
961 960 committing subrepository s
962 961 Adding with a pattern silently ignores subrepos
963 962 $ echo c11 > fm11
964 963 $ echo c12 > fn12
965 964 $ echo c13 > s/fm13
966 965 $ echo c14 > s/fn14
967 966 $ hg st -S
968 967 ? fm11
969 968 ? fn12
970 969 ? s/fm13
971 970 ? s/fn14
972 971 $ hg add 'glob:**fm*'
973 972 adding fm11
974 973 $ hg st -S
975 974 A fm11
976 975 ? fn12
977 976 ? s/fm13
978 977 ? s/fn14
979 978 $ hg ci -R s -Am4
980 979 adding fm13
981 980 adding fn14
982 981 $ hg ci -Am5
983 982 adding fn12
984 983 committing subrepository s
985 984 Adding with a pattern with -S also adds matches in subrepos
986 985 $ echo c15 > fm15
987 986 $ echo c16 > fn16
988 987 $ echo c17 > s/fm17
989 988 $ echo c18 > s/fn18
990 989 $ hg st -S
991 990 ? fm15
992 991 ? fn16
993 992 ? s/fm17
994 993 ? s/fn18
995 994 $ hg add -S 'glob:**fm*'
996 995 adding fm15
997 996 adding s/fm17
998 997 $ hg st -S
999 998 A fm15
1000 999 A s/fm17
1001 1000 ? fn16
1002 1001 ? s/fn18
1003 1002 $ hg ci -R s -Am5
1004 1003 adding fn18
1005 1004 $ hg ci -Am6
1006 1005 adding fn16
1007 1006 committing subrepository s
1008 1007 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now