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