##// END OF EJS Templates
subrepo: add argument to "diff()" to pass "ui" of caller side (issue3712) (API)...
FUJIWARA Katsunori -
r18006:0c10cf81 2.4.1 stable
parent child Browse files
Show More
@@ -1,2022 +1,2022 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, context, repair, bookmarks, graphmod, revset, phases, obsolete
14 14 import changelog
15 15 import lock as lockmod
16 16
17 17 def parsealiases(cmd):
18 18 return cmd.lstrip("^").split("|")
19 19
20 20 def findpossible(cmd, table, strict=False):
21 21 """
22 22 Return cmd -> (aliases, command table entry)
23 23 for each matching command.
24 24 Return debug commands (or their aliases) only if no normal command matches.
25 25 """
26 26 choice = {}
27 27 debugchoice = {}
28 28
29 29 if cmd in table:
30 30 # short-circuit exact matches, "log" alias beats "^log|history"
31 31 keys = [cmd]
32 32 else:
33 33 keys = table.keys()
34 34
35 35 for e in keys:
36 36 aliases = parsealiases(e)
37 37 found = None
38 38 if cmd in aliases:
39 39 found = cmd
40 40 elif not strict:
41 41 for a in aliases:
42 42 if a.startswith(cmd):
43 43 found = a
44 44 break
45 45 if found is not None:
46 46 if aliases[0].startswith("debug") or found.startswith("debug"):
47 47 debugchoice[found] = (aliases, table[e])
48 48 else:
49 49 choice[found] = (aliases, table[e])
50 50
51 51 if not choice and debugchoice:
52 52 choice = debugchoice
53 53
54 54 return choice
55 55
56 56 def findcmd(cmd, table, strict=True):
57 57 """Return (aliases, command table entry) for command string."""
58 58 choice = findpossible(cmd, table, strict)
59 59
60 60 if cmd in choice:
61 61 return choice[cmd]
62 62
63 63 if len(choice) > 1:
64 64 clist = choice.keys()
65 65 clist.sort()
66 66 raise error.AmbiguousCommand(cmd, clist)
67 67
68 68 if choice:
69 69 return choice.values()[0]
70 70
71 71 raise error.UnknownCommand(cmd)
72 72
73 73 def findrepo(p):
74 74 while not os.path.isdir(os.path.join(p, ".hg")):
75 75 oldp, p = p, os.path.dirname(p)
76 76 if p == oldp:
77 77 return None
78 78
79 79 return p
80 80
81 81 def bailifchanged(repo):
82 82 if repo.dirstate.p2() != nullid:
83 83 raise util.Abort(_('outstanding uncommitted merge'))
84 84 modified, added, removed, deleted = repo.status()[:4]
85 85 if modified or added or removed or deleted:
86 86 raise util.Abort(_("outstanding uncommitted changes"))
87 87 ctx = repo[None]
88 88 for s in ctx.substate:
89 89 if ctx.sub(s).dirty():
90 90 raise util.Abort(_("uncommitted changes in subrepo %s") % s)
91 91
92 92 def logmessage(ui, opts):
93 93 """ get the log message according to -m and -l option """
94 94 message = opts.get('message')
95 95 logfile = opts.get('logfile')
96 96
97 97 if message and logfile:
98 98 raise util.Abort(_('options --message and --logfile are mutually '
99 99 'exclusive'))
100 100 if not message and logfile:
101 101 try:
102 102 if logfile == '-':
103 103 message = ui.fin.read()
104 104 else:
105 105 message = '\n'.join(util.readfile(logfile).splitlines())
106 106 except IOError, inst:
107 107 raise util.Abort(_("can't read commit message '%s': %s") %
108 108 (logfile, inst.strerror))
109 109 return message
110 110
111 111 def loglimit(opts):
112 112 """get the log limit according to option -l/--limit"""
113 113 limit = opts.get('limit')
114 114 if limit:
115 115 try:
116 116 limit = int(limit)
117 117 except ValueError:
118 118 raise util.Abort(_('limit must be a positive integer'))
119 119 if limit <= 0:
120 120 raise util.Abort(_('limit must be positive'))
121 121 else:
122 122 limit = None
123 123 return limit
124 124
125 125 def makefilename(repo, pat, node, desc=None,
126 126 total=None, seqno=None, revwidth=None, pathname=None):
127 127 node_expander = {
128 128 'H': lambda: hex(node),
129 129 'R': lambda: str(repo.changelog.rev(node)),
130 130 'h': lambda: short(node),
131 131 'm': lambda: re.sub('[^\w]', '_', str(desc))
132 132 }
133 133 expander = {
134 134 '%': lambda: '%',
135 135 'b': lambda: os.path.basename(repo.root),
136 136 }
137 137
138 138 try:
139 139 if node:
140 140 expander.update(node_expander)
141 141 if node:
142 142 expander['r'] = (lambda:
143 143 str(repo.changelog.rev(node)).zfill(revwidth or 0))
144 144 if total is not None:
145 145 expander['N'] = lambda: str(total)
146 146 if seqno is not None:
147 147 expander['n'] = lambda: str(seqno)
148 148 if total is not None and seqno is not None:
149 149 expander['n'] = lambda: str(seqno).zfill(len(str(total)))
150 150 if pathname is not None:
151 151 expander['s'] = lambda: os.path.basename(pathname)
152 152 expander['d'] = lambda: os.path.dirname(pathname) or '.'
153 153 expander['p'] = lambda: pathname
154 154
155 155 newname = []
156 156 patlen = len(pat)
157 157 i = 0
158 158 while i < patlen:
159 159 c = pat[i]
160 160 if c == '%':
161 161 i += 1
162 162 c = pat[i]
163 163 c = expander[c]()
164 164 newname.append(c)
165 165 i += 1
166 166 return ''.join(newname)
167 167 except KeyError, inst:
168 168 raise util.Abort(_("invalid format spec '%%%s' in output filename") %
169 169 inst.args[0])
170 170
171 171 def makefileobj(repo, pat, node=None, desc=None, total=None,
172 172 seqno=None, revwidth=None, mode='wb', pathname=None):
173 173
174 174 writable = mode not in ('r', 'rb')
175 175
176 176 if not pat or pat == '-':
177 177 fp = writable and repo.ui.fout or repo.ui.fin
178 178 if util.safehasattr(fp, 'fileno'):
179 179 return os.fdopen(os.dup(fp.fileno()), mode)
180 180 else:
181 181 # if this fp can't be duped properly, return
182 182 # a dummy object that can be closed
183 183 class wrappedfileobj(object):
184 184 noop = lambda x: None
185 185 def __init__(self, f):
186 186 self.f = f
187 187 def __getattr__(self, attr):
188 188 if attr == 'close':
189 189 return self.noop
190 190 else:
191 191 return getattr(self.f, attr)
192 192
193 193 return wrappedfileobj(fp)
194 194 if util.safehasattr(pat, 'write') and writable:
195 195 return pat
196 196 if util.safehasattr(pat, 'read') and 'r' in mode:
197 197 return pat
198 198 return open(makefilename(repo, pat, node, desc, total, seqno, revwidth,
199 199 pathname),
200 200 mode)
201 201
202 202 def openrevlog(repo, cmd, file_, opts):
203 203 """opens the changelog, manifest, a filelog or a given revlog"""
204 204 cl = opts['changelog']
205 205 mf = opts['manifest']
206 206 msg = None
207 207 if cl and mf:
208 208 msg = _('cannot specify --changelog and --manifest at the same time')
209 209 elif cl or mf:
210 210 if file_:
211 211 msg = _('cannot specify filename with --changelog or --manifest')
212 212 elif not repo:
213 213 msg = _('cannot specify --changelog or --manifest '
214 214 'without a repository')
215 215 if msg:
216 216 raise util.Abort(msg)
217 217
218 218 r = None
219 219 if repo:
220 220 if cl:
221 221 r = repo.changelog
222 222 elif mf:
223 223 r = repo.manifest
224 224 elif file_:
225 225 filelog = repo.file(file_)
226 226 if len(filelog):
227 227 r = filelog
228 228 if not r:
229 229 if not file_:
230 230 raise error.CommandError(cmd, _('invalid arguments'))
231 231 if not os.path.isfile(file_):
232 232 raise util.Abort(_("revlog '%s' not found") % file_)
233 233 r = revlog.revlog(scmutil.opener(os.getcwd(), audit=False),
234 234 file_[:-2] + ".i")
235 235 return r
236 236
237 237 def copy(ui, repo, pats, opts, rename=False):
238 238 # called with the repo lock held
239 239 #
240 240 # hgsep => pathname that uses "/" to separate directories
241 241 # ossep => pathname that uses os.sep to separate directories
242 242 cwd = repo.getcwd()
243 243 targets = {}
244 244 after = opts.get("after")
245 245 dryrun = opts.get("dry_run")
246 246 wctx = repo[None]
247 247
248 248 def walkpat(pat):
249 249 srcs = []
250 250 badstates = after and '?' or '?r'
251 251 m = scmutil.match(repo[None], [pat], opts, globbed=True)
252 252 for abs in repo.walk(m):
253 253 state = repo.dirstate[abs]
254 254 rel = m.rel(abs)
255 255 exact = m.exact(abs)
256 256 if state in badstates:
257 257 if exact and state == '?':
258 258 ui.warn(_('%s: not copying - file is not managed\n') % rel)
259 259 if exact and state == 'r':
260 260 ui.warn(_('%s: not copying - file has been marked for'
261 261 ' remove\n') % rel)
262 262 continue
263 263 # abs: hgsep
264 264 # rel: ossep
265 265 srcs.append((abs, rel, exact))
266 266 return srcs
267 267
268 268 # abssrc: hgsep
269 269 # relsrc: ossep
270 270 # otarget: ossep
271 271 def copyfile(abssrc, relsrc, otarget, exact):
272 272 abstarget = scmutil.canonpath(repo.root, cwd, otarget)
273 273 if '/' in abstarget:
274 274 # We cannot normalize abstarget itself, this would prevent
275 275 # case only renames, like a => A.
276 276 abspath, absname = abstarget.rsplit('/', 1)
277 277 abstarget = repo.dirstate.normalize(abspath) + '/' + absname
278 278 reltarget = repo.pathto(abstarget, cwd)
279 279 target = repo.wjoin(abstarget)
280 280 src = repo.wjoin(abssrc)
281 281 state = repo.dirstate[abstarget]
282 282
283 283 scmutil.checkportable(ui, abstarget)
284 284
285 285 # check for collisions
286 286 prevsrc = targets.get(abstarget)
287 287 if prevsrc is not None:
288 288 ui.warn(_('%s: not overwriting - %s collides with %s\n') %
289 289 (reltarget, repo.pathto(abssrc, cwd),
290 290 repo.pathto(prevsrc, cwd)))
291 291 return
292 292
293 293 # check for overwrites
294 294 exists = os.path.lexists(target)
295 295 samefile = False
296 296 if exists and abssrc != abstarget:
297 297 if (repo.dirstate.normalize(abssrc) ==
298 298 repo.dirstate.normalize(abstarget)):
299 299 if not rename:
300 300 ui.warn(_("%s: can't copy - same file\n") % reltarget)
301 301 return
302 302 exists = False
303 303 samefile = True
304 304
305 305 if not after and exists or after and state in 'mn':
306 306 if not opts['force']:
307 307 ui.warn(_('%s: not overwriting - file exists\n') %
308 308 reltarget)
309 309 return
310 310
311 311 if after:
312 312 if not exists:
313 313 if rename:
314 314 ui.warn(_('%s: not recording move - %s does not exist\n') %
315 315 (relsrc, reltarget))
316 316 else:
317 317 ui.warn(_('%s: not recording copy - %s does not exist\n') %
318 318 (relsrc, reltarget))
319 319 return
320 320 elif not dryrun:
321 321 try:
322 322 if exists:
323 323 os.unlink(target)
324 324 targetdir = os.path.dirname(target) or '.'
325 325 if not os.path.isdir(targetdir):
326 326 os.makedirs(targetdir)
327 327 if samefile:
328 328 tmp = target + "~hgrename"
329 329 os.rename(src, tmp)
330 330 os.rename(tmp, target)
331 331 else:
332 332 util.copyfile(src, target)
333 333 srcexists = True
334 334 except IOError, inst:
335 335 if inst.errno == errno.ENOENT:
336 336 ui.warn(_('%s: deleted in working copy\n') % relsrc)
337 337 srcexists = False
338 338 else:
339 339 ui.warn(_('%s: cannot copy - %s\n') %
340 340 (relsrc, inst.strerror))
341 341 return True # report a failure
342 342
343 343 if ui.verbose or not exact:
344 344 if rename:
345 345 ui.status(_('moving %s to %s\n') % (relsrc, reltarget))
346 346 else:
347 347 ui.status(_('copying %s to %s\n') % (relsrc, reltarget))
348 348
349 349 targets[abstarget] = abssrc
350 350
351 351 # fix up dirstate
352 352 scmutil.dirstatecopy(ui, repo, wctx, abssrc, abstarget,
353 353 dryrun=dryrun, cwd=cwd)
354 354 if rename and not dryrun:
355 355 if not after and srcexists and not samefile:
356 356 util.unlinkpath(repo.wjoin(abssrc))
357 357 wctx.forget([abssrc])
358 358
359 359 # pat: ossep
360 360 # dest ossep
361 361 # srcs: list of (hgsep, hgsep, ossep, bool)
362 362 # return: function that takes hgsep and returns ossep
363 363 def targetpathfn(pat, dest, srcs):
364 364 if os.path.isdir(pat):
365 365 abspfx = scmutil.canonpath(repo.root, cwd, pat)
366 366 abspfx = util.localpath(abspfx)
367 367 if destdirexists:
368 368 striplen = len(os.path.split(abspfx)[0])
369 369 else:
370 370 striplen = len(abspfx)
371 371 if striplen:
372 372 striplen += len(os.sep)
373 373 res = lambda p: os.path.join(dest, util.localpath(p)[striplen:])
374 374 elif destdirexists:
375 375 res = lambda p: os.path.join(dest,
376 376 os.path.basename(util.localpath(p)))
377 377 else:
378 378 res = lambda p: dest
379 379 return res
380 380
381 381 # pat: ossep
382 382 # dest ossep
383 383 # srcs: list of (hgsep, hgsep, ossep, bool)
384 384 # return: function that takes hgsep and returns ossep
385 385 def targetpathafterfn(pat, dest, srcs):
386 386 if matchmod.patkind(pat):
387 387 # a mercurial pattern
388 388 res = lambda p: os.path.join(dest,
389 389 os.path.basename(util.localpath(p)))
390 390 else:
391 391 abspfx = scmutil.canonpath(repo.root, cwd, pat)
392 392 if len(abspfx) < len(srcs[0][0]):
393 393 # A directory. Either the target path contains the last
394 394 # component of the source path or it does not.
395 395 def evalpath(striplen):
396 396 score = 0
397 397 for s in srcs:
398 398 t = os.path.join(dest, util.localpath(s[0])[striplen:])
399 399 if os.path.lexists(t):
400 400 score += 1
401 401 return score
402 402
403 403 abspfx = util.localpath(abspfx)
404 404 striplen = len(abspfx)
405 405 if striplen:
406 406 striplen += len(os.sep)
407 407 if os.path.isdir(os.path.join(dest, os.path.split(abspfx)[1])):
408 408 score = evalpath(striplen)
409 409 striplen1 = len(os.path.split(abspfx)[0])
410 410 if striplen1:
411 411 striplen1 += len(os.sep)
412 412 if evalpath(striplen1) > score:
413 413 striplen = striplen1
414 414 res = lambda p: os.path.join(dest,
415 415 util.localpath(p)[striplen:])
416 416 else:
417 417 # a file
418 418 if destdirexists:
419 419 res = lambda p: os.path.join(dest,
420 420 os.path.basename(util.localpath(p)))
421 421 else:
422 422 res = lambda p: dest
423 423 return res
424 424
425 425
426 426 pats = scmutil.expandpats(pats)
427 427 if not pats:
428 428 raise util.Abort(_('no source or destination specified'))
429 429 if len(pats) == 1:
430 430 raise util.Abort(_('no destination specified'))
431 431 dest = pats.pop()
432 432 destdirexists = os.path.isdir(dest) and not os.path.islink(dest)
433 433 if not destdirexists:
434 434 if len(pats) > 1 or matchmod.patkind(pats[0]):
435 435 raise util.Abort(_('with multiple sources, destination must be an '
436 436 'existing directory'))
437 437 if util.endswithsep(dest):
438 438 raise util.Abort(_('destination %s is not a directory') % dest)
439 439
440 440 tfn = targetpathfn
441 441 if after:
442 442 tfn = targetpathafterfn
443 443 copylist = []
444 444 for pat in pats:
445 445 srcs = walkpat(pat)
446 446 if not srcs:
447 447 continue
448 448 copylist.append((tfn(pat, dest, srcs), srcs))
449 449 if not copylist:
450 450 raise util.Abort(_('no files to copy'))
451 451
452 452 errors = 0
453 453 for targetpath, srcs in copylist:
454 454 for abssrc, relsrc, exact in srcs:
455 455 if copyfile(abssrc, relsrc, targetpath(abssrc), exact):
456 456 errors += 1
457 457
458 458 if errors:
459 459 ui.warn(_('(consider using --after)\n'))
460 460
461 461 return errors != 0
462 462
463 463 def service(opts, parentfn=None, initfn=None, runfn=None, logfile=None,
464 464 runargs=None, appendpid=False):
465 465 '''Run a command as a service.'''
466 466
467 467 if opts['daemon'] and not opts['daemon_pipefds']:
468 468 # Signal child process startup with file removal
469 469 lockfd, lockpath = tempfile.mkstemp(prefix='hg-service-')
470 470 os.close(lockfd)
471 471 try:
472 472 if not runargs:
473 473 runargs = util.hgcmd() + sys.argv[1:]
474 474 runargs.append('--daemon-pipefds=%s' % lockpath)
475 475 # Don't pass --cwd to the child process, because we've already
476 476 # changed directory.
477 477 for i in xrange(1, len(runargs)):
478 478 if runargs[i].startswith('--cwd='):
479 479 del runargs[i]
480 480 break
481 481 elif runargs[i].startswith('--cwd'):
482 482 del runargs[i:i + 2]
483 483 break
484 484 def condfn():
485 485 return not os.path.exists(lockpath)
486 486 pid = util.rundetached(runargs, condfn)
487 487 if pid < 0:
488 488 raise util.Abort(_('child process failed to start'))
489 489 finally:
490 490 try:
491 491 os.unlink(lockpath)
492 492 except OSError, e:
493 493 if e.errno != errno.ENOENT:
494 494 raise
495 495 if parentfn:
496 496 return parentfn(pid)
497 497 else:
498 498 return
499 499
500 500 if initfn:
501 501 initfn()
502 502
503 503 if opts['pid_file']:
504 504 mode = appendpid and 'a' or 'w'
505 505 fp = open(opts['pid_file'], mode)
506 506 fp.write(str(os.getpid()) + '\n')
507 507 fp.close()
508 508
509 509 if opts['daemon_pipefds']:
510 510 lockpath = opts['daemon_pipefds']
511 511 try:
512 512 os.setsid()
513 513 except AttributeError:
514 514 pass
515 515 os.unlink(lockpath)
516 516 util.hidewindow()
517 517 sys.stdout.flush()
518 518 sys.stderr.flush()
519 519
520 520 nullfd = os.open(os.devnull, os.O_RDWR)
521 521 logfilefd = nullfd
522 522 if logfile:
523 523 logfilefd = os.open(logfile, os.O_RDWR | os.O_CREAT | os.O_APPEND)
524 524 os.dup2(nullfd, 0)
525 525 os.dup2(logfilefd, 1)
526 526 os.dup2(logfilefd, 2)
527 527 if nullfd not in (0, 1, 2):
528 528 os.close(nullfd)
529 529 if logfile and logfilefd not in (0, 1, 2):
530 530 os.close(logfilefd)
531 531
532 532 if runfn:
533 533 return runfn()
534 534
535 535 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
536 536 opts=None):
537 537 '''export changesets as hg patches.'''
538 538
539 539 total = len(revs)
540 540 revwidth = max([len(str(rev)) for rev in revs])
541 541
542 542 def single(rev, seqno, fp):
543 543 ctx = repo[rev]
544 544 node = ctx.node()
545 545 parents = [p.node() for p in ctx.parents() if p]
546 546 branch = ctx.branch()
547 547 if switch_parent:
548 548 parents.reverse()
549 549 prev = (parents and parents[0]) or nullid
550 550
551 551 shouldclose = False
552 552 if not fp and len(template) > 0:
553 553 desc_lines = ctx.description().rstrip().split('\n')
554 554 desc = desc_lines[0] #Commit always has a first line.
555 555 fp = makefileobj(repo, template, node, desc=desc, total=total,
556 556 seqno=seqno, revwidth=revwidth, mode='ab')
557 557 if fp != template:
558 558 shouldclose = True
559 559 if fp and fp != sys.stdout and util.safehasattr(fp, 'name'):
560 560 repo.ui.note("%s\n" % fp.name)
561 561
562 562 if not fp:
563 563 write = repo.ui.write
564 564 else:
565 565 def write(s, **kw):
566 566 fp.write(s)
567 567
568 568
569 569 write("# HG changeset patch\n")
570 570 write("# User %s\n" % ctx.user())
571 571 write("# Date %d %d\n" % ctx.date())
572 572 if branch and branch != 'default':
573 573 write("# Branch %s\n" % branch)
574 574 write("# Node ID %s\n" % hex(node))
575 575 write("# Parent %s\n" % hex(prev))
576 576 if len(parents) > 1:
577 577 write("# Parent %s\n" % hex(parents[1]))
578 578 write(ctx.description().rstrip())
579 579 write("\n\n")
580 580
581 581 for chunk, label in patch.diffui(repo, prev, node, opts=opts):
582 582 write(chunk, label=label)
583 583
584 584 if shouldclose:
585 585 fp.close()
586 586
587 587 for seqno, rev in enumerate(revs):
588 588 single(rev, seqno + 1, fp)
589 589
590 590 def diffordiffstat(ui, repo, diffopts, node1, node2, match,
591 591 changes=None, stat=False, fp=None, prefix='',
592 592 listsubrepos=False):
593 593 '''show diff or diffstat.'''
594 594 if fp is None:
595 595 write = ui.write
596 596 else:
597 597 def write(s, **kw):
598 598 fp.write(s)
599 599
600 600 if stat:
601 601 diffopts = diffopts.copy(context=0)
602 602 width = 80
603 603 if not ui.plain():
604 604 width = ui.termwidth()
605 605 chunks = patch.diff(repo, node1, node2, match, changes, diffopts,
606 606 prefix=prefix)
607 607 for chunk, label in patch.diffstatui(util.iterlines(chunks),
608 608 width=width,
609 609 git=diffopts.git):
610 610 write(chunk, label=label)
611 611 else:
612 612 for chunk, label in patch.diffui(repo, node1, node2, match,
613 613 changes, diffopts, prefix=prefix):
614 614 write(chunk, label=label)
615 615
616 616 if listsubrepos:
617 617 ctx1 = repo[node1]
618 618 ctx2 = repo[node2]
619 619 for subpath, sub in subrepo.itersubrepos(ctx1, ctx2):
620 620 tempnode2 = node2
621 621 try:
622 622 if node2 is not None:
623 623 tempnode2 = ctx2.substate[subpath][1]
624 624 except KeyError:
625 625 # A subrepo that existed in node1 was deleted between node1 and
626 626 # node2 (inclusive). Thus, ctx2's substate won't contain that
627 627 # subpath. The best we can do is to ignore it.
628 628 tempnode2 = None
629 629 submatch = matchmod.narrowmatcher(subpath, match)
630 sub.diff(diffopts, tempnode2, submatch, changes=changes,
630 sub.diff(ui, diffopts, tempnode2, submatch, changes=changes,
631 631 stat=stat, fp=fp, prefix=prefix)
632 632
633 633 class changeset_printer(object):
634 634 '''show changeset information when templating not requested.'''
635 635
636 636 def __init__(self, ui, repo, patch, diffopts, buffered):
637 637 self.ui = ui
638 638 self.repo = repo
639 639 self.buffered = buffered
640 640 self.patch = patch
641 641 self.diffopts = diffopts
642 642 self.header = {}
643 643 self.hunk = {}
644 644 self.lastheader = None
645 645 self.footer = None
646 646
647 647 def flush(self, rev):
648 648 if rev in self.header:
649 649 h = self.header[rev]
650 650 if h != self.lastheader:
651 651 self.lastheader = h
652 652 self.ui.write(h)
653 653 del self.header[rev]
654 654 if rev in self.hunk:
655 655 self.ui.write(self.hunk[rev])
656 656 del self.hunk[rev]
657 657 return 1
658 658 return 0
659 659
660 660 def close(self):
661 661 if self.footer:
662 662 self.ui.write(self.footer)
663 663
664 664 def show(self, ctx, copies=None, matchfn=None, **props):
665 665 if self.buffered:
666 666 self.ui.pushbuffer()
667 667 self._show(ctx, copies, matchfn, props)
668 668 self.hunk[ctx.rev()] = self.ui.popbuffer(labeled=True)
669 669 else:
670 670 self._show(ctx, copies, matchfn, props)
671 671
672 672 def _show(self, ctx, copies, matchfn, props):
673 673 '''show a single changeset or file revision'''
674 674 changenode = ctx.node()
675 675 rev = ctx.rev()
676 676
677 677 if self.ui.quiet:
678 678 self.ui.write("%d:%s\n" % (rev, short(changenode)),
679 679 label='log.node')
680 680 return
681 681
682 682 log = self.repo.changelog
683 683 date = util.datestr(ctx.date())
684 684
685 685 hexfunc = self.ui.debugflag and hex or short
686 686
687 687 parents = [(p, hexfunc(log.node(p)))
688 688 for p in self._meaningful_parentrevs(log, rev)]
689 689
690 690 # i18n: column positioning for "hg log"
691 691 self.ui.write(_("changeset: %d:%s\n") % (rev, hexfunc(changenode)),
692 692 label='log.changeset changeset.%s' % ctx.phasestr())
693 693
694 694 branch = ctx.branch()
695 695 # don't show the default branch name
696 696 if branch != 'default':
697 697 # i18n: column positioning for "hg log"
698 698 self.ui.write(_("branch: %s\n") % branch,
699 699 label='log.branch')
700 700 for bookmark in self.repo.nodebookmarks(changenode):
701 701 # i18n: column positioning for "hg log"
702 702 self.ui.write(_("bookmark: %s\n") % bookmark,
703 703 label='log.bookmark')
704 704 for tag in self.repo.nodetags(changenode):
705 705 # i18n: column positioning for "hg log"
706 706 self.ui.write(_("tag: %s\n") % tag,
707 707 label='log.tag')
708 708 if self.ui.debugflag and ctx.phase():
709 709 # i18n: column positioning for "hg log"
710 710 self.ui.write(_("phase: %s\n") % _(ctx.phasestr()),
711 711 label='log.phase')
712 712 for parent in parents:
713 713 # i18n: column positioning for "hg log"
714 714 self.ui.write(_("parent: %d:%s\n") % parent,
715 715 label='log.parent changeset.%s' % ctx.phasestr())
716 716
717 717 if self.ui.debugflag:
718 718 mnode = ctx.manifestnode()
719 719 # i18n: column positioning for "hg log"
720 720 self.ui.write(_("manifest: %d:%s\n") %
721 721 (self.repo.manifest.rev(mnode), hex(mnode)),
722 722 label='ui.debug log.manifest')
723 723 # i18n: column positioning for "hg log"
724 724 self.ui.write(_("user: %s\n") % ctx.user(),
725 725 label='log.user')
726 726 # i18n: column positioning for "hg log"
727 727 self.ui.write(_("date: %s\n") % date,
728 728 label='log.date')
729 729
730 730 if self.ui.debugflag:
731 731 files = self.repo.status(log.parents(changenode)[0], changenode)[:3]
732 732 for key, value in zip([# i18n: column positioning for "hg log"
733 733 _("files:"),
734 734 # i18n: column positioning for "hg log"
735 735 _("files+:"),
736 736 # i18n: column positioning for "hg log"
737 737 _("files-:")], files):
738 738 if value:
739 739 self.ui.write("%-12s %s\n" % (key, " ".join(value)),
740 740 label='ui.debug log.files')
741 741 elif ctx.files() and self.ui.verbose:
742 742 # i18n: column positioning for "hg log"
743 743 self.ui.write(_("files: %s\n") % " ".join(ctx.files()),
744 744 label='ui.note log.files')
745 745 if copies and self.ui.verbose:
746 746 copies = ['%s (%s)' % c for c in copies]
747 747 # i18n: column positioning for "hg log"
748 748 self.ui.write(_("copies: %s\n") % ' '.join(copies),
749 749 label='ui.note log.copies')
750 750
751 751 extra = ctx.extra()
752 752 if extra and self.ui.debugflag:
753 753 for key, value in sorted(extra.items()):
754 754 # i18n: column positioning for "hg log"
755 755 self.ui.write(_("extra: %s=%s\n")
756 756 % (key, value.encode('string_escape')),
757 757 label='ui.debug log.extra')
758 758
759 759 description = ctx.description().strip()
760 760 if description:
761 761 if self.ui.verbose:
762 762 self.ui.write(_("description:\n"),
763 763 label='ui.note log.description')
764 764 self.ui.write(description,
765 765 label='ui.note log.description')
766 766 self.ui.write("\n\n")
767 767 else:
768 768 # i18n: column positioning for "hg log"
769 769 self.ui.write(_("summary: %s\n") %
770 770 description.splitlines()[0],
771 771 label='log.summary')
772 772 self.ui.write("\n")
773 773
774 774 self.showpatch(changenode, matchfn)
775 775
776 776 def showpatch(self, node, matchfn):
777 777 if not matchfn:
778 778 matchfn = self.patch
779 779 if matchfn:
780 780 stat = self.diffopts.get('stat')
781 781 diff = self.diffopts.get('patch')
782 782 diffopts = patch.diffopts(self.ui, self.diffopts)
783 783 prev = self.repo.changelog.parents(node)[0]
784 784 if stat:
785 785 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
786 786 match=matchfn, stat=True)
787 787 if diff:
788 788 if stat:
789 789 self.ui.write("\n")
790 790 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
791 791 match=matchfn, stat=False)
792 792 self.ui.write("\n")
793 793
794 794 def _meaningful_parentrevs(self, log, rev):
795 795 """Return list of meaningful (or all if debug) parentrevs for rev.
796 796
797 797 For merges (two non-nullrev revisions) both parents are meaningful.
798 798 Otherwise the first parent revision is considered meaningful if it
799 799 is not the preceding revision.
800 800 """
801 801 parents = log.parentrevs(rev)
802 802 if not self.ui.debugflag and parents[1] == nullrev:
803 803 if parents[0] >= rev - 1:
804 804 parents = []
805 805 else:
806 806 parents = [parents[0]]
807 807 return parents
808 808
809 809
810 810 class changeset_templater(changeset_printer):
811 811 '''format changeset information.'''
812 812
813 813 def __init__(self, ui, repo, patch, diffopts, mapfile, buffered):
814 814 changeset_printer.__init__(self, ui, repo, patch, diffopts, buffered)
815 815 formatnode = ui.debugflag and (lambda x: x) or (lambda x: x[:12])
816 816 defaulttempl = {
817 817 'parent': '{rev}:{node|formatnode} ',
818 818 'manifest': '{rev}:{node|formatnode}',
819 819 'file_copy': '{name} ({source})',
820 820 'extra': '{key}={value|stringescape}'
821 821 }
822 822 # filecopy is preserved for compatibility reasons
823 823 defaulttempl['filecopy'] = defaulttempl['file_copy']
824 824 self.t = templater.templater(mapfile, {'formatnode': formatnode},
825 825 cache=defaulttempl)
826 826 self.cache = {}
827 827
828 828 def use_template(self, t):
829 829 '''set template string to use'''
830 830 self.t.cache['changeset'] = t
831 831
832 832 def _meaningful_parentrevs(self, ctx):
833 833 """Return list of meaningful (or all if debug) parentrevs for rev.
834 834 """
835 835 parents = ctx.parents()
836 836 if len(parents) > 1:
837 837 return parents
838 838 if self.ui.debugflag:
839 839 return [parents[0], self.repo['null']]
840 840 if parents[0].rev() >= ctx.rev() - 1:
841 841 return []
842 842 return parents
843 843
844 844 def _show(self, ctx, copies, matchfn, props):
845 845 '''show a single changeset or file revision'''
846 846
847 847 showlist = templatekw.showlist
848 848
849 849 # showparents() behaviour depends on ui trace level which
850 850 # causes unexpected behaviours at templating level and makes
851 851 # it harder to extract it in a standalone function. Its
852 852 # behaviour cannot be changed so leave it here for now.
853 853 def showparents(**args):
854 854 ctx = args['ctx']
855 855 parents = [[('rev', p.rev()), ('node', p.hex())]
856 856 for p in self._meaningful_parentrevs(ctx)]
857 857 return showlist('parent', parents, **args)
858 858
859 859 props = props.copy()
860 860 props.update(templatekw.keywords)
861 861 props['parents'] = showparents
862 862 props['templ'] = self.t
863 863 props['ctx'] = ctx
864 864 props['repo'] = self.repo
865 865 props['revcache'] = {'copies': copies}
866 866 props['cache'] = self.cache
867 867
868 868 # find correct templates for current mode
869 869
870 870 tmplmodes = [
871 871 (True, None),
872 872 (self.ui.verbose, 'verbose'),
873 873 (self.ui.quiet, 'quiet'),
874 874 (self.ui.debugflag, 'debug'),
875 875 ]
876 876
877 877 types = {'header': '', 'footer':'', 'changeset': 'changeset'}
878 878 for mode, postfix in tmplmodes:
879 879 for type in types:
880 880 cur = postfix and ('%s_%s' % (type, postfix)) or type
881 881 if mode and cur in self.t:
882 882 types[type] = cur
883 883
884 884 try:
885 885
886 886 # write header
887 887 if types['header']:
888 888 h = templater.stringify(self.t(types['header'], **props))
889 889 if self.buffered:
890 890 self.header[ctx.rev()] = h
891 891 else:
892 892 if self.lastheader != h:
893 893 self.lastheader = h
894 894 self.ui.write(h)
895 895
896 896 # write changeset metadata, then patch if requested
897 897 key = types['changeset']
898 898 self.ui.write(templater.stringify(self.t(key, **props)))
899 899 self.showpatch(ctx.node(), matchfn)
900 900
901 901 if types['footer']:
902 902 if not self.footer:
903 903 self.footer = templater.stringify(self.t(types['footer'],
904 904 **props))
905 905
906 906 except KeyError, inst:
907 907 msg = _("%s: no key named '%s'")
908 908 raise util.Abort(msg % (self.t.mapfile, inst.args[0]))
909 909 except SyntaxError, inst:
910 910 raise util.Abort('%s: %s' % (self.t.mapfile, inst.args[0]))
911 911
912 912 def show_changeset(ui, repo, opts, buffered=False):
913 913 """show one changeset using template or regular display.
914 914
915 915 Display format will be the first non-empty hit of:
916 916 1. option 'template'
917 917 2. option 'style'
918 918 3. [ui] setting 'logtemplate'
919 919 4. [ui] setting 'style'
920 920 If all of these values are either the unset or the empty string,
921 921 regular display via changeset_printer() is done.
922 922 """
923 923 # options
924 924 patch = False
925 925 if opts.get('patch') or opts.get('stat'):
926 926 patch = scmutil.matchall(repo)
927 927
928 928 tmpl = opts.get('template')
929 929 style = None
930 930 if tmpl:
931 931 tmpl = templater.parsestring(tmpl, quoted=False)
932 932 else:
933 933 style = opts.get('style')
934 934
935 935 # ui settings
936 936 if not (tmpl or style):
937 937 tmpl = ui.config('ui', 'logtemplate')
938 938 if tmpl:
939 939 try:
940 940 tmpl = templater.parsestring(tmpl)
941 941 except SyntaxError:
942 942 tmpl = templater.parsestring(tmpl, quoted=False)
943 943 else:
944 944 style = util.expandpath(ui.config('ui', 'style', ''))
945 945
946 946 if not (tmpl or style):
947 947 return changeset_printer(ui, repo, patch, opts, buffered)
948 948
949 949 mapfile = None
950 950 if style and not tmpl:
951 951 mapfile = style
952 952 if not os.path.split(mapfile)[0]:
953 953 mapname = (templater.templatepath('map-cmdline.' + mapfile)
954 954 or templater.templatepath(mapfile))
955 955 if mapname:
956 956 mapfile = mapname
957 957
958 958 try:
959 959 t = changeset_templater(ui, repo, patch, opts, mapfile, buffered)
960 960 except SyntaxError, inst:
961 961 raise util.Abort(inst.args[0])
962 962 if tmpl:
963 963 t.use_template(tmpl)
964 964 return t
965 965
966 966 def finddate(ui, repo, date):
967 967 """Find the tipmost changeset that matches the given date spec"""
968 968
969 969 df = util.matchdate(date)
970 970 m = scmutil.matchall(repo)
971 971 results = {}
972 972
973 973 def prep(ctx, fns):
974 974 d = ctx.date()
975 975 if df(d[0]):
976 976 results[ctx.rev()] = d
977 977
978 978 for ctx in walkchangerevs(repo, m, {'rev': None}, prep):
979 979 rev = ctx.rev()
980 980 if rev in results:
981 981 ui.status(_("found revision %s from %s\n") %
982 982 (rev, util.datestr(results[rev])))
983 983 return str(rev)
984 984
985 985 raise util.Abort(_("revision matching date not found"))
986 986
987 987 def increasingwindows(start, end, windowsize=8, sizelimit=512):
988 988 if start < end:
989 989 while start < end:
990 990 yield start, min(windowsize, end - start)
991 991 start += windowsize
992 992 if windowsize < sizelimit:
993 993 windowsize *= 2
994 994 else:
995 995 while start > end:
996 996 yield start, min(windowsize, start - end - 1)
997 997 start -= windowsize
998 998 if windowsize < sizelimit:
999 999 windowsize *= 2
1000 1000
1001 1001 def walkchangerevs(repo, match, opts, prepare):
1002 1002 '''Iterate over files and the revs in which they changed.
1003 1003
1004 1004 Callers most commonly need to iterate backwards over the history
1005 1005 in which they are interested. Doing so has awful (quadratic-looking)
1006 1006 performance, so we use iterators in a "windowed" way.
1007 1007
1008 1008 We walk a window of revisions in the desired order. Within the
1009 1009 window, we first walk forwards to gather data, then in the desired
1010 1010 order (usually backwards) to display it.
1011 1011
1012 1012 This function returns an iterator yielding contexts. Before
1013 1013 yielding each context, the iterator will first call the prepare
1014 1014 function on each context in the window in forward order.'''
1015 1015
1016 1016 follow = opts.get('follow') or opts.get('follow_first')
1017 1017
1018 1018 if not len(repo):
1019 1019 return []
1020 1020 if opts.get('rev'):
1021 1021 revs = scmutil.revrange(repo, opts.get('rev'))
1022 1022 elif follow:
1023 1023 revs = repo.revs('reverse(:.)')
1024 1024 else:
1025 1025 revs = list(repo)
1026 1026 revs.reverse()
1027 1027 if not revs:
1028 1028 return []
1029 1029 wanted = set()
1030 1030 slowpath = match.anypats() or (match.files() and opts.get('removed'))
1031 1031 fncache = {}
1032 1032 change = repo.changectx
1033 1033
1034 1034 # First step is to fill wanted, the set of revisions that we want to yield.
1035 1035 # When it does not induce extra cost, we also fill fncache for revisions in
1036 1036 # wanted: a cache of filenames that were changed (ctx.files()) and that
1037 1037 # match the file filtering conditions.
1038 1038
1039 1039 if not slowpath and not match.files():
1040 1040 # No files, no patterns. Display all revs.
1041 1041 wanted = set(revs)
1042 1042 copies = []
1043 1043
1044 1044 if not slowpath and match.files():
1045 1045 # We only have to read through the filelog to find wanted revisions
1046 1046
1047 1047 minrev, maxrev = min(revs), max(revs)
1048 1048 def filerevgen(filelog, last):
1049 1049 """
1050 1050 Only files, no patterns. Check the history of each file.
1051 1051
1052 1052 Examines filelog entries within minrev, maxrev linkrev range
1053 1053 Returns an iterator yielding (linkrev, parentlinkrevs, copied)
1054 1054 tuples in backwards order
1055 1055 """
1056 1056 cl_count = len(repo)
1057 1057 revs = []
1058 1058 for j in xrange(0, last + 1):
1059 1059 linkrev = filelog.linkrev(j)
1060 1060 if linkrev < minrev:
1061 1061 continue
1062 1062 # only yield rev for which we have the changelog, it can
1063 1063 # happen while doing "hg log" during a pull or commit
1064 1064 if linkrev >= cl_count:
1065 1065 break
1066 1066
1067 1067 parentlinkrevs = []
1068 1068 for p in filelog.parentrevs(j):
1069 1069 if p != nullrev:
1070 1070 parentlinkrevs.append(filelog.linkrev(p))
1071 1071 n = filelog.node(j)
1072 1072 revs.append((linkrev, parentlinkrevs,
1073 1073 follow and filelog.renamed(n)))
1074 1074
1075 1075 return reversed(revs)
1076 1076 def iterfiles():
1077 1077 pctx = repo['.']
1078 1078 for filename in match.files():
1079 1079 if follow:
1080 1080 if filename not in pctx:
1081 1081 raise util.Abort(_('cannot follow file not in parent '
1082 1082 'revision: "%s"') % filename)
1083 1083 yield filename, pctx[filename].filenode()
1084 1084 else:
1085 1085 yield filename, None
1086 1086 for filename_node in copies:
1087 1087 yield filename_node
1088 1088 for file_, node in iterfiles():
1089 1089 filelog = repo.file(file_)
1090 1090 if not len(filelog):
1091 1091 if node is None:
1092 1092 # A zero count may be a directory or deleted file, so
1093 1093 # try to find matching entries on the slow path.
1094 1094 if follow:
1095 1095 raise util.Abort(
1096 1096 _('cannot follow nonexistent file: "%s"') % file_)
1097 1097 slowpath = True
1098 1098 break
1099 1099 else:
1100 1100 continue
1101 1101
1102 1102 if node is None:
1103 1103 last = len(filelog) - 1
1104 1104 else:
1105 1105 last = filelog.rev(node)
1106 1106
1107 1107
1108 1108 # keep track of all ancestors of the file
1109 1109 ancestors = set([filelog.linkrev(last)])
1110 1110
1111 1111 # iterate from latest to oldest revision
1112 1112 for rev, flparentlinkrevs, copied in filerevgen(filelog, last):
1113 1113 if not follow:
1114 1114 if rev > maxrev:
1115 1115 continue
1116 1116 else:
1117 1117 # Note that last might not be the first interesting
1118 1118 # rev to us:
1119 1119 # if the file has been changed after maxrev, we'll
1120 1120 # have linkrev(last) > maxrev, and we still need
1121 1121 # to explore the file graph
1122 1122 if rev not in ancestors:
1123 1123 continue
1124 1124 # XXX insert 1327 fix here
1125 1125 if flparentlinkrevs:
1126 1126 ancestors.update(flparentlinkrevs)
1127 1127
1128 1128 fncache.setdefault(rev, []).append(file_)
1129 1129 wanted.add(rev)
1130 1130 if copied:
1131 1131 copies.append(copied)
1132 1132
1133 1133 # We decided to fall back to the slowpath because at least one
1134 1134 # of the paths was not a file. Check to see if at least one of them
1135 1135 # existed in history, otherwise simply return
1136 1136 if slowpath:
1137 1137 for path in match.files():
1138 1138 if path == '.' or path in repo.store:
1139 1139 break
1140 1140 else:
1141 1141 return []
1142 1142
1143 1143 if slowpath:
1144 1144 # We have to read the changelog to match filenames against
1145 1145 # changed files
1146 1146
1147 1147 if follow:
1148 1148 raise util.Abort(_('can only follow copies/renames for explicit '
1149 1149 'filenames'))
1150 1150
1151 1151 # The slow path checks files modified in every changeset.
1152 1152 for i in sorted(revs):
1153 1153 ctx = change(i)
1154 1154 matches = filter(match, ctx.files())
1155 1155 if matches:
1156 1156 fncache[i] = matches
1157 1157 wanted.add(i)
1158 1158
1159 1159 class followfilter(object):
1160 1160 def __init__(self, onlyfirst=False):
1161 1161 self.startrev = nullrev
1162 1162 self.roots = set()
1163 1163 self.onlyfirst = onlyfirst
1164 1164
1165 1165 def match(self, rev):
1166 1166 def realparents(rev):
1167 1167 if self.onlyfirst:
1168 1168 return repo.changelog.parentrevs(rev)[0:1]
1169 1169 else:
1170 1170 return filter(lambda x: x != nullrev,
1171 1171 repo.changelog.parentrevs(rev))
1172 1172
1173 1173 if self.startrev == nullrev:
1174 1174 self.startrev = rev
1175 1175 return True
1176 1176
1177 1177 if rev > self.startrev:
1178 1178 # forward: all descendants
1179 1179 if not self.roots:
1180 1180 self.roots.add(self.startrev)
1181 1181 for parent in realparents(rev):
1182 1182 if parent in self.roots:
1183 1183 self.roots.add(rev)
1184 1184 return True
1185 1185 else:
1186 1186 # backwards: all parents
1187 1187 if not self.roots:
1188 1188 self.roots.update(realparents(self.startrev))
1189 1189 if rev in self.roots:
1190 1190 self.roots.remove(rev)
1191 1191 self.roots.update(realparents(rev))
1192 1192 return True
1193 1193
1194 1194 return False
1195 1195
1196 1196 # it might be worthwhile to do this in the iterator if the rev range
1197 1197 # is descending and the prune args are all within that range
1198 1198 for rev in opts.get('prune', ()):
1199 1199 rev = repo[rev].rev()
1200 1200 ff = followfilter()
1201 1201 stop = min(revs[0], revs[-1])
1202 1202 for x in xrange(rev, stop - 1, -1):
1203 1203 if ff.match(x):
1204 1204 wanted.discard(x)
1205 1205
1206 1206 # Now that wanted is correctly initialized, we can iterate over the
1207 1207 # revision range, yielding only revisions in wanted.
1208 1208 def iterate():
1209 1209 if follow and not match.files():
1210 1210 ff = followfilter(onlyfirst=opts.get('follow_first'))
1211 1211 def want(rev):
1212 1212 return ff.match(rev) and rev in wanted
1213 1213 else:
1214 1214 def want(rev):
1215 1215 return rev in wanted
1216 1216
1217 1217 for i, window in increasingwindows(0, len(revs)):
1218 1218 nrevs = [rev for rev in revs[i:i + window] if want(rev)]
1219 1219 for rev in sorted(nrevs):
1220 1220 fns = fncache.get(rev)
1221 1221 ctx = change(rev)
1222 1222 if not fns:
1223 1223 def fns_generator():
1224 1224 for f in ctx.files():
1225 1225 if match(f):
1226 1226 yield f
1227 1227 fns = fns_generator()
1228 1228 prepare(ctx, fns)
1229 1229 for rev in nrevs:
1230 1230 yield change(rev)
1231 1231 return iterate()
1232 1232
1233 1233 def _makegraphfilematcher(repo, pats, followfirst):
1234 1234 # When displaying a revision with --patch --follow FILE, we have
1235 1235 # to know which file of the revision must be diffed. With
1236 1236 # --follow, we want the names of the ancestors of FILE in the
1237 1237 # revision, stored in "fcache". "fcache" is populated by
1238 1238 # reproducing the graph traversal already done by --follow revset
1239 1239 # and relating linkrevs to file names (which is not "correct" but
1240 1240 # good enough).
1241 1241 fcache = {}
1242 1242 fcacheready = [False]
1243 1243 pctx = repo['.']
1244 1244 wctx = repo[None]
1245 1245
1246 1246 def populate():
1247 1247 for fn in pats:
1248 1248 for i in ((pctx[fn],), pctx[fn].ancestors(followfirst=followfirst)):
1249 1249 for c in i:
1250 1250 fcache.setdefault(c.linkrev(), set()).add(c.path())
1251 1251
1252 1252 def filematcher(rev):
1253 1253 if not fcacheready[0]:
1254 1254 # Lazy initialization
1255 1255 fcacheready[0] = True
1256 1256 populate()
1257 1257 return scmutil.match(wctx, fcache.get(rev, []), default='path')
1258 1258
1259 1259 return filematcher
1260 1260
1261 1261 def _makegraphlogrevset(repo, pats, opts, revs):
1262 1262 """Return (expr, filematcher) where expr is a revset string built
1263 1263 from log options and file patterns or None. If --stat or --patch
1264 1264 are not passed filematcher is None. Otherwise it is a callable
1265 1265 taking a revision number and returning a match objects filtering
1266 1266 the files to be detailed when displaying the revision.
1267 1267 """
1268 1268 opt2revset = {
1269 1269 'no_merges': ('not merge()', None),
1270 1270 'only_merges': ('merge()', None),
1271 1271 '_ancestors': ('ancestors(%(val)s)', None),
1272 1272 '_fancestors': ('_firstancestors(%(val)s)', None),
1273 1273 '_descendants': ('descendants(%(val)s)', None),
1274 1274 '_fdescendants': ('_firstdescendants(%(val)s)', None),
1275 1275 '_matchfiles': ('_matchfiles(%(val)s)', None),
1276 1276 'date': ('date(%(val)r)', None),
1277 1277 'branch': ('branch(%(val)r)', ' or '),
1278 1278 '_patslog': ('filelog(%(val)r)', ' or '),
1279 1279 '_patsfollow': ('follow(%(val)r)', ' or '),
1280 1280 '_patsfollowfirst': ('_followfirst(%(val)r)', ' or '),
1281 1281 'keyword': ('keyword(%(val)r)', ' or '),
1282 1282 'prune': ('not (%(val)r or ancestors(%(val)r))', ' and '),
1283 1283 'user': ('user(%(val)r)', ' or '),
1284 1284 }
1285 1285
1286 1286 opts = dict(opts)
1287 1287 # follow or not follow?
1288 1288 follow = opts.get('follow') or opts.get('follow_first')
1289 1289 followfirst = opts.get('follow_first') and 1 or 0
1290 1290 # --follow with FILE behaviour depends on revs...
1291 1291 startrev = revs[0]
1292 1292 followdescendants = (len(revs) > 1 and revs[0] < revs[1]) and 1 or 0
1293 1293
1294 1294 # branch and only_branch are really aliases and must be handled at
1295 1295 # the same time
1296 1296 opts['branch'] = opts.get('branch', []) + opts.get('only_branch', [])
1297 1297 opts['branch'] = [repo.lookupbranch(b) for b in opts['branch']]
1298 1298 # pats/include/exclude are passed to match.match() directly in
1299 1299 # _matchfiles() revset but walkchangerevs() builds its matcher with
1300 1300 # scmutil.match(). The difference is input pats are globbed on
1301 1301 # platforms without shell expansion (windows).
1302 1302 pctx = repo[None]
1303 1303 match, pats = scmutil.matchandpats(pctx, pats, opts)
1304 1304 slowpath = match.anypats() or (match.files() and opts.get('removed'))
1305 1305 if not slowpath:
1306 1306 for f in match.files():
1307 1307 if follow and f not in pctx:
1308 1308 raise util.Abort(_('cannot follow file not in parent '
1309 1309 'revision: "%s"') % f)
1310 1310 filelog = repo.file(f)
1311 1311 if not len(filelog):
1312 1312 # A zero count may be a directory or deleted file, so
1313 1313 # try to find matching entries on the slow path.
1314 1314 if follow:
1315 1315 raise util.Abort(
1316 1316 _('cannot follow nonexistent file: "%s"') % f)
1317 1317 slowpath = True
1318 1318
1319 1319 # We decided to fall back to the slowpath because at least one
1320 1320 # of the paths was not a file. Check to see if at least one of them
1321 1321 # existed in history - in that case, we'll continue down the
1322 1322 # slowpath; otherwise, we can turn off the slowpath
1323 1323 if slowpath:
1324 1324 for path in match.files():
1325 1325 if path == '.' or path in repo.store:
1326 1326 break
1327 1327 else:
1328 1328 slowpath = False
1329 1329
1330 1330 if slowpath:
1331 1331 # See walkchangerevs() slow path.
1332 1332 #
1333 1333 if follow:
1334 1334 raise util.Abort(_('can only follow copies/renames for explicit '
1335 1335 'filenames'))
1336 1336 # pats/include/exclude cannot be represented as separate
1337 1337 # revset expressions as their filtering logic applies at file
1338 1338 # level. For instance "-I a -X a" matches a revision touching
1339 1339 # "a" and "b" while "file(a) and not file(b)" does
1340 1340 # not. Besides, filesets are evaluated against the working
1341 1341 # directory.
1342 1342 matchargs = ['r:', 'd:relpath']
1343 1343 for p in pats:
1344 1344 matchargs.append('p:' + p)
1345 1345 for p in opts.get('include', []):
1346 1346 matchargs.append('i:' + p)
1347 1347 for p in opts.get('exclude', []):
1348 1348 matchargs.append('x:' + p)
1349 1349 matchargs = ','.join(('%r' % p) for p in matchargs)
1350 1350 opts['_matchfiles'] = matchargs
1351 1351 else:
1352 1352 if follow:
1353 1353 fpats = ('_patsfollow', '_patsfollowfirst')
1354 1354 fnopats = (('_ancestors', '_fancestors'),
1355 1355 ('_descendants', '_fdescendants'))
1356 1356 if pats:
1357 1357 # follow() revset interprets its file argument as a
1358 1358 # manifest entry, so use match.files(), not pats.
1359 1359 opts[fpats[followfirst]] = list(match.files())
1360 1360 else:
1361 1361 opts[fnopats[followdescendants][followfirst]] = str(startrev)
1362 1362 else:
1363 1363 opts['_patslog'] = list(pats)
1364 1364
1365 1365 filematcher = None
1366 1366 if opts.get('patch') or opts.get('stat'):
1367 1367 if follow:
1368 1368 filematcher = _makegraphfilematcher(repo, pats, followfirst)
1369 1369 else:
1370 1370 filematcher = lambda rev: match
1371 1371
1372 1372 expr = []
1373 1373 for op, val in opts.iteritems():
1374 1374 if not val:
1375 1375 continue
1376 1376 if op not in opt2revset:
1377 1377 continue
1378 1378 revop, andor = opt2revset[op]
1379 1379 if '%(val)' not in revop:
1380 1380 expr.append(revop)
1381 1381 else:
1382 1382 if not isinstance(val, list):
1383 1383 e = revop % {'val': val}
1384 1384 else:
1385 1385 e = '(' + andor.join((revop % {'val': v}) for v in val) + ')'
1386 1386 expr.append(e)
1387 1387
1388 1388 if expr:
1389 1389 expr = '(' + ' and '.join(expr) + ')'
1390 1390 else:
1391 1391 expr = None
1392 1392 return expr, filematcher
1393 1393
1394 1394 def getgraphlogrevs(repo, pats, opts):
1395 1395 """Return (revs, expr, filematcher) where revs is an iterable of
1396 1396 revision numbers, expr is a revset string built from log options
1397 1397 and file patterns or None, and used to filter 'revs'. If --stat or
1398 1398 --patch are not passed filematcher is None. Otherwise it is a
1399 1399 callable taking a revision number and returning a match objects
1400 1400 filtering the files to be detailed when displaying the revision.
1401 1401 """
1402 1402 def increasingrevs(repo, revs, matcher):
1403 1403 # The sorted input rev sequence is chopped in sub-sequences
1404 1404 # which are sorted in ascending order and passed to the
1405 1405 # matcher. The filtered revs are sorted again as they were in
1406 1406 # the original sub-sequence. This achieve several things:
1407 1407 #
1408 1408 # - getlogrevs() now returns a generator which behaviour is
1409 1409 # adapted to log need. First results come fast, last ones
1410 1410 # are batched for performances.
1411 1411 #
1412 1412 # - revset matchers often operate faster on revision in
1413 1413 # changelog order, because most filters deal with the
1414 1414 # changelog.
1415 1415 #
1416 1416 # - revset matchers can reorder revisions. "A or B" typically
1417 1417 # returns returns the revision matching A then the revision
1418 1418 # matching B. We want to hide this internal implementation
1419 1419 # detail from the caller, and sorting the filtered revision
1420 1420 # again achieves this.
1421 1421 for i, window in increasingwindows(0, len(revs), windowsize=1):
1422 1422 orevs = revs[i:i + window]
1423 1423 nrevs = set(matcher(repo, sorted(orevs)))
1424 1424 for rev in orevs:
1425 1425 if rev in nrevs:
1426 1426 yield rev
1427 1427
1428 1428 if not len(repo):
1429 1429 return iter([]), None, None
1430 1430 # Default --rev value depends on --follow but --follow behaviour
1431 1431 # depends on revisions resolved from --rev...
1432 1432 follow = opts.get('follow') or opts.get('follow_first')
1433 1433 if opts.get('rev'):
1434 1434 revs = scmutil.revrange(repo, opts['rev'])
1435 1435 else:
1436 1436 if follow and len(repo) > 0:
1437 1437 revs = repo.revs('reverse(:.)')
1438 1438 else:
1439 1439 revs = list(repo.changelog)
1440 1440 revs.reverse()
1441 1441 if not revs:
1442 1442 return iter([]), None, None
1443 1443 expr, filematcher = _makegraphlogrevset(repo, pats, opts, revs)
1444 1444 if expr:
1445 1445 matcher = revset.match(repo.ui, expr)
1446 1446 revs = increasingrevs(repo, revs, matcher)
1447 1447 if not opts.get('hidden'):
1448 1448 # --hidden is still experimental and not worth a dedicated revset
1449 1449 # yet. Fortunately, filtering revision number is fast.
1450 1450 revs = (r for r in revs if r not in repo.hiddenrevs)
1451 1451 else:
1452 1452 revs = iter(revs)
1453 1453 return revs, expr, filematcher
1454 1454
1455 1455 def displaygraph(ui, dag, displayer, showparents, edgefn, getrenamed=None,
1456 1456 filematcher=None):
1457 1457 seen, state = [], graphmod.asciistate()
1458 1458 for rev, type, ctx, parents in dag:
1459 1459 char = 'o'
1460 1460 if ctx.node() in showparents:
1461 1461 char = '@'
1462 1462 elif ctx.obsolete():
1463 1463 char = 'x'
1464 1464 copies = None
1465 1465 if getrenamed and ctx.rev():
1466 1466 copies = []
1467 1467 for fn in ctx.files():
1468 1468 rename = getrenamed(fn, ctx.rev())
1469 1469 if rename:
1470 1470 copies.append((fn, rename[0]))
1471 1471 revmatchfn = None
1472 1472 if filematcher is not None:
1473 1473 revmatchfn = filematcher(ctx.rev())
1474 1474 displayer.show(ctx, copies=copies, matchfn=revmatchfn)
1475 1475 lines = displayer.hunk.pop(rev).split('\n')
1476 1476 if not lines[-1]:
1477 1477 del lines[-1]
1478 1478 displayer.flush(rev)
1479 1479 edges = edgefn(type, char, lines, seen, rev, parents)
1480 1480 for type, char, lines, coldata in edges:
1481 1481 graphmod.ascii(ui, state, type, char, lines, coldata)
1482 1482 displayer.close()
1483 1483
1484 1484 def graphlog(ui, repo, *pats, **opts):
1485 1485 # Parameters are identical to log command ones
1486 1486 revs, expr, filematcher = getgraphlogrevs(repo, pats, opts)
1487 1487 revs = sorted(revs, reverse=1)
1488 1488 limit = loglimit(opts)
1489 1489 if limit is not None:
1490 1490 revs = revs[:limit]
1491 1491 revdag = graphmod.dagwalker(repo, revs)
1492 1492
1493 1493 getrenamed = None
1494 1494 if opts.get('copies'):
1495 1495 endrev = None
1496 1496 if opts.get('rev'):
1497 1497 endrev = max(scmutil.revrange(repo, opts.get('rev'))) + 1
1498 1498 getrenamed = templatekw.getrenamedfn(repo, endrev=endrev)
1499 1499 displayer = show_changeset(ui, repo, opts, buffered=True)
1500 1500 showparents = [ctx.node() for ctx in repo[None].parents()]
1501 1501 displaygraph(ui, revdag, displayer, showparents,
1502 1502 graphmod.asciiedges, getrenamed, filematcher)
1503 1503
1504 1504 def checkunsupportedgraphflags(pats, opts):
1505 1505 for op in ["newest_first"]:
1506 1506 if op in opts and opts[op]:
1507 1507 raise util.Abort(_("-G/--graph option is incompatible with --%s")
1508 1508 % op.replace("_", "-"))
1509 1509
1510 1510 def graphrevs(repo, nodes, opts):
1511 1511 limit = loglimit(opts)
1512 1512 nodes.reverse()
1513 1513 if limit is not None:
1514 1514 nodes = nodes[:limit]
1515 1515 return graphmod.nodes(repo, nodes)
1516 1516
1517 1517 def add(ui, repo, match, dryrun, listsubrepos, prefix, explicitonly):
1518 1518 join = lambda f: os.path.join(prefix, f)
1519 1519 bad = []
1520 1520 oldbad = match.bad
1521 1521 match.bad = lambda x, y: bad.append(x) or oldbad(x, y)
1522 1522 names = []
1523 1523 wctx = repo[None]
1524 1524 cca = None
1525 1525 abort, warn = scmutil.checkportabilityalert(ui)
1526 1526 if abort or warn:
1527 1527 cca = scmutil.casecollisionauditor(ui, abort, repo.dirstate)
1528 1528 for f in repo.walk(match):
1529 1529 exact = match.exact(f)
1530 1530 if exact or not explicitonly and f not in repo.dirstate:
1531 1531 if cca:
1532 1532 cca(f)
1533 1533 names.append(f)
1534 1534 if ui.verbose or not exact:
1535 1535 ui.status(_('adding %s\n') % match.rel(join(f)))
1536 1536
1537 1537 for subpath in wctx.substate:
1538 1538 sub = wctx.sub(subpath)
1539 1539 try:
1540 1540 submatch = matchmod.narrowmatcher(subpath, match)
1541 1541 if listsubrepos:
1542 1542 bad.extend(sub.add(ui, submatch, dryrun, listsubrepos, prefix,
1543 1543 False))
1544 1544 else:
1545 1545 bad.extend(sub.add(ui, submatch, dryrun, listsubrepos, prefix,
1546 1546 True))
1547 1547 except error.LookupError:
1548 1548 ui.status(_("skipping missing subrepository: %s\n")
1549 1549 % join(subpath))
1550 1550
1551 1551 if not dryrun:
1552 1552 rejected = wctx.add(names, prefix)
1553 1553 bad.extend(f for f in rejected if f in match.files())
1554 1554 return bad
1555 1555
1556 1556 def forget(ui, repo, match, prefix, explicitonly):
1557 1557 join = lambda f: os.path.join(prefix, f)
1558 1558 bad = []
1559 1559 oldbad = match.bad
1560 1560 match.bad = lambda x, y: bad.append(x) or oldbad(x, y)
1561 1561 wctx = repo[None]
1562 1562 forgot = []
1563 1563 s = repo.status(match=match, clean=True)
1564 1564 forget = sorted(s[0] + s[1] + s[3] + s[6])
1565 1565 if explicitonly:
1566 1566 forget = [f for f in forget if match.exact(f)]
1567 1567
1568 1568 for subpath in wctx.substate:
1569 1569 sub = wctx.sub(subpath)
1570 1570 try:
1571 1571 submatch = matchmod.narrowmatcher(subpath, match)
1572 1572 subbad, subforgot = sub.forget(ui, submatch, prefix)
1573 1573 bad.extend([subpath + '/' + f for f in subbad])
1574 1574 forgot.extend([subpath + '/' + f for f in subforgot])
1575 1575 except error.LookupError:
1576 1576 ui.status(_("skipping missing subrepository: %s\n")
1577 1577 % join(subpath))
1578 1578
1579 1579 if not explicitonly:
1580 1580 for f in match.files():
1581 1581 if f not in repo.dirstate and not os.path.isdir(match.rel(join(f))):
1582 1582 if f not in forgot:
1583 1583 if os.path.exists(match.rel(join(f))):
1584 1584 ui.warn(_('not removing %s: '
1585 1585 'file is already untracked\n')
1586 1586 % match.rel(join(f)))
1587 1587 bad.append(f)
1588 1588
1589 1589 for f in forget:
1590 1590 if ui.verbose or not match.exact(f):
1591 1591 ui.status(_('removing %s\n') % match.rel(join(f)))
1592 1592
1593 1593 rejected = wctx.forget(forget, prefix)
1594 1594 bad.extend(f for f in rejected if f in match.files())
1595 1595 forgot.extend(forget)
1596 1596 return bad, forgot
1597 1597
1598 1598 def duplicatecopies(repo, rev, p1):
1599 1599 "Reproduce copies found in the source revision in the dirstate for grafts"
1600 1600 for dst, src in copies.pathcopies(repo[p1], repo[rev]).iteritems():
1601 1601 repo.dirstate.copy(src, dst)
1602 1602
1603 1603 def commit(ui, repo, commitfunc, pats, opts):
1604 1604 '''commit the specified files or all outstanding changes'''
1605 1605 date = opts.get('date')
1606 1606 if date:
1607 1607 opts['date'] = util.parsedate(date)
1608 1608 message = logmessage(ui, opts)
1609 1609
1610 1610 # extract addremove carefully -- this function can be called from a command
1611 1611 # that doesn't support addremove
1612 1612 if opts.get('addremove'):
1613 1613 scmutil.addremove(repo, pats, opts)
1614 1614
1615 1615 return commitfunc(ui, repo, message,
1616 1616 scmutil.match(repo[None], pats, opts), opts)
1617 1617
1618 1618 def amend(ui, repo, commitfunc, old, extra, pats, opts):
1619 1619 ui.note(_('amending changeset %s\n') % old)
1620 1620 base = old.p1()
1621 1621
1622 1622 wlock = lock = None
1623 1623 try:
1624 1624 wlock = repo.wlock()
1625 1625 lock = repo.lock()
1626 1626 tr = repo.transaction('amend')
1627 1627 try:
1628 1628 # See if we got a message from -m or -l, if not, open the editor
1629 1629 # with the message of the changeset to amend
1630 1630 message = logmessage(ui, opts)
1631 1631 # ensure logfile does not conflict with later enforcement of the
1632 1632 # message. potential logfile content has been processed by
1633 1633 # `logmessage` anyway.
1634 1634 opts.pop('logfile')
1635 1635 # First, do a regular commit to record all changes in the working
1636 1636 # directory (if there are any)
1637 1637 ui.callhooks = False
1638 1638 try:
1639 1639 opts['message'] = 'temporary amend commit for %s' % old
1640 1640 node = commit(ui, repo, commitfunc, pats, opts)
1641 1641 finally:
1642 1642 ui.callhooks = True
1643 1643 ctx = repo[node]
1644 1644
1645 1645 # Participating changesets:
1646 1646 #
1647 1647 # node/ctx o - new (intermediate) commit that contains changes
1648 1648 # | from working dir to go into amending commit
1649 1649 # | (or a workingctx if there were no changes)
1650 1650 # |
1651 1651 # old o - changeset to amend
1652 1652 # |
1653 1653 # base o - parent of amending changeset
1654 1654
1655 1655 # Update extra dict from amended commit (e.g. to preserve graft
1656 1656 # source)
1657 1657 extra.update(old.extra())
1658 1658
1659 1659 # Also update it from the intermediate commit or from the wctx
1660 1660 extra.update(ctx.extra())
1661 1661
1662 1662 files = set(old.files())
1663 1663
1664 1664 # Second, we use either the commit we just did, or if there were no
1665 1665 # changes the parent of the working directory as the version of the
1666 1666 # files in the final amend commit
1667 1667 if node:
1668 1668 ui.note(_('copying changeset %s to %s\n') % (ctx, base))
1669 1669
1670 1670 user = ctx.user()
1671 1671 date = ctx.date()
1672 1672 # Recompute copies (avoid recording a -> b -> a)
1673 1673 copied = copies.pathcopies(base, ctx)
1674 1674
1675 1675 # Prune files which were reverted by the updates: if old
1676 1676 # introduced file X and our intermediate commit, node,
1677 1677 # renamed that file, then those two files are the same and
1678 1678 # we can discard X from our list of files. Likewise if X
1679 1679 # was deleted, it's no longer relevant
1680 1680 files.update(ctx.files())
1681 1681
1682 1682 def samefile(f):
1683 1683 if f in ctx.manifest():
1684 1684 a = ctx.filectx(f)
1685 1685 if f in base.manifest():
1686 1686 b = base.filectx(f)
1687 1687 return (not a.cmp(b)
1688 1688 and a.flags() == b.flags())
1689 1689 else:
1690 1690 return False
1691 1691 else:
1692 1692 return f not in base.manifest()
1693 1693 files = [f for f in files if not samefile(f)]
1694 1694
1695 1695 def filectxfn(repo, ctx_, path):
1696 1696 try:
1697 1697 fctx = ctx[path]
1698 1698 flags = fctx.flags()
1699 1699 mctx = context.memfilectx(fctx.path(), fctx.data(),
1700 1700 islink='l' in flags,
1701 1701 isexec='x' in flags,
1702 1702 copied=copied.get(path))
1703 1703 return mctx
1704 1704 except KeyError:
1705 1705 raise IOError
1706 1706 else:
1707 1707 ui.note(_('copying changeset %s to %s\n') % (old, base))
1708 1708
1709 1709 # Use version of files as in the old cset
1710 1710 def filectxfn(repo, ctx_, path):
1711 1711 try:
1712 1712 return old.filectx(path)
1713 1713 except KeyError:
1714 1714 raise IOError
1715 1715
1716 1716 user = opts.get('user') or old.user()
1717 1717 date = opts.get('date') or old.date()
1718 1718 editmsg = False
1719 1719 if not message:
1720 1720 editmsg = True
1721 1721 message = old.description()
1722 1722
1723 1723 pureextra = extra.copy()
1724 1724 extra['amend_source'] = old.hex()
1725 1725
1726 1726 new = context.memctx(repo,
1727 1727 parents=[base.node(), nullid],
1728 1728 text=message,
1729 1729 files=files,
1730 1730 filectxfn=filectxfn,
1731 1731 user=user,
1732 1732 date=date,
1733 1733 extra=extra)
1734 1734 if editmsg:
1735 1735 new._text = commitforceeditor(repo, new, [])
1736 1736
1737 1737 newdesc = changelog.stripdesc(new.description())
1738 1738 if ((not node)
1739 1739 and newdesc == old.description()
1740 1740 and user == old.user()
1741 1741 and date == old.date()
1742 1742 and pureextra == old.extra()):
1743 1743 # nothing changed. continuing here would create a new node
1744 1744 # anyway because of the amend_source noise.
1745 1745 #
1746 1746 # This not what we expect from amend.
1747 1747 return old.node()
1748 1748
1749 1749 ph = repo.ui.config('phases', 'new-commit', phases.draft)
1750 1750 try:
1751 1751 repo.ui.setconfig('phases', 'new-commit', old.phase())
1752 1752 newid = repo.commitctx(new)
1753 1753 finally:
1754 1754 repo.ui.setconfig('phases', 'new-commit', ph)
1755 1755 if newid != old.node():
1756 1756 # Reroute the working copy parent to the new changeset
1757 1757 repo.setparents(newid, nullid)
1758 1758
1759 1759 # Move bookmarks from old parent to amend commit
1760 1760 bms = repo.nodebookmarks(old.node())
1761 1761 if bms:
1762 1762 for bm in bms:
1763 1763 repo._bookmarks[bm] = newid
1764 1764 bookmarks.write(repo)
1765 1765 #commit the whole amend process
1766 1766 if obsolete._enabled and newid != old.node():
1767 1767 # mark the new changeset as successor of the rewritten one
1768 1768 new = repo[newid]
1769 1769 obs = [(old, (new,))]
1770 1770 if node:
1771 1771 obs.append((ctx, ()))
1772 1772
1773 1773 obsolete.createmarkers(repo, obs)
1774 1774 tr.close()
1775 1775 finally:
1776 1776 tr.release()
1777 1777 if (not obsolete._enabled) and newid != old.node():
1778 1778 # Strip the intermediate commit (if there was one) and the amended
1779 1779 # commit
1780 1780 if node:
1781 1781 ui.note(_('stripping intermediate changeset %s\n') % ctx)
1782 1782 ui.note(_('stripping amended changeset %s\n') % old)
1783 1783 repair.strip(ui, repo, old.node(), topic='amend-backup')
1784 1784 finally:
1785 1785 lockmod.release(wlock, lock)
1786 1786 return newid
1787 1787
1788 1788 def commiteditor(repo, ctx, subs):
1789 1789 if ctx.description():
1790 1790 return ctx.description()
1791 1791 return commitforceeditor(repo, ctx, subs)
1792 1792
1793 1793 def commitforceeditor(repo, ctx, subs):
1794 1794 edittext = []
1795 1795 modified, added, removed = ctx.modified(), ctx.added(), ctx.removed()
1796 1796 if ctx.description():
1797 1797 edittext.append(ctx.description())
1798 1798 edittext.append("")
1799 1799 edittext.append("") # Empty line between message and comments.
1800 1800 edittext.append(_("HG: Enter commit message."
1801 1801 " Lines beginning with 'HG:' are removed."))
1802 1802 edittext.append(_("HG: Leave message empty to abort commit."))
1803 1803 edittext.append("HG: --")
1804 1804 edittext.append(_("HG: user: %s") % ctx.user())
1805 1805 if ctx.p2():
1806 1806 edittext.append(_("HG: branch merge"))
1807 1807 if ctx.branch():
1808 1808 edittext.append(_("HG: branch '%s'") % ctx.branch())
1809 1809 edittext.extend([_("HG: subrepo %s") % s for s in subs])
1810 1810 edittext.extend([_("HG: added %s") % f for f in added])
1811 1811 edittext.extend([_("HG: changed %s") % f for f in modified])
1812 1812 edittext.extend([_("HG: removed %s") % f for f in removed])
1813 1813 if not added and not modified and not removed:
1814 1814 edittext.append(_("HG: no files changed"))
1815 1815 edittext.append("")
1816 1816 # run editor in the repository root
1817 1817 olddir = os.getcwd()
1818 1818 os.chdir(repo.root)
1819 1819 text = repo.ui.edit("\n".join(edittext), ctx.user())
1820 1820 text = re.sub("(?m)^HG:.*(\n|$)", "", text)
1821 1821 os.chdir(olddir)
1822 1822
1823 1823 if not text.strip():
1824 1824 raise util.Abort(_("empty commit message"))
1825 1825
1826 1826 return text
1827 1827
1828 1828 def revert(ui, repo, ctx, parents, *pats, **opts):
1829 1829 parent, p2 = parents
1830 1830 node = ctx.node()
1831 1831
1832 1832 mf = ctx.manifest()
1833 1833 if node == parent:
1834 1834 pmf = mf
1835 1835 else:
1836 1836 pmf = None
1837 1837
1838 1838 # need all matching names in dirstate and manifest of target rev,
1839 1839 # so have to walk both. do not print errors if files exist in one
1840 1840 # but not other.
1841 1841
1842 1842 names = {}
1843 1843
1844 1844 wlock = repo.wlock()
1845 1845 try:
1846 1846 # walk dirstate.
1847 1847
1848 1848 m = scmutil.match(repo[None], pats, opts)
1849 1849 m.bad = lambda x, y: False
1850 1850 for abs in repo.walk(m):
1851 1851 names[abs] = m.rel(abs), m.exact(abs)
1852 1852
1853 1853 # walk target manifest.
1854 1854
1855 1855 def badfn(path, msg):
1856 1856 if path in names:
1857 1857 return
1858 1858 if path in ctx.substate:
1859 1859 return
1860 1860 path_ = path + '/'
1861 1861 for f in names:
1862 1862 if f.startswith(path_):
1863 1863 return
1864 1864 ui.warn("%s: %s\n" % (m.rel(path), msg))
1865 1865
1866 1866 m = scmutil.match(ctx, pats, opts)
1867 1867 m.bad = badfn
1868 1868 for abs in ctx.walk(m):
1869 1869 if abs not in names:
1870 1870 names[abs] = m.rel(abs), m.exact(abs)
1871 1871
1872 1872 # get the list of subrepos that must be reverted
1873 1873 targetsubs = [s for s in ctx.substate if m(s)]
1874 1874 m = scmutil.matchfiles(repo, names)
1875 1875 changes = repo.status(match=m)[:4]
1876 1876 modified, added, removed, deleted = map(set, changes)
1877 1877
1878 1878 # if f is a rename, also revert the source
1879 1879 cwd = repo.getcwd()
1880 1880 for f in added:
1881 1881 src = repo.dirstate.copied(f)
1882 1882 if src and src not in names and repo.dirstate[src] == 'r':
1883 1883 removed.add(src)
1884 1884 names[src] = (repo.pathto(src, cwd), True)
1885 1885
1886 1886 def removeforget(abs):
1887 1887 if repo.dirstate[abs] == 'a':
1888 1888 return _('forgetting %s\n')
1889 1889 return _('removing %s\n')
1890 1890
1891 1891 revert = ([], _('reverting %s\n'))
1892 1892 add = ([], _('adding %s\n'))
1893 1893 remove = ([], removeforget)
1894 1894 undelete = ([], _('undeleting %s\n'))
1895 1895
1896 1896 disptable = (
1897 1897 # dispatch table:
1898 1898 # file state
1899 1899 # action if in target manifest
1900 1900 # action if not in target manifest
1901 1901 # make backup if in target manifest
1902 1902 # make backup if not in target manifest
1903 1903 (modified, revert, remove, True, True),
1904 1904 (added, revert, remove, True, False),
1905 1905 (removed, undelete, None, False, False),
1906 1906 (deleted, revert, remove, False, False),
1907 1907 )
1908 1908
1909 1909 for abs, (rel, exact) in sorted(names.items()):
1910 1910 mfentry = mf.get(abs)
1911 1911 target = repo.wjoin(abs)
1912 1912 def handle(xlist, dobackup):
1913 1913 xlist[0].append(abs)
1914 1914 if (dobackup and not opts.get('no_backup') and
1915 1915 os.path.lexists(target)):
1916 1916 bakname = "%s.orig" % rel
1917 1917 ui.note(_('saving current version of %s as %s\n') %
1918 1918 (rel, bakname))
1919 1919 if not opts.get('dry_run'):
1920 1920 util.rename(target, bakname)
1921 1921 if ui.verbose or not exact:
1922 1922 msg = xlist[1]
1923 1923 if not isinstance(msg, basestring):
1924 1924 msg = msg(abs)
1925 1925 ui.status(msg % rel)
1926 1926 for table, hitlist, misslist, backuphit, backupmiss in disptable:
1927 1927 if abs not in table:
1928 1928 continue
1929 1929 # file has changed in dirstate
1930 1930 if mfentry:
1931 1931 handle(hitlist, backuphit)
1932 1932 elif misslist is not None:
1933 1933 handle(misslist, backupmiss)
1934 1934 break
1935 1935 else:
1936 1936 if abs not in repo.dirstate:
1937 1937 if mfentry:
1938 1938 handle(add, True)
1939 1939 elif exact:
1940 1940 ui.warn(_('file not managed: %s\n') % rel)
1941 1941 continue
1942 1942 # file has not changed in dirstate
1943 1943 if node == parent:
1944 1944 if exact:
1945 1945 ui.warn(_('no changes needed to %s\n') % rel)
1946 1946 continue
1947 1947 if pmf is None:
1948 1948 # only need parent manifest in this unlikely case,
1949 1949 # so do not read by default
1950 1950 pmf = repo[parent].manifest()
1951 1951 if abs in pmf and mfentry:
1952 1952 # if version of file is same in parent and target
1953 1953 # manifests, do nothing
1954 1954 if (pmf[abs] != mfentry or
1955 1955 pmf.flags(abs) != mf.flags(abs)):
1956 1956 handle(revert, False)
1957 1957 else:
1958 1958 handle(remove, False)
1959 1959
1960 1960 if not opts.get('dry_run'):
1961 1961 def checkout(f):
1962 1962 fc = ctx[f]
1963 1963 repo.wwrite(f, fc.data(), fc.flags())
1964 1964
1965 1965 audit_path = scmutil.pathauditor(repo.root)
1966 1966 for f in remove[0]:
1967 1967 if repo.dirstate[f] == 'a':
1968 1968 repo.dirstate.drop(f)
1969 1969 continue
1970 1970 audit_path(f)
1971 1971 try:
1972 1972 util.unlinkpath(repo.wjoin(f))
1973 1973 except OSError:
1974 1974 pass
1975 1975 repo.dirstate.remove(f)
1976 1976
1977 1977 normal = None
1978 1978 if node == parent:
1979 1979 # We're reverting to our parent. If possible, we'd like status
1980 1980 # to report the file as clean. We have to use normallookup for
1981 1981 # merges to avoid losing information about merged/dirty files.
1982 1982 if p2 != nullid:
1983 1983 normal = repo.dirstate.normallookup
1984 1984 else:
1985 1985 normal = repo.dirstate.normal
1986 1986 for f in revert[0]:
1987 1987 checkout(f)
1988 1988 if normal:
1989 1989 normal(f)
1990 1990
1991 1991 for f in add[0]:
1992 1992 checkout(f)
1993 1993 repo.dirstate.add(f)
1994 1994
1995 1995 normal = repo.dirstate.normallookup
1996 1996 if node == parent and p2 == nullid:
1997 1997 normal = repo.dirstate.normal
1998 1998 for f in undelete[0]:
1999 1999 checkout(f)
2000 2000 normal(f)
2001 2001
2002 2002 if targetsubs:
2003 2003 # Revert the subrepos on the revert list
2004 2004 for sub in targetsubs:
2005 2005 ctx.sub(sub).revert(ui, ctx.substate[sub], *pats, **opts)
2006 2006 finally:
2007 2007 wlock.release()
2008 2008
2009 2009 def command(table):
2010 2010 '''returns a function object bound to table which can be used as
2011 2011 a decorator for populating table as a command table'''
2012 2012
2013 2013 def cmd(name, options, synopsis=None):
2014 2014 def decorator(func):
2015 2015 if synopsis:
2016 2016 table[name] = func, options[:], synopsis
2017 2017 else:
2018 2018 table[name] = func, options[:]
2019 2019 return func
2020 2020 return decorator
2021 2021
2022 2022 return cmd
@@ -1,1281 +1,1281 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, match as matchmod
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 i, l in enumerate(ctx['.hgsubstate'].data().splitlines()):
47 47 l = l.lstrip()
48 48 if not l:
49 49 continue
50 50 try:
51 51 revision, path = l.split(" ", 1)
52 52 except ValueError:
53 53 raise util.Abort(_("invalid subrepository revision "
54 54 "specifier in .hgsubstate line %d")
55 55 % (i + 1))
56 56 rev[path] = revision
57 57 except IOError, err:
58 58 if err.errno != errno.ENOENT:
59 59 raise
60 60
61 61 def remap(src):
62 62 for pattern, repl in p.items('subpaths'):
63 63 # Turn r'C:\foo\bar' into r'C:\\foo\\bar' since re.sub
64 64 # does a string decode.
65 65 repl = repl.encode('string-escape')
66 66 # However, we still want to allow back references to go
67 67 # through unharmed, so we turn r'\\1' into r'\1'. Again,
68 68 # extra escapes are needed because re.sub string decodes.
69 69 repl = re.sub(r'\\\\([0-9]+)', r'\\\1', repl)
70 70 try:
71 71 src = re.sub(pattern, repl, src, 1)
72 72 except re.error, e:
73 73 raise util.Abort(_("bad subrepository pattern in %s: %s")
74 74 % (p.source('subpaths', pattern), e))
75 75 return src
76 76
77 77 state = {}
78 78 for path, src in p[''].items():
79 79 kind = 'hg'
80 80 if src.startswith('['):
81 81 if ']' not in src:
82 82 raise util.Abort(_('missing ] in subrepo source'))
83 83 kind, src = src.split(']', 1)
84 84 kind = kind[1:]
85 85 src = src.lstrip() # strip any extra whitespace after ']'
86 86
87 87 if not util.url(src).isabs():
88 88 parent = _abssource(ctx._repo, abort=False)
89 89 if parent:
90 90 parent = util.url(parent)
91 91 parent.path = posixpath.join(parent.path or '', src)
92 92 parent.path = posixpath.normpath(parent.path)
93 93 joined = str(parent)
94 94 # Remap the full joined path and use it if it changes,
95 95 # else remap the original source.
96 96 remapped = remap(joined)
97 97 if remapped == joined:
98 98 src = remap(src)
99 99 else:
100 100 src = remapped
101 101
102 102 src = remap(src)
103 103 state[util.pconvert(path)] = (src.strip(), rev.get(path, ''), kind)
104 104
105 105 return state
106 106
107 107 def writestate(repo, state):
108 108 """rewrite .hgsubstate in (outer) repo with these subrepo states"""
109 109 lines = ['%s %s\n' % (state[s][1], s) for s in sorted(state)]
110 110 repo.wwrite('.hgsubstate', ''.join(lines), '')
111 111
112 112 def submerge(repo, wctx, mctx, actx, overwrite):
113 113 """delegated from merge.applyupdates: merging of .hgsubstate file
114 114 in working context, merging context and ancestor context"""
115 115 if mctx == actx: # backwards?
116 116 actx = wctx.p1()
117 117 s1 = wctx.substate
118 118 s2 = mctx.substate
119 119 sa = actx.substate
120 120 sm = {}
121 121
122 122 repo.ui.debug("subrepo merge %s %s %s\n" % (wctx, mctx, actx))
123 123
124 124 def debug(s, msg, r=""):
125 125 if r:
126 126 r = "%s:%s:%s" % r
127 127 repo.ui.debug(" subrepo %s: %s %s\n" % (s, msg, r))
128 128
129 129 for s, l in s1.items():
130 130 a = sa.get(s, nullstate)
131 131 ld = l # local state with possible dirty flag for compares
132 132 if wctx.sub(s).dirty():
133 133 ld = (l[0], l[1] + "+")
134 134 if wctx == actx: # overwrite
135 135 a = ld
136 136
137 137 if s in s2:
138 138 r = s2[s]
139 139 if ld == r or r == a: # no change or local is newer
140 140 sm[s] = l
141 141 continue
142 142 elif ld == a: # other side changed
143 143 debug(s, "other changed, get", r)
144 144 wctx.sub(s).get(r, overwrite)
145 145 sm[s] = r
146 146 elif ld[0] != r[0]: # sources differ
147 147 if repo.ui.promptchoice(
148 148 _(' subrepository sources for %s differ\n'
149 149 'use (l)ocal source (%s) or (r)emote source (%s)?')
150 150 % (s, l[0], r[0]),
151 151 (_('&Local'), _('&Remote')), 0):
152 152 debug(s, "prompt changed, get", r)
153 153 wctx.sub(s).get(r, overwrite)
154 154 sm[s] = r
155 155 elif ld[1] == a[1]: # local side is unchanged
156 156 debug(s, "other side changed, get", r)
157 157 wctx.sub(s).get(r, overwrite)
158 158 sm[s] = r
159 159 else:
160 160 debug(s, "both sides changed, merge with", r)
161 161 wctx.sub(s).merge(r)
162 162 sm[s] = l
163 163 elif ld == a: # remote removed, local unchanged
164 164 debug(s, "remote removed, remove")
165 165 wctx.sub(s).remove()
166 166 elif a == nullstate: # not present in remote or ancestor
167 167 debug(s, "local added, keep")
168 168 sm[s] = l
169 169 continue
170 170 else:
171 171 if repo.ui.promptchoice(
172 172 _(' local changed subrepository %s which remote removed\n'
173 173 'use (c)hanged version or (d)elete?') % s,
174 174 (_('&Changed'), _('&Delete')), 0):
175 175 debug(s, "prompt remove")
176 176 wctx.sub(s).remove()
177 177
178 178 for s, r in sorted(s2.items()):
179 179 if s in s1:
180 180 continue
181 181 elif s not in sa:
182 182 debug(s, "remote added, get", r)
183 183 mctx.sub(s).get(r)
184 184 sm[s] = r
185 185 elif r != sa[s]:
186 186 if repo.ui.promptchoice(
187 187 _(' remote changed subrepository %s which local removed\n'
188 188 'use (c)hanged version or (d)elete?') % s,
189 189 (_('&Changed'), _('&Delete')), 0) == 0:
190 190 debug(s, "prompt recreate", r)
191 191 wctx.sub(s).get(r)
192 192 sm[s] = r
193 193
194 194 # record merged .hgsubstate
195 195 writestate(repo, sm)
196 196
197 197 def _updateprompt(ui, sub, dirty, local, remote):
198 198 if dirty:
199 199 msg = (_(' subrepository sources for %s differ\n'
200 200 'use (l)ocal source (%s) or (r)emote source (%s)?\n')
201 201 % (subrelpath(sub), local, remote))
202 202 else:
203 203 msg = (_(' subrepository sources for %s differ (in checked out '
204 204 'version)\n'
205 205 'use (l)ocal source (%s) or (r)emote source (%s)?\n')
206 206 % (subrelpath(sub), local, remote))
207 207 return ui.promptchoice(msg, (_('&Local'), _('&Remote')), 0)
208 208
209 209 def reporelpath(repo):
210 210 """return path to this (sub)repo as seen from outermost repo"""
211 211 parent = repo
212 212 while util.safehasattr(parent, '_subparent'):
213 213 parent = parent._subparent
214 214 p = parent.root.rstrip(os.sep)
215 215 return repo.root[len(p) + 1:]
216 216
217 217 def subrelpath(sub):
218 218 """return path to this subrepo as seen from outermost repo"""
219 219 if util.safehasattr(sub, '_relpath'):
220 220 return sub._relpath
221 221 if not util.safehasattr(sub, '_repo'):
222 222 return sub._path
223 223 return reporelpath(sub._repo)
224 224
225 225 def _abssource(repo, push=False, abort=True):
226 226 """return pull/push path of repo - either based on parent repo .hgsub info
227 227 or on the top repo config. Abort or return None if no source found."""
228 228 if util.safehasattr(repo, '_subparent'):
229 229 source = util.url(repo._subsource)
230 230 if source.isabs():
231 231 return str(source)
232 232 source.path = posixpath.normpath(source.path)
233 233 parent = _abssource(repo._subparent, push, abort=False)
234 234 if parent:
235 235 parent = util.url(util.pconvert(parent))
236 236 parent.path = posixpath.join(parent.path or '', source.path)
237 237 parent.path = posixpath.normpath(parent.path)
238 238 return str(parent)
239 239 else: # recursion reached top repo
240 240 if util.safehasattr(repo, '_subtoppath'):
241 241 return repo._subtoppath
242 242 if push and repo.ui.config('paths', 'default-push'):
243 243 return repo.ui.config('paths', 'default-push')
244 244 if repo.ui.config('paths', 'default'):
245 245 return repo.ui.config('paths', 'default')
246 246 if abort:
247 247 raise util.Abort(_("default path for subrepository %s not found") %
248 248 reporelpath(repo))
249 249
250 250 def itersubrepos(ctx1, ctx2):
251 251 """find subrepos in ctx1 or ctx2"""
252 252 # Create a (subpath, ctx) mapping where we prefer subpaths from
253 253 # ctx1. The subpaths from ctx2 are important when the .hgsub file
254 254 # has been modified (in ctx2) but not yet committed (in ctx1).
255 255 subpaths = dict.fromkeys(ctx2.substate, ctx2)
256 256 subpaths.update(dict.fromkeys(ctx1.substate, ctx1))
257 257 for subpath, ctx in sorted(subpaths.iteritems()):
258 258 yield subpath, ctx.sub(subpath)
259 259
260 260 def subrepo(ctx, path):
261 261 """return instance of the right subrepo class for subrepo in path"""
262 262 # subrepo inherently violates our import layering rules
263 263 # because it wants to make repo objects from deep inside the stack
264 264 # so we manually delay the circular imports to not break
265 265 # scripts that don't use our demand-loading
266 266 global hg
267 267 import hg as h
268 268 hg = h
269 269
270 270 scmutil.pathauditor(ctx._repo.root)(path)
271 271 state = ctx.substate[path]
272 272 if state[2] not in types:
273 273 raise util.Abort(_('unknown subrepo type %s') % state[2])
274 274 return types[state[2]](ctx, path, state[:2])
275 275
276 276 # subrepo classes need to implement the following abstract class:
277 277
278 278 class abstractsubrepo(object):
279 279
280 280 def dirty(self, ignoreupdate=False):
281 281 """returns true if the dirstate of the subrepo is dirty or does not
282 282 match current stored state. If ignoreupdate is true, only check
283 283 whether the subrepo has uncommitted changes in its dirstate.
284 284 """
285 285 raise NotImplementedError
286 286
287 287 def basestate(self):
288 288 """current working directory base state, disregarding .hgsubstate
289 289 state and working directory modifications"""
290 290 raise NotImplementedError
291 291
292 292 def checknested(self, path):
293 293 """check if path is a subrepository within this repository"""
294 294 return False
295 295
296 296 def commit(self, text, user, date):
297 297 """commit the current changes to the subrepo with the given
298 298 log message. Use given user and date if possible. Return the
299 299 new state of the subrepo.
300 300 """
301 301 raise NotImplementedError
302 302
303 303 def remove(self):
304 304 """remove the subrepo
305 305
306 306 (should verify the dirstate is not dirty first)
307 307 """
308 308 raise NotImplementedError
309 309
310 310 def get(self, state, overwrite=False):
311 311 """run whatever commands are needed to put the subrepo into
312 312 this state
313 313 """
314 314 raise NotImplementedError
315 315
316 316 def merge(self, state):
317 317 """merge currently-saved state with the new state."""
318 318 raise NotImplementedError
319 319
320 320 def push(self, opts):
321 321 """perform whatever action is analogous to 'hg push'
322 322
323 323 This may be a no-op on some systems.
324 324 """
325 325 raise NotImplementedError
326 326
327 327 def add(self, ui, match, dryrun, listsubrepos, prefix, explicitonly):
328 328 return []
329 329
330 330 def status(self, rev2, **opts):
331 331 return [], [], [], [], [], [], []
332 332
333 def diff(self, diffopts, node2, match, prefix, **opts):
333 def diff(self, ui, diffopts, node2, match, prefix, **opts):
334 334 pass
335 335
336 336 def outgoing(self, ui, dest, opts):
337 337 return 1
338 338
339 339 def incoming(self, ui, source, opts):
340 340 return 1
341 341
342 342 def files(self):
343 343 """return filename iterator"""
344 344 raise NotImplementedError
345 345
346 346 def filedata(self, name):
347 347 """return file data"""
348 348 raise NotImplementedError
349 349
350 350 def fileflags(self, name):
351 351 """return file flags"""
352 352 return ''
353 353
354 354 def archive(self, ui, archiver, prefix, match=None):
355 355 if match is not None:
356 356 files = [f for f in self.files() if match(f)]
357 357 else:
358 358 files = self.files()
359 359 total = len(files)
360 360 relpath = subrelpath(self)
361 361 ui.progress(_('archiving (%s)') % relpath, 0,
362 362 unit=_('files'), total=total)
363 363 for i, name in enumerate(files):
364 364 flags = self.fileflags(name)
365 365 mode = 'x' in flags and 0755 or 0644
366 366 symlink = 'l' in flags
367 367 archiver.addfile(os.path.join(prefix, self._path, name),
368 368 mode, symlink, self.filedata(name))
369 369 ui.progress(_('archiving (%s)') % relpath, i + 1,
370 370 unit=_('files'), total=total)
371 371 ui.progress(_('archiving (%s)') % relpath, None)
372 372
373 373 def walk(self, match):
374 374 '''
375 375 walk recursively through the directory tree, finding all files
376 376 matched by the match function
377 377 '''
378 378 pass
379 379
380 380 def forget(self, ui, match, prefix):
381 381 return ([], [])
382 382
383 383 def revert(self, ui, substate, *pats, **opts):
384 384 ui.warn('%s: reverting %s subrepos is unsupported\n' \
385 385 % (substate[0], substate[2]))
386 386 return []
387 387
388 388 class hgsubrepo(abstractsubrepo):
389 389 def __init__(self, ctx, path, state):
390 390 self._path = path
391 391 self._state = state
392 392 r = ctx._repo
393 393 root = r.wjoin(path)
394 394 create = False
395 395 if not os.path.exists(os.path.join(root, '.hg')):
396 396 create = True
397 397 util.makedirs(root)
398 398 self._repo = hg.repository(r.baseui, root, create=create)
399 399 for s, k in [('ui', 'commitsubrepos')]:
400 400 v = r.ui.config(s, k)
401 401 if v:
402 402 self._repo.ui.setconfig(s, k, v)
403 403 self._initrepo(r, state[0], create)
404 404
405 405 def _initrepo(self, parentrepo, source, create):
406 406 self._repo._subparent = parentrepo
407 407 self._repo._subsource = source
408 408
409 409 if create:
410 410 fp = self._repo.opener("hgrc", "w", text=True)
411 411 fp.write('[paths]\n')
412 412
413 413 def addpathconfig(key, value):
414 414 if value:
415 415 fp.write('%s = %s\n' % (key, value))
416 416 self._repo.ui.setconfig('paths', key, value)
417 417
418 418 defpath = _abssource(self._repo, abort=False)
419 419 defpushpath = _abssource(self._repo, True, abort=False)
420 420 addpathconfig('default', defpath)
421 421 if defpath != defpushpath:
422 422 addpathconfig('default-push', defpushpath)
423 423 fp.close()
424 424
425 425 def add(self, ui, match, dryrun, listsubrepos, prefix, explicitonly):
426 426 return cmdutil.add(ui, self._repo, match, dryrun, listsubrepos,
427 427 os.path.join(prefix, self._path), explicitonly)
428 428
429 429 def status(self, rev2, **opts):
430 430 try:
431 431 rev1 = self._state[1]
432 432 ctx1 = self._repo[rev1]
433 433 ctx2 = self._repo[rev2]
434 434 return self._repo.status(ctx1, ctx2, **opts)
435 435 except error.RepoLookupError, inst:
436 436 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
437 437 % (inst, subrelpath(self)))
438 438 return [], [], [], [], [], [], []
439 439
440 def diff(self, diffopts, node2, match, prefix, **opts):
440 def diff(self, ui, diffopts, node2, match, prefix, **opts):
441 441 try:
442 442 node1 = node.bin(self._state[1])
443 443 # We currently expect node2 to come from substate and be
444 444 # in hex format
445 445 if node2 is not None:
446 446 node2 = node.bin(node2)
447 cmdutil.diffordiffstat(self._repo.ui, self._repo, diffopts,
447 cmdutil.diffordiffstat(ui, self._repo, diffopts,
448 448 node1, node2, match,
449 449 prefix=os.path.join(prefix, self._path),
450 450 listsubrepos=True, **opts)
451 451 except error.RepoLookupError, inst:
452 452 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
453 453 % (inst, subrelpath(self)))
454 454
455 455 def archive(self, ui, archiver, prefix, match=None):
456 456 self._get(self._state + ('hg',))
457 457 abstractsubrepo.archive(self, ui, archiver, prefix, match)
458 458
459 459 rev = self._state[1]
460 460 ctx = self._repo[rev]
461 461 for subpath in ctx.substate:
462 462 s = subrepo(ctx, subpath)
463 463 submatch = matchmod.narrowmatcher(subpath, match)
464 464 s.archive(ui, archiver, os.path.join(prefix, self._path), submatch)
465 465
466 466 def dirty(self, ignoreupdate=False):
467 467 r = self._state[1]
468 468 if r == '' and not ignoreupdate: # no state recorded
469 469 return True
470 470 w = self._repo[None]
471 471 if r != w.p1().hex() and not ignoreupdate:
472 472 # different version checked out
473 473 return True
474 474 return w.dirty() # working directory changed
475 475
476 476 def basestate(self):
477 477 return self._repo['.'].hex()
478 478
479 479 def checknested(self, path):
480 480 return self._repo._checknested(self._repo.wjoin(path))
481 481
482 482 def commit(self, text, user, date):
483 483 # don't bother committing in the subrepo if it's only been
484 484 # updated
485 485 if not self.dirty(True):
486 486 return self._repo['.'].hex()
487 487 self._repo.ui.debug("committing subrepo %s\n" % subrelpath(self))
488 488 n = self._repo.commit(text, user, date)
489 489 if not n:
490 490 return self._repo['.'].hex() # different version checked out
491 491 return node.hex(n)
492 492
493 493 def remove(self):
494 494 # we can't fully delete the repository as it may contain
495 495 # local-only history
496 496 self._repo.ui.note(_('removing subrepo %s\n') % subrelpath(self))
497 497 hg.clean(self._repo, node.nullid, False)
498 498
499 499 def _get(self, state):
500 500 source, revision, kind = state
501 501 if revision not in self._repo:
502 502 self._repo._subsource = source
503 503 srcurl = _abssource(self._repo)
504 504 other = hg.peer(self._repo, {}, srcurl)
505 505 if len(self._repo) == 0:
506 506 self._repo.ui.status(_('cloning subrepo %s from %s\n')
507 507 % (subrelpath(self), srcurl))
508 508 parentrepo = self._repo._subparent
509 509 shutil.rmtree(self._repo.path)
510 510 other, cloned = hg.clone(self._repo._subparent.baseui, {},
511 511 other, self._repo.root,
512 512 update=False)
513 513 self._repo = cloned.local()
514 514 self._initrepo(parentrepo, source, create=True)
515 515 else:
516 516 self._repo.ui.status(_('pulling subrepo %s from %s\n')
517 517 % (subrelpath(self), srcurl))
518 518 self._repo.pull(other)
519 519 bookmarks.updatefromremote(self._repo.ui, self._repo, other,
520 520 srcurl)
521 521
522 522 def get(self, state, overwrite=False):
523 523 self._get(state)
524 524 source, revision, kind = state
525 525 self._repo.ui.debug("getting subrepo %s\n" % self._path)
526 526 hg.updaterepo(self._repo, revision, overwrite)
527 527
528 528 def merge(self, state):
529 529 self._get(state)
530 530 cur = self._repo['.']
531 531 dst = self._repo[state[1]]
532 532 anc = dst.ancestor(cur)
533 533
534 534 def mergefunc():
535 535 if anc == cur and dst.branch() == cur.branch():
536 536 self._repo.ui.debug("updating subrepo %s\n" % subrelpath(self))
537 537 hg.update(self._repo, state[1])
538 538 elif anc == dst:
539 539 self._repo.ui.debug("skipping subrepo %s\n" % subrelpath(self))
540 540 else:
541 541 self._repo.ui.debug("merging subrepo %s\n" % subrelpath(self))
542 542 hg.merge(self._repo, state[1], remind=False)
543 543
544 544 wctx = self._repo[None]
545 545 if self.dirty():
546 546 if anc != dst:
547 547 if _updateprompt(self._repo.ui, self, wctx.dirty(), cur, dst):
548 548 mergefunc()
549 549 else:
550 550 mergefunc()
551 551 else:
552 552 mergefunc()
553 553
554 554 def push(self, opts):
555 555 force = opts.get('force')
556 556 newbranch = opts.get('new_branch')
557 557 ssh = opts.get('ssh')
558 558
559 559 # push subrepos depth-first for coherent ordering
560 560 c = self._repo['']
561 561 subs = c.substate # only repos that are committed
562 562 for s in sorted(subs):
563 563 if c.sub(s).push(opts) == 0:
564 564 return False
565 565
566 566 dsturl = _abssource(self._repo, True)
567 567 self._repo.ui.status(_('pushing subrepo %s to %s\n') %
568 568 (subrelpath(self), dsturl))
569 569 other = hg.peer(self._repo, {'ssh': ssh}, dsturl)
570 570 return self._repo.push(other, force, newbranch=newbranch)
571 571
572 572 def outgoing(self, ui, dest, opts):
573 573 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
574 574
575 575 def incoming(self, ui, source, opts):
576 576 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
577 577
578 578 def files(self):
579 579 rev = self._state[1]
580 580 ctx = self._repo[rev]
581 581 return ctx.manifest()
582 582
583 583 def filedata(self, name):
584 584 rev = self._state[1]
585 585 return self._repo[rev][name].data()
586 586
587 587 def fileflags(self, name):
588 588 rev = self._state[1]
589 589 ctx = self._repo[rev]
590 590 return ctx.flags(name)
591 591
592 592 def walk(self, match):
593 593 ctx = self._repo[None]
594 594 return ctx.walk(match)
595 595
596 596 def forget(self, ui, match, prefix):
597 597 return cmdutil.forget(ui, self._repo, match,
598 598 os.path.join(prefix, self._path), True)
599 599
600 600 def revert(self, ui, substate, *pats, **opts):
601 601 # reverting a subrepo is a 2 step process:
602 602 # 1. if the no_backup is not set, revert all modified
603 603 # files inside the subrepo
604 604 # 2. update the subrepo to the revision specified in
605 605 # the corresponding substate dictionary
606 606 ui.status(_('reverting subrepo %s\n') % substate[0])
607 607 if not opts.get('no_backup'):
608 608 # Revert all files on the subrepo, creating backups
609 609 # Note that this will not recursively revert subrepos
610 610 # We could do it if there was a set:subrepos() predicate
611 611 opts = opts.copy()
612 612 opts['date'] = None
613 613 opts['rev'] = substate[1]
614 614
615 615 pats = []
616 616 if not opts['all']:
617 617 pats = ['set:modified()']
618 618 self.filerevert(ui, *pats, **opts)
619 619
620 620 # Update the repo to the revision specified in the given substate
621 621 self.get(substate, overwrite=True)
622 622
623 623 def filerevert(self, ui, *pats, **opts):
624 624 ctx = self._repo[opts['rev']]
625 625 parents = self._repo.dirstate.parents()
626 626 if opts['all']:
627 627 pats = ['set:modified()']
628 628 else:
629 629 pats = []
630 630 cmdutil.revert(ui, self._repo, ctx, parents, *pats, **opts)
631 631
632 632 class svnsubrepo(abstractsubrepo):
633 633 def __init__(self, ctx, path, state):
634 634 self._path = path
635 635 self._state = state
636 636 self._ctx = ctx
637 637 self._ui = ctx._repo.ui
638 638 self._exe = util.findexe('svn')
639 639 if not self._exe:
640 640 raise util.Abort(_("'svn' executable not found for subrepo '%s'")
641 641 % self._path)
642 642
643 643 def _svncommand(self, commands, filename='', failok=False):
644 644 cmd = [self._exe]
645 645 extrakw = {}
646 646 if not self._ui.interactive():
647 647 # Making stdin be a pipe should prevent svn from behaving
648 648 # interactively even if we can't pass --non-interactive.
649 649 extrakw['stdin'] = subprocess.PIPE
650 650 # Starting in svn 1.5 --non-interactive is a global flag
651 651 # instead of being per-command, but we need to support 1.4 so
652 652 # we have to be intelligent about what commands take
653 653 # --non-interactive.
654 654 if commands[0] in ('update', 'checkout', 'commit'):
655 655 cmd.append('--non-interactive')
656 656 cmd.extend(commands)
657 657 if filename is not None:
658 658 path = os.path.join(self._ctx._repo.origroot, self._path, filename)
659 659 cmd.append(path)
660 660 env = dict(os.environ)
661 661 # Avoid localized output, preserve current locale for everything else.
662 662 lc_all = env.get('LC_ALL')
663 663 if lc_all:
664 664 env['LANG'] = lc_all
665 665 del env['LC_ALL']
666 666 env['LC_MESSAGES'] = 'C'
667 667 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
668 668 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
669 669 universal_newlines=True, env=env, **extrakw)
670 670 stdout, stderr = p.communicate()
671 671 stderr = stderr.strip()
672 672 if not failok:
673 673 if p.returncode:
674 674 raise util.Abort(stderr or 'exited with code %d' % p.returncode)
675 675 if stderr:
676 676 self._ui.warn(stderr + '\n')
677 677 return stdout, stderr
678 678
679 679 @propertycache
680 680 def _svnversion(self):
681 681 output, err = self._svncommand(['--version', '--quiet'], filename=None)
682 682 m = re.search(r'^(\d+)\.(\d+)', output)
683 683 if not m:
684 684 raise util.Abort(_('cannot retrieve svn tool version'))
685 685 return (int(m.group(1)), int(m.group(2)))
686 686
687 687 def _wcrevs(self):
688 688 # Get the working directory revision as well as the last
689 689 # commit revision so we can compare the subrepo state with
690 690 # both. We used to store the working directory one.
691 691 output, err = self._svncommand(['info', '--xml'])
692 692 doc = xml.dom.minidom.parseString(output)
693 693 entries = doc.getElementsByTagName('entry')
694 694 lastrev, rev = '0', '0'
695 695 if entries:
696 696 rev = str(entries[0].getAttribute('revision')) or '0'
697 697 commits = entries[0].getElementsByTagName('commit')
698 698 if commits:
699 699 lastrev = str(commits[0].getAttribute('revision')) or '0'
700 700 return (lastrev, rev)
701 701
702 702 def _wcrev(self):
703 703 return self._wcrevs()[0]
704 704
705 705 def _wcchanged(self):
706 706 """Return (changes, extchanges, missing) where changes is True
707 707 if the working directory was changed, extchanges is
708 708 True if any of these changes concern an external entry and missing
709 709 is True if any change is a missing entry.
710 710 """
711 711 output, err = self._svncommand(['status', '--xml'])
712 712 externals, changes, missing = [], [], []
713 713 doc = xml.dom.minidom.parseString(output)
714 714 for e in doc.getElementsByTagName('entry'):
715 715 s = e.getElementsByTagName('wc-status')
716 716 if not s:
717 717 continue
718 718 item = s[0].getAttribute('item')
719 719 props = s[0].getAttribute('props')
720 720 path = e.getAttribute('path')
721 721 if item == 'external':
722 722 externals.append(path)
723 723 elif item == 'missing':
724 724 missing.append(path)
725 725 if (item not in ('', 'normal', 'unversioned', 'external')
726 726 or props not in ('', 'none', 'normal')):
727 727 changes.append(path)
728 728 for path in changes:
729 729 for ext in externals:
730 730 if path == ext or path.startswith(ext + os.sep):
731 731 return True, True, bool(missing)
732 732 return bool(changes), False, bool(missing)
733 733
734 734 def dirty(self, ignoreupdate=False):
735 735 if not self._wcchanged()[0]:
736 736 if self._state[1] in self._wcrevs() or ignoreupdate:
737 737 return False
738 738 return True
739 739
740 740 def basestate(self):
741 741 lastrev, rev = self._wcrevs()
742 742 if lastrev != rev:
743 743 # Last committed rev is not the same than rev. We would
744 744 # like to take lastrev but we do not know if the subrepo
745 745 # URL exists at lastrev. Test it and fallback to rev it
746 746 # is not there.
747 747 try:
748 748 self._svncommand(['list', '%s@%s' % (self._state[0], lastrev)])
749 749 return lastrev
750 750 except error.Abort:
751 751 pass
752 752 return rev
753 753
754 754 def commit(self, text, user, date):
755 755 # user and date are out of our hands since svn is centralized
756 756 changed, extchanged, missing = self._wcchanged()
757 757 if not changed:
758 758 return self.basestate()
759 759 if extchanged:
760 760 # Do not try to commit externals
761 761 raise util.Abort(_('cannot commit svn externals'))
762 762 if missing:
763 763 # svn can commit with missing entries but aborting like hg
764 764 # seems a better approach.
765 765 raise util.Abort(_('cannot commit missing svn entries'))
766 766 commitinfo, err = self._svncommand(['commit', '-m', text])
767 767 self._ui.status(commitinfo)
768 768 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
769 769 if not newrev:
770 770 if not commitinfo.strip():
771 771 # Sometimes, our definition of "changed" differs from
772 772 # svn one. For instance, svn ignores missing files
773 773 # when committing. If there are only missing files, no
774 774 # commit is made, no output and no error code.
775 775 raise util.Abort(_('failed to commit svn changes'))
776 776 raise util.Abort(commitinfo.splitlines()[-1])
777 777 newrev = newrev.groups()[0]
778 778 self._ui.status(self._svncommand(['update', '-r', newrev])[0])
779 779 return newrev
780 780
781 781 def remove(self):
782 782 if self.dirty():
783 783 self._ui.warn(_('not removing repo %s because '
784 784 'it has changes.\n' % self._path))
785 785 return
786 786 self._ui.note(_('removing subrepo %s\n') % self._path)
787 787
788 788 def onerror(function, path, excinfo):
789 789 if function is not os.remove:
790 790 raise
791 791 # read-only files cannot be unlinked under Windows
792 792 s = os.stat(path)
793 793 if (s.st_mode & stat.S_IWRITE) != 0:
794 794 raise
795 795 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
796 796 os.remove(path)
797 797
798 798 path = self._ctx._repo.wjoin(self._path)
799 799 shutil.rmtree(path, onerror=onerror)
800 800 try:
801 801 os.removedirs(os.path.dirname(path))
802 802 except OSError:
803 803 pass
804 804
805 805 def get(self, state, overwrite=False):
806 806 if overwrite:
807 807 self._svncommand(['revert', '--recursive'])
808 808 args = ['checkout']
809 809 if self._svnversion >= (1, 5):
810 810 args.append('--force')
811 811 # The revision must be specified at the end of the URL to properly
812 812 # update to a directory which has since been deleted and recreated.
813 813 args.append('%s@%s' % (state[0], state[1]))
814 814 status, err = self._svncommand(args, failok=True)
815 815 if not re.search('Checked out revision [0-9]+.', status):
816 816 if ('is already a working copy for a different URL' in err
817 817 and (self._wcchanged()[:2] == (False, False))):
818 818 # obstructed but clean working copy, so just blow it away.
819 819 self.remove()
820 820 self.get(state, overwrite=False)
821 821 return
822 822 raise util.Abort((status or err).splitlines()[-1])
823 823 self._ui.status(status)
824 824
825 825 def merge(self, state):
826 826 old = self._state[1]
827 827 new = state[1]
828 828 wcrev = self._wcrev()
829 829 if new != wcrev:
830 830 dirty = old == wcrev or self._wcchanged()[0]
831 831 if _updateprompt(self._ui, self, dirty, wcrev, new):
832 832 self.get(state, False)
833 833
834 834 def push(self, opts):
835 835 # push is a no-op for SVN
836 836 return True
837 837
838 838 def files(self):
839 839 output = self._svncommand(['list', '--recursive', '--xml'])[0]
840 840 doc = xml.dom.minidom.parseString(output)
841 841 paths = []
842 842 for e in doc.getElementsByTagName('entry'):
843 843 kind = str(e.getAttribute('kind'))
844 844 if kind != 'file':
845 845 continue
846 846 name = ''.join(c.data for c
847 847 in e.getElementsByTagName('name')[0].childNodes
848 848 if c.nodeType == c.TEXT_NODE)
849 849 paths.append(name.encode('utf-8'))
850 850 return paths
851 851
852 852 def filedata(self, name):
853 853 return self._svncommand(['cat'], name)[0]
854 854
855 855
856 856 class gitsubrepo(abstractsubrepo):
857 857 def __init__(self, ctx, path, state):
858 858 self._state = state
859 859 self._ctx = ctx
860 860 self._path = path
861 861 self._relpath = os.path.join(reporelpath(ctx._repo), path)
862 862 self._abspath = ctx._repo.wjoin(path)
863 863 self._subparent = ctx._repo
864 864 self._ui = ctx._repo.ui
865 865 self._ensuregit()
866 866
867 867 def _ensuregit(self):
868 868 try:
869 869 self._gitexecutable = 'git'
870 870 out, err = self._gitnodir(['--version'])
871 871 except OSError, e:
872 872 if e.errno != 2 or os.name != 'nt':
873 873 raise
874 874 self._gitexecutable = 'git.cmd'
875 875 out, err = self._gitnodir(['--version'])
876 876 m = re.search(r'^git version (\d+)\.(\d+)\.(\d+)', out)
877 877 if not m:
878 878 self._ui.warn(_('cannot retrieve git version'))
879 879 return
880 880 version = (int(m.group(1)), m.group(2), m.group(3))
881 881 # git 1.4.0 can't work at all, but 1.5.X can in at least some cases,
882 882 # despite the docstring comment. For now, error on 1.4.0, warn on
883 883 # 1.5.0 but attempt to continue.
884 884 if version < (1, 5, 0):
885 885 raise util.Abort(_('git subrepo requires at least 1.6.0 or later'))
886 886 elif version < (1, 6, 0):
887 887 self._ui.warn(_('git subrepo requires at least 1.6.0 or later'))
888 888
889 889 def _gitcommand(self, commands, env=None, stream=False):
890 890 return self._gitdir(commands, env=env, stream=stream)[0]
891 891
892 892 def _gitdir(self, commands, env=None, stream=False):
893 893 return self._gitnodir(commands, env=env, stream=stream,
894 894 cwd=self._abspath)
895 895
896 896 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
897 897 """Calls the git command
898 898
899 899 The methods tries to call the git command. versions prior to 1.6.0
900 900 are not supported and very probably fail.
901 901 """
902 902 self._ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
903 903 # unless ui.quiet is set, print git's stderr,
904 904 # which is mostly progress and useful info
905 905 errpipe = None
906 906 if self._ui.quiet:
907 907 errpipe = open(os.devnull, 'w')
908 908 p = subprocess.Popen([self._gitexecutable] + commands, bufsize=-1,
909 909 cwd=cwd, env=env, close_fds=util.closefds,
910 910 stdout=subprocess.PIPE, stderr=errpipe)
911 911 if stream:
912 912 return p.stdout, None
913 913
914 914 retdata = p.stdout.read().strip()
915 915 # wait for the child to exit to avoid race condition.
916 916 p.wait()
917 917
918 918 if p.returncode != 0 and p.returncode != 1:
919 919 # there are certain error codes that are ok
920 920 command = commands[0]
921 921 if command in ('cat-file', 'symbolic-ref'):
922 922 return retdata, p.returncode
923 923 # for all others, abort
924 924 raise util.Abort('git %s error %d in %s' %
925 925 (command, p.returncode, self._relpath))
926 926
927 927 return retdata, p.returncode
928 928
929 929 def _gitmissing(self):
930 930 return not os.path.exists(os.path.join(self._abspath, '.git'))
931 931
932 932 def _gitstate(self):
933 933 return self._gitcommand(['rev-parse', 'HEAD'])
934 934
935 935 def _gitcurrentbranch(self):
936 936 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
937 937 if err:
938 938 current = None
939 939 return current
940 940
941 941 def _gitremote(self, remote):
942 942 out = self._gitcommand(['remote', 'show', '-n', remote])
943 943 line = out.split('\n')[1]
944 944 i = line.index('URL: ') + len('URL: ')
945 945 return line[i:]
946 946
947 947 def _githavelocally(self, revision):
948 948 out, code = self._gitdir(['cat-file', '-e', revision])
949 949 return code == 0
950 950
951 951 def _gitisancestor(self, r1, r2):
952 952 base = self._gitcommand(['merge-base', r1, r2])
953 953 return base == r1
954 954
955 955 def _gitisbare(self):
956 956 return self._gitcommand(['config', '--bool', 'core.bare']) == 'true'
957 957
958 958 def _gitupdatestat(self):
959 959 """This must be run before git diff-index.
960 960 diff-index only looks at changes to file stat;
961 961 this command looks at file contents and updates the stat."""
962 962 self._gitcommand(['update-index', '-q', '--refresh'])
963 963
964 964 def _gitbranchmap(self):
965 965 '''returns 2 things:
966 966 a map from git branch to revision
967 967 a map from revision to branches'''
968 968 branch2rev = {}
969 969 rev2branch = {}
970 970
971 971 out = self._gitcommand(['for-each-ref', '--format',
972 972 '%(objectname) %(refname)'])
973 973 for line in out.split('\n'):
974 974 revision, ref = line.split(' ')
975 975 if (not ref.startswith('refs/heads/') and
976 976 not ref.startswith('refs/remotes/')):
977 977 continue
978 978 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
979 979 continue # ignore remote/HEAD redirects
980 980 branch2rev[ref] = revision
981 981 rev2branch.setdefault(revision, []).append(ref)
982 982 return branch2rev, rev2branch
983 983
984 984 def _gittracking(self, branches):
985 985 'return map of remote branch to local tracking branch'
986 986 # assumes no more than one local tracking branch for each remote
987 987 tracking = {}
988 988 for b in branches:
989 989 if b.startswith('refs/remotes/'):
990 990 continue
991 991 bname = b.split('/', 2)[2]
992 992 remote = self._gitcommand(['config', 'branch.%s.remote' % bname])
993 993 if remote:
994 994 ref = self._gitcommand(['config', 'branch.%s.merge' % bname])
995 995 tracking['refs/remotes/%s/%s' %
996 996 (remote, ref.split('/', 2)[2])] = b
997 997 return tracking
998 998
999 999 def _abssource(self, source):
1000 1000 if '://' not in source:
1001 1001 # recognize the scp syntax as an absolute source
1002 1002 colon = source.find(':')
1003 1003 if colon != -1 and '/' not in source[:colon]:
1004 1004 return source
1005 1005 self._subsource = source
1006 1006 return _abssource(self)
1007 1007
1008 1008 def _fetch(self, source, revision):
1009 1009 if self._gitmissing():
1010 1010 source = self._abssource(source)
1011 1011 self._ui.status(_('cloning subrepo %s from %s\n') %
1012 1012 (self._relpath, source))
1013 1013 self._gitnodir(['clone', source, self._abspath])
1014 1014 if self._githavelocally(revision):
1015 1015 return
1016 1016 self._ui.status(_('pulling subrepo %s from %s\n') %
1017 1017 (self._relpath, self._gitremote('origin')))
1018 1018 # try only origin: the originally cloned repo
1019 1019 self._gitcommand(['fetch'])
1020 1020 if not self._githavelocally(revision):
1021 1021 raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
1022 1022 (revision, self._relpath))
1023 1023
1024 1024 def dirty(self, ignoreupdate=False):
1025 1025 if self._gitmissing():
1026 1026 return self._state[1] != ''
1027 1027 if self._gitisbare():
1028 1028 return True
1029 1029 if not ignoreupdate and self._state[1] != self._gitstate():
1030 1030 # different version checked out
1031 1031 return True
1032 1032 # check for staged changes or modified files; ignore untracked files
1033 1033 self._gitupdatestat()
1034 1034 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1035 1035 return code == 1
1036 1036
1037 1037 def basestate(self):
1038 1038 return self._gitstate()
1039 1039
1040 1040 def get(self, state, overwrite=False):
1041 1041 source, revision, kind = state
1042 1042 if not revision:
1043 1043 self.remove()
1044 1044 return
1045 1045 self._fetch(source, revision)
1046 1046 # if the repo was set to be bare, unbare it
1047 1047 if self._gitisbare():
1048 1048 self._gitcommand(['config', 'core.bare', 'false'])
1049 1049 if self._gitstate() == revision:
1050 1050 self._gitcommand(['reset', '--hard', 'HEAD'])
1051 1051 return
1052 1052 elif self._gitstate() == revision:
1053 1053 if overwrite:
1054 1054 # first reset the index to unmark new files for commit, because
1055 1055 # reset --hard will otherwise throw away files added for commit,
1056 1056 # not just unmark them.
1057 1057 self._gitcommand(['reset', 'HEAD'])
1058 1058 self._gitcommand(['reset', '--hard', 'HEAD'])
1059 1059 return
1060 1060 branch2rev, rev2branch = self._gitbranchmap()
1061 1061
1062 1062 def checkout(args):
1063 1063 cmd = ['checkout']
1064 1064 if overwrite:
1065 1065 # first reset the index to unmark new files for commit, because
1066 1066 # the -f option will otherwise throw away files added for
1067 1067 # commit, not just unmark them.
1068 1068 self._gitcommand(['reset', 'HEAD'])
1069 1069 cmd.append('-f')
1070 1070 self._gitcommand(cmd + args)
1071 1071
1072 1072 def rawcheckout():
1073 1073 # no branch to checkout, check it out with no branch
1074 1074 self._ui.warn(_('checking out detached HEAD in subrepo %s\n') %
1075 1075 self._relpath)
1076 1076 self._ui.warn(_('check out a git branch if you intend '
1077 1077 'to make changes\n'))
1078 1078 checkout(['-q', revision])
1079 1079
1080 1080 if revision not in rev2branch:
1081 1081 rawcheckout()
1082 1082 return
1083 1083 branches = rev2branch[revision]
1084 1084 firstlocalbranch = None
1085 1085 for b in branches:
1086 1086 if b == 'refs/heads/master':
1087 1087 # master trumps all other branches
1088 1088 checkout(['refs/heads/master'])
1089 1089 return
1090 1090 if not firstlocalbranch and not b.startswith('refs/remotes/'):
1091 1091 firstlocalbranch = b
1092 1092 if firstlocalbranch:
1093 1093 checkout([firstlocalbranch])
1094 1094 return
1095 1095
1096 1096 tracking = self._gittracking(branch2rev.keys())
1097 1097 # choose a remote branch already tracked if possible
1098 1098 remote = branches[0]
1099 1099 if remote not in tracking:
1100 1100 for b in branches:
1101 1101 if b in tracking:
1102 1102 remote = b
1103 1103 break
1104 1104
1105 1105 if remote not in tracking:
1106 1106 # create a new local tracking branch
1107 1107 local = remote.split('/', 2)[2]
1108 1108 checkout(['-b', local, remote])
1109 1109 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
1110 1110 # When updating to a tracked remote branch,
1111 1111 # if the local tracking branch is downstream of it,
1112 1112 # a normal `git pull` would have performed a "fast-forward merge"
1113 1113 # which is equivalent to updating the local branch to the remote.
1114 1114 # Since we are only looking at branching at update, we need to
1115 1115 # detect this situation and perform this action lazily.
1116 1116 if tracking[remote] != self._gitcurrentbranch():
1117 1117 checkout([tracking[remote]])
1118 1118 self._gitcommand(['merge', '--ff', remote])
1119 1119 else:
1120 1120 # a real merge would be required, just checkout the revision
1121 1121 rawcheckout()
1122 1122
1123 1123 def commit(self, text, user, date):
1124 1124 if self._gitmissing():
1125 1125 raise util.Abort(_("subrepo %s is missing") % self._relpath)
1126 1126 cmd = ['commit', '-a', '-m', text]
1127 1127 env = os.environ.copy()
1128 1128 if user:
1129 1129 cmd += ['--author', user]
1130 1130 if date:
1131 1131 # git's date parser silently ignores when seconds < 1e9
1132 1132 # convert to ISO8601
1133 1133 env['GIT_AUTHOR_DATE'] = util.datestr(date,
1134 1134 '%Y-%m-%dT%H:%M:%S %1%2')
1135 1135 self._gitcommand(cmd, env=env)
1136 1136 # make sure commit works otherwise HEAD might not exist under certain
1137 1137 # circumstances
1138 1138 return self._gitstate()
1139 1139
1140 1140 def merge(self, state):
1141 1141 source, revision, kind = state
1142 1142 self._fetch(source, revision)
1143 1143 base = self._gitcommand(['merge-base', revision, self._state[1]])
1144 1144 self._gitupdatestat()
1145 1145 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1146 1146
1147 1147 def mergefunc():
1148 1148 if base == revision:
1149 1149 self.get(state) # fast forward merge
1150 1150 elif base != self._state[1]:
1151 1151 self._gitcommand(['merge', '--no-commit', revision])
1152 1152
1153 1153 if self.dirty():
1154 1154 if self._gitstate() != revision:
1155 1155 dirty = self._gitstate() == self._state[1] or code != 0
1156 1156 if _updateprompt(self._ui, self, dirty,
1157 1157 self._state[1][:7], revision[:7]):
1158 1158 mergefunc()
1159 1159 else:
1160 1160 mergefunc()
1161 1161
1162 1162 def push(self, opts):
1163 1163 force = opts.get('force')
1164 1164
1165 1165 if not self._state[1]:
1166 1166 return True
1167 1167 if self._gitmissing():
1168 1168 raise util.Abort(_("subrepo %s is missing") % self._relpath)
1169 1169 # if a branch in origin contains the revision, nothing to do
1170 1170 branch2rev, rev2branch = self._gitbranchmap()
1171 1171 if self._state[1] in rev2branch:
1172 1172 for b in rev2branch[self._state[1]]:
1173 1173 if b.startswith('refs/remotes/origin/'):
1174 1174 return True
1175 1175 for b, revision in branch2rev.iteritems():
1176 1176 if b.startswith('refs/remotes/origin/'):
1177 1177 if self._gitisancestor(self._state[1], revision):
1178 1178 return True
1179 1179 # otherwise, try to push the currently checked out branch
1180 1180 cmd = ['push']
1181 1181 if force:
1182 1182 cmd.append('--force')
1183 1183
1184 1184 current = self._gitcurrentbranch()
1185 1185 if current:
1186 1186 # determine if the current branch is even useful
1187 1187 if not self._gitisancestor(self._state[1], current):
1188 1188 self._ui.warn(_('unrelated git branch checked out '
1189 1189 'in subrepo %s\n') % self._relpath)
1190 1190 return False
1191 1191 self._ui.status(_('pushing branch %s of subrepo %s\n') %
1192 1192 (current.split('/', 2)[2], self._relpath))
1193 1193 self._gitcommand(cmd + ['origin', current])
1194 1194 return True
1195 1195 else:
1196 1196 self._ui.warn(_('no branch checked out in subrepo %s\n'
1197 1197 'cannot push revision %s\n') %
1198 1198 (self._relpath, self._state[1]))
1199 1199 return False
1200 1200
1201 1201 def remove(self):
1202 1202 if self._gitmissing():
1203 1203 return
1204 1204 if self.dirty():
1205 1205 self._ui.warn(_('not removing repo %s because '
1206 1206 'it has changes.\n') % self._relpath)
1207 1207 return
1208 1208 # we can't fully delete the repository as it may contain
1209 1209 # local-only history
1210 1210 self._ui.note(_('removing subrepo %s\n') % self._relpath)
1211 1211 self._gitcommand(['config', 'core.bare', 'true'])
1212 1212 for f in os.listdir(self._abspath):
1213 1213 if f == '.git':
1214 1214 continue
1215 1215 path = os.path.join(self._abspath, f)
1216 1216 if os.path.isdir(path) and not os.path.islink(path):
1217 1217 shutil.rmtree(path)
1218 1218 else:
1219 1219 os.remove(path)
1220 1220
1221 1221 def archive(self, ui, archiver, prefix, match=None):
1222 1222 source, revision = self._state
1223 1223 if not revision:
1224 1224 return
1225 1225 self._fetch(source, revision)
1226 1226
1227 1227 # Parse git's native archive command.
1228 1228 # This should be much faster than manually traversing the trees
1229 1229 # and objects with many subprocess calls.
1230 1230 tarstream = self._gitcommand(['archive', revision], stream=True)
1231 1231 tar = tarfile.open(fileobj=tarstream, mode='r|')
1232 1232 relpath = subrelpath(self)
1233 1233 ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
1234 1234 for i, info in enumerate(tar):
1235 1235 if info.isdir():
1236 1236 continue
1237 1237 if match and not match(info.name):
1238 1238 continue
1239 1239 if info.issym():
1240 1240 data = info.linkname
1241 1241 else:
1242 1242 data = tar.extractfile(info).read()
1243 1243 archiver.addfile(os.path.join(prefix, self._path, info.name),
1244 1244 info.mode, info.issym(), data)
1245 1245 ui.progress(_('archiving (%s)') % relpath, i + 1,
1246 1246 unit=_('files'))
1247 1247 ui.progress(_('archiving (%s)') % relpath, None)
1248 1248
1249 1249
1250 1250 def status(self, rev2, **opts):
1251 1251 rev1 = self._state[1]
1252 1252 if self._gitmissing() or not rev1:
1253 1253 # if the repo is missing, return no results
1254 1254 return [], [], [], [], [], [], []
1255 1255 modified, added, removed = [], [], []
1256 1256 self._gitupdatestat()
1257 1257 if rev2:
1258 1258 command = ['diff-tree', rev1, rev2]
1259 1259 else:
1260 1260 command = ['diff-index', rev1]
1261 1261 out = self._gitcommand(command)
1262 1262 for line in out.split('\n'):
1263 1263 tab = line.find('\t')
1264 1264 if tab == -1:
1265 1265 continue
1266 1266 status, f = line[tab - 1], line[tab + 1:]
1267 1267 if status == 'M':
1268 1268 modified.append(f)
1269 1269 elif status == 'A':
1270 1270 added.append(f)
1271 1271 elif status == 'D':
1272 1272 removed.append(f)
1273 1273
1274 1274 deleted = unknown = ignored = clean = []
1275 1275 return modified, added, removed, deleted, unknown, ignored, clean
1276 1276
1277 1277 types = {
1278 1278 'hg': hgsubrepo,
1279 1279 'svn': svnsubrepo,
1280 1280 'git': gitsubrepo,
1281 1281 }
@@ -1,130 +1,162 b''
1 1 Setup
2 2
3 3 $ echo "[color]" >> $HGRCPATH
4 4 $ echo "mode = ansi" >> $HGRCPATH
5 5 $ echo "[extensions]" >> $HGRCPATH
6 6 $ echo "color=" >> $HGRCPATH
7 7 $ hg init repo
8 8 $ cd repo
9 9 $ cat > a <<EOF
10 10 > c
11 11 > c
12 12 > a
13 13 > a
14 14 > b
15 15 > a
16 16 > a
17 17 > c
18 18 > c
19 19 > EOF
20 20 $ hg ci -Am adda
21 21 adding a
22 22 $ cat > a <<EOF
23 23 > c
24 24 > c
25 25 > a
26 26 > a
27 27 > dd
28 28 > a
29 29 > a
30 30 > c
31 31 > c
32 32 > EOF
33 33
34 34 default context
35 35
36 36 $ hg diff --nodates --color=always
37 37 \x1b[0;1mdiff -r cf9f4ba66af2 a\x1b[0m (esc)
38 38 \x1b[0;31;1m--- a/a\x1b[0m (esc)
39 39 \x1b[0;32;1m+++ b/a\x1b[0m (esc)
40 40 \x1b[0;35m@@ -2,7 +2,7 @@\x1b[0m (esc)
41 41 c
42 42 a
43 43 a
44 44 \x1b[0;31m-b\x1b[0m (esc)
45 45 \x1b[0;32m+dd\x1b[0m (esc)
46 46 a
47 47 a
48 48 c
49 49
50 50 --unified=2
51 51
52 52 $ hg diff --nodates -U 2 --color=always
53 53 \x1b[0;1mdiff -r cf9f4ba66af2 a\x1b[0m (esc)
54 54 \x1b[0;31;1m--- a/a\x1b[0m (esc)
55 55 \x1b[0;32;1m+++ b/a\x1b[0m (esc)
56 56 \x1b[0;35m@@ -3,5 +3,5 @@\x1b[0m (esc)
57 57 a
58 58 a
59 59 \x1b[0;31m-b\x1b[0m (esc)
60 60 \x1b[0;32m+dd\x1b[0m (esc)
61 61 a
62 62 a
63 63
64 64 diffstat
65 65
66 66 $ hg diff --stat --color=always
67 67 a | 2 \x1b[0;32m+\x1b[0m\x1b[0;31m-\x1b[0m (esc)
68 68 1 files changed, 1 insertions(+), 1 deletions(-)
69 69 $ echo "record=" >> $HGRCPATH
70 70 $ echo "[ui]" >> $HGRCPATH
71 71 $ echo "interactive=true" >> $HGRCPATH
72 72 $ echo "[diff]" >> $HGRCPATH
73 73 $ echo "git=True" >> $HGRCPATH
74 74
75 75 #if execbit
76 76
77 77 record
78 78
79 79 $ chmod +x a
80 80 $ hg record --color=always -m moda a <<EOF
81 81 > y
82 82 > y
83 83 > EOF
84 84 \x1b[0;1mdiff --git a/a b/a\x1b[0m (esc)
85 85 \x1b[0;36;1mold mode 100644\x1b[0m (esc)
86 86 \x1b[0;36;1mnew mode 100755\x1b[0m (esc)
87 87 1 hunks, 1 lines changed
88 88 \x1b[0;33mexamine changes to 'a'? [Ynesfdaq?]\x1b[0m (esc)
89 89 \x1b[0;35m@@ -2,7 +2,7 @@\x1b[0m (esc)
90 90 c
91 91 a
92 92 a
93 93 \x1b[0;31m-b\x1b[0m (esc)
94 94 \x1b[0;32m+dd\x1b[0m (esc)
95 95 a
96 96 a
97 97 c
98 98 \x1b[0;33mrecord this change to 'a'? [Ynesfdaq?]\x1b[0m (esc)
99 99
100 100 $ echo "[extensions]" >> $HGRCPATH
101 101 $ echo "mq=" >> $HGRCPATH
102 102 $ hg rollback
103 103 repository tip rolled back to revision 0 (undo commit)
104 104 working directory now based on revision 0
105 105
106 106 qrecord
107 107
108 108 $ hg qrecord --color=always -m moda patch <<EOF
109 109 > y
110 110 > y
111 111 > EOF
112 112 \x1b[0;1mdiff --git a/a b/a\x1b[0m (esc)
113 113 \x1b[0;36;1mold mode 100644\x1b[0m (esc)
114 114 \x1b[0;36;1mnew mode 100755\x1b[0m (esc)
115 115 1 hunks, 1 lines changed
116 116 \x1b[0;33mexamine changes to 'a'? [Ynesfdaq?]\x1b[0m (esc)
117 117 \x1b[0;35m@@ -2,7 +2,7 @@\x1b[0m (esc)
118 118 c
119 119 a
120 120 a
121 121 \x1b[0;31m-b\x1b[0m (esc)
122 122 \x1b[0;32m+dd\x1b[0m (esc)
123 123 a
124 124 a
125 125 c
126 126 \x1b[0;33mrecord this change to 'a'? [Ynesfdaq?]\x1b[0m (esc)
127 127
128 $ hg qpop -a
129 popping patch
130 patch queue now empty
131
128 132 #endif
129 133
134 issue3712: test colorization of subrepo diff
135
136 $ hg init sub
137 $ echo b > sub/b
138 $ hg -R sub commit -Am 'create sub'
139 adding b
140 $ echo 'sub = sub' > .hgsub
141 $ hg add .hgsub
142 $ hg commit -m 'add subrepo sub'
143 $ echo aa >> a
144 $ echo bb >> sub/b
145
146 $ hg diff --color=always -S
147 \x1b[0;1mdiff --git a/a b/a\x1b[0m (esc)
148 \x1b[0;31;1m--- a/a\x1b[0m (esc)
149 \x1b[0;32;1m+++ b/a\x1b[0m (esc)
150 \x1b[0;35m@@ -7,3 +7,4 @@\x1b[0m (esc)
151 a
152 c
153 c
154 \x1b[0;32m+aa\x1b[0m (esc)
155 \x1b[0;1mdiff --git a/sub/b b/sub/b\x1b[0m (esc)
156 \x1b[0;31;1m--- a/sub/b\x1b[0m (esc)
157 \x1b[0;32;1m+++ b/sub/b\x1b[0m (esc)
158 \x1b[0;35m@@ -1,1 +1,2 @@\x1b[0m (esc)
159 b
160 \x1b[0;32m+bb\x1b[0m (esc)
161
130 162 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now