##// END OF EJS Templates
log: add an extension hook-point in changeset_printer...
Boris Feld -
r33155:4ecc6047 default
parent child Browse files
Show More
@@ -0,0 +1,56
1 Test hg log changeset printer external hook
2 -------------------------------------------
3
4 $ cat > $TESTTMP/logexthook.py <<EOF
5 > from mercurial import repair, commands
6 > from mercurial import cmdutil
7 > def rot13description(self, ctx):
8 > summary = "summary".encode('rot13')
9 > description = ctx.description().strip().splitlines()[0].encode('rot13')
10 > self.ui.write("%s: %s\n" % (summary, description))
11 > def reposetup(ui, repo):
12 > cmdutil.changeset_printer._exthook = rot13description
13 > EOF
14
15 Prepare the repository
16
17 $ hg init empty
18 $ cd empty
19 $ touch ROOT
20 $ hg commit -A -m "Root" ROOT
21
22 $ touch a b c
23 $ hg commit -A -m "Add A, B, C" a b c
24
25 Check the log
26
27 $ hg log --config extensions.t=$TESTTMP/logexthook.py
28 changeset: 1:70fc82b23320
29 tag: tip
30 user: test
31 date: Thu Jan 01 00:00:00 1970 +0000
32 fhzznel: Nqq N, O, P
33 summary: Add A, B, C
34
35 changeset: 0:b00443a54871
36 user: test
37 date: Thu Jan 01 00:00:00 1970 +0000
38 fhzznel: Ebbg
39 summary: Root
40
41 Check that exthook is working with graph log too
42
43 $ hg log -G --config extensions.t=$TESTTMP/logexthook.py
44 @ changeset: 1:70fc82b23320
45 | tag: tip
46 | user: test
47 | date: Thu Jan 01 00:00:00 1970 +0000
48 | fhzznel: Nqq N, O, P
49 | summary: Add A, B, C
50 |
51 o changeset: 0:b00443a54871
52 user: test
53 date: Thu Jan 01 00:00:00 1970 +0000
54 fhzznel: Ebbg
55 summary: Root
56
@@ -1,3602 +1,3609
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 __future__ import absolute_import
9 9
10 10 import errno
11 11 import itertools
12 12 import os
13 13 import re
14 14 import tempfile
15 15
16 16 from .i18n import _
17 17 from .node import (
18 18 hex,
19 19 nullid,
20 20 nullrev,
21 21 short,
22 22 )
23 23
24 24 from . import (
25 25 bookmarks,
26 26 changelog,
27 27 copies,
28 28 crecord as crecordmod,
29 29 encoding,
30 30 error,
31 31 formatter,
32 32 graphmod,
33 33 match as matchmod,
34 34 obsolete,
35 35 patch,
36 36 pathutil,
37 37 phases,
38 38 pycompat,
39 39 registrar,
40 40 repair,
41 41 revlog,
42 42 revset,
43 43 scmutil,
44 44 smartset,
45 45 templatekw,
46 46 templater,
47 47 util,
48 48 vfs as vfsmod,
49 49 )
50 50 stringio = util.stringio
51 51
52 52 # templates of common command options
53 53
54 54 dryrunopts = [
55 55 ('n', 'dry-run', None,
56 56 _('do not perform actions, just print output')),
57 57 ]
58 58
59 59 remoteopts = [
60 60 ('e', 'ssh', '',
61 61 _('specify ssh command to use'), _('CMD')),
62 62 ('', 'remotecmd', '',
63 63 _('specify hg command to run on the remote side'), _('CMD')),
64 64 ('', 'insecure', None,
65 65 _('do not verify server certificate (ignoring web.cacerts config)')),
66 66 ]
67 67
68 68 walkopts = [
69 69 ('I', 'include', [],
70 70 _('include names matching the given patterns'), _('PATTERN')),
71 71 ('X', 'exclude', [],
72 72 _('exclude names matching the given patterns'), _('PATTERN')),
73 73 ]
74 74
75 75 commitopts = [
76 76 ('m', 'message', '',
77 77 _('use text as commit message'), _('TEXT')),
78 78 ('l', 'logfile', '',
79 79 _('read commit message from file'), _('FILE')),
80 80 ]
81 81
82 82 commitopts2 = [
83 83 ('d', 'date', '',
84 84 _('record the specified date as commit date'), _('DATE')),
85 85 ('u', 'user', '',
86 86 _('record the specified user as committer'), _('USER')),
87 87 ]
88 88
89 89 # hidden for now
90 90 formatteropts = [
91 91 ('T', 'template', '',
92 92 _('display with template (EXPERIMENTAL)'), _('TEMPLATE')),
93 93 ]
94 94
95 95 templateopts = [
96 96 ('', 'style', '',
97 97 _('display using template map file (DEPRECATED)'), _('STYLE')),
98 98 ('T', 'template', '',
99 99 _('display with template'), _('TEMPLATE')),
100 100 ]
101 101
102 102 logopts = [
103 103 ('p', 'patch', None, _('show patch')),
104 104 ('g', 'git', None, _('use git extended diff format')),
105 105 ('l', 'limit', '',
106 106 _('limit number of changes displayed'), _('NUM')),
107 107 ('M', 'no-merges', None, _('do not show merges')),
108 108 ('', 'stat', None, _('output diffstat-style summary of changes')),
109 109 ('G', 'graph', None, _("show the revision DAG")),
110 110 ] + templateopts
111 111
112 112 diffopts = [
113 113 ('a', 'text', None, _('treat all files as text')),
114 114 ('g', 'git', None, _('use git extended diff format')),
115 115 ('', 'binary', None, _('generate binary diffs in git mode (default)')),
116 116 ('', 'nodates', None, _('omit dates from diff headers'))
117 117 ]
118 118
119 119 diffwsopts = [
120 120 ('w', 'ignore-all-space', None,
121 121 _('ignore white space when comparing lines')),
122 122 ('b', 'ignore-space-change', None,
123 123 _('ignore changes in the amount of white space')),
124 124 ('B', 'ignore-blank-lines', None,
125 125 _('ignore changes whose lines are all blank')),
126 126 ]
127 127
128 128 diffopts2 = [
129 129 ('', 'noprefix', None, _('omit a/ and b/ prefixes from filenames')),
130 130 ('p', 'show-function', None, _('show which function each change is in')),
131 131 ('', 'reverse', None, _('produce a diff that undoes the changes')),
132 132 ] + diffwsopts + [
133 133 ('U', 'unified', '',
134 134 _('number of lines of context to show'), _('NUM')),
135 135 ('', 'stat', None, _('output diffstat-style summary of changes')),
136 136 ('', 'root', '', _('produce diffs relative to subdirectory'), _('DIR')),
137 137 ]
138 138
139 139 mergetoolopts = [
140 140 ('t', 'tool', '', _('specify merge tool')),
141 141 ]
142 142
143 143 similarityopts = [
144 144 ('s', 'similarity', '',
145 145 _('guess renamed files by similarity (0<=s<=100)'), _('SIMILARITY'))
146 146 ]
147 147
148 148 subrepoopts = [
149 149 ('S', 'subrepos', None,
150 150 _('recurse into subrepositories'))
151 151 ]
152 152
153 153 debugrevlogopts = [
154 154 ('c', 'changelog', False, _('open changelog')),
155 155 ('m', 'manifest', False, _('open manifest')),
156 156 ('', 'dir', '', _('open directory manifest')),
157 157 ]
158 158
159 159 # special string such that everything below this line will be ingored in the
160 160 # editor text
161 161 _linebelow = "^HG: ------------------------ >8 ------------------------$"
162 162
163 163 def ishunk(x):
164 164 hunkclasses = (crecordmod.uihunk, patch.recordhunk)
165 165 return isinstance(x, hunkclasses)
166 166
167 167 def newandmodified(chunks, originalchunks):
168 168 newlyaddedandmodifiedfiles = set()
169 169 for chunk in chunks:
170 170 if ishunk(chunk) and chunk.header.isnewfile() and chunk not in \
171 171 originalchunks:
172 172 newlyaddedandmodifiedfiles.add(chunk.header.filename())
173 173 return newlyaddedandmodifiedfiles
174 174
175 175 def parsealiases(cmd):
176 176 return cmd.lstrip("^").split("|")
177 177
178 178 def setupwrapcolorwrite(ui):
179 179 # wrap ui.write so diff output can be labeled/colorized
180 180 def wrapwrite(orig, *args, **kw):
181 181 label = kw.pop('label', '')
182 182 for chunk, l in patch.difflabel(lambda: args):
183 183 orig(chunk, label=label + l)
184 184
185 185 oldwrite = ui.write
186 186 def wrap(*args, **kwargs):
187 187 return wrapwrite(oldwrite, *args, **kwargs)
188 188 setattr(ui, 'write', wrap)
189 189 return oldwrite
190 190
191 191 def filterchunks(ui, originalhunks, usecurses, testfile, operation=None):
192 192 if usecurses:
193 193 if testfile:
194 194 recordfn = crecordmod.testdecorator(testfile,
195 195 crecordmod.testchunkselector)
196 196 else:
197 197 recordfn = crecordmod.chunkselector
198 198
199 199 return crecordmod.filterpatch(ui, originalhunks, recordfn, operation)
200 200
201 201 else:
202 202 return patch.filterpatch(ui, originalhunks, operation)
203 203
204 204 def recordfilter(ui, originalhunks, operation=None):
205 205 """ Prompts the user to filter the originalhunks and return a list of
206 206 selected hunks.
207 207 *operation* is used for to build ui messages to indicate the user what
208 208 kind of filtering they are doing: reverting, committing, shelving, etc.
209 209 (see patch.filterpatch).
210 210 """
211 211 usecurses = crecordmod.checkcurses(ui)
212 212 testfile = ui.config('experimental', 'crecordtest', None)
213 213 oldwrite = setupwrapcolorwrite(ui)
214 214 try:
215 215 newchunks, newopts = filterchunks(ui, originalhunks, usecurses,
216 216 testfile, operation)
217 217 finally:
218 218 ui.write = oldwrite
219 219 return newchunks, newopts
220 220
221 221 def dorecord(ui, repo, commitfunc, cmdsuggest, backupall,
222 222 filterfn, *pats, **opts):
223 223 from . import merge as mergemod
224 224 opts = pycompat.byteskwargs(opts)
225 225 if not ui.interactive():
226 226 if cmdsuggest:
227 227 msg = _('running non-interactively, use %s instead') % cmdsuggest
228 228 else:
229 229 msg = _('running non-interactively')
230 230 raise error.Abort(msg)
231 231
232 232 # make sure username is set before going interactive
233 233 if not opts.get('user'):
234 234 ui.username() # raise exception, username not provided
235 235
236 236 def recordfunc(ui, repo, message, match, opts):
237 237 """This is generic record driver.
238 238
239 239 Its job is to interactively filter local changes, and
240 240 accordingly prepare working directory into a state in which the
241 241 job can be delegated to a non-interactive commit command such as
242 242 'commit' or 'qrefresh'.
243 243
244 244 After the actual job is done by non-interactive command, the
245 245 working directory is restored to its original state.
246 246
247 247 In the end we'll record interesting changes, and everything else
248 248 will be left in place, so the user can continue working.
249 249 """
250 250
251 251 checkunfinished(repo, commit=True)
252 252 wctx = repo[None]
253 253 merge = len(wctx.parents()) > 1
254 254 if merge:
255 255 raise error.Abort(_('cannot partially commit a merge '
256 256 '(use "hg commit" instead)'))
257 257
258 258 def fail(f, msg):
259 259 raise error.Abort('%s: %s' % (f, msg))
260 260
261 261 force = opts.get('force')
262 262 if not force:
263 263 vdirs = []
264 264 match.explicitdir = vdirs.append
265 265 match.bad = fail
266 266
267 267 status = repo.status(match=match)
268 268 if not force:
269 269 repo.checkcommitpatterns(wctx, vdirs, match, status, fail)
270 270 diffopts = patch.difffeatureopts(ui, opts=opts, whitespace=True)
271 271 diffopts.nodates = True
272 272 diffopts.git = True
273 273 diffopts.showfunc = True
274 274 originaldiff = patch.diff(repo, changes=status, opts=diffopts)
275 275 originalchunks = patch.parsepatch(originaldiff)
276 276
277 277 # 1. filter patch, since we are intending to apply subset of it
278 278 try:
279 279 chunks, newopts = filterfn(ui, originalchunks)
280 280 except patch.PatchError as err:
281 281 raise error.Abort(_('error parsing patch: %s') % err)
282 282 opts.update(newopts)
283 283
284 284 # We need to keep a backup of files that have been newly added and
285 285 # modified during the recording process because there is a previous
286 286 # version without the edit in the workdir
287 287 newlyaddedandmodifiedfiles = newandmodified(chunks, originalchunks)
288 288 contenders = set()
289 289 for h in chunks:
290 290 try:
291 291 contenders.update(set(h.files()))
292 292 except AttributeError:
293 293 pass
294 294
295 295 changed = status.modified + status.added + status.removed
296 296 newfiles = [f for f in changed if f in contenders]
297 297 if not newfiles:
298 298 ui.status(_('no changes to record\n'))
299 299 return 0
300 300
301 301 modified = set(status.modified)
302 302
303 303 # 2. backup changed files, so we can restore them in the end
304 304
305 305 if backupall:
306 306 tobackup = changed
307 307 else:
308 308 tobackup = [f for f in newfiles if f in modified or f in \
309 309 newlyaddedandmodifiedfiles]
310 310 backups = {}
311 311 if tobackup:
312 312 backupdir = repo.vfs.join('record-backups')
313 313 try:
314 314 os.mkdir(backupdir)
315 315 except OSError as err:
316 316 if err.errno != errno.EEXIST:
317 317 raise
318 318 try:
319 319 # backup continues
320 320 for f in tobackup:
321 321 fd, tmpname = tempfile.mkstemp(prefix=f.replace('/', '_')+'.',
322 322 dir=backupdir)
323 323 os.close(fd)
324 324 ui.debug('backup %r as %r\n' % (f, tmpname))
325 325 util.copyfile(repo.wjoin(f), tmpname, copystat=True)
326 326 backups[f] = tmpname
327 327
328 328 fp = stringio()
329 329 for c in chunks:
330 330 fname = c.filename()
331 331 if fname in backups:
332 332 c.write(fp)
333 333 dopatch = fp.tell()
334 334 fp.seek(0)
335 335
336 336 # 2.5 optionally review / modify patch in text editor
337 337 if opts.get('review', False):
338 338 patchtext = (crecordmod.diffhelptext
339 339 + crecordmod.patchhelptext
340 340 + fp.read())
341 341 reviewedpatch = ui.edit(patchtext, "",
342 342 extra={"suffix": ".diff"},
343 343 repopath=repo.path)
344 344 fp.truncate(0)
345 345 fp.write(reviewedpatch)
346 346 fp.seek(0)
347 347
348 348 [os.unlink(repo.wjoin(c)) for c in newlyaddedandmodifiedfiles]
349 349 # 3a. apply filtered patch to clean repo (clean)
350 350 if backups:
351 351 # Equivalent to hg.revert
352 352 m = scmutil.matchfiles(repo, backups.keys())
353 353 mergemod.update(repo, repo.dirstate.p1(),
354 354 False, True, matcher=m)
355 355
356 356 # 3b. (apply)
357 357 if dopatch:
358 358 try:
359 359 ui.debug('applying patch\n')
360 360 ui.debug(fp.getvalue())
361 361 patch.internalpatch(ui, repo, fp, 1, eolmode=None)
362 362 except patch.PatchError as err:
363 363 raise error.Abort(str(err))
364 364 del fp
365 365
366 366 # 4. We prepared working directory according to filtered
367 367 # patch. Now is the time to delegate the job to
368 368 # commit/qrefresh or the like!
369 369
370 370 # Make all of the pathnames absolute.
371 371 newfiles = [repo.wjoin(nf) for nf in newfiles]
372 372 return commitfunc(ui, repo, *newfiles, **opts)
373 373 finally:
374 374 # 5. finally restore backed-up files
375 375 try:
376 376 dirstate = repo.dirstate
377 377 for realname, tmpname in backups.iteritems():
378 378 ui.debug('restoring %r to %r\n' % (tmpname, realname))
379 379
380 380 if dirstate[realname] == 'n':
381 381 # without normallookup, restoring timestamp
382 382 # may cause partially committed files
383 383 # to be treated as unmodified
384 384 dirstate.normallookup(realname)
385 385
386 386 # copystat=True here and above are a hack to trick any
387 387 # editors that have f open that we haven't modified them.
388 388 #
389 389 # Also note that this racy as an editor could notice the
390 390 # file's mtime before we've finished writing it.
391 391 util.copyfile(tmpname, repo.wjoin(realname), copystat=True)
392 392 os.unlink(tmpname)
393 393 if tobackup:
394 394 os.rmdir(backupdir)
395 395 except OSError:
396 396 pass
397 397
398 398 def recordinwlock(ui, repo, message, match, opts):
399 399 with repo.wlock():
400 400 return recordfunc(ui, repo, message, match, opts)
401 401
402 402 return commit(ui, repo, recordinwlock, pats, opts)
403 403
404 404 def findpossible(cmd, table, strict=False):
405 405 """
406 406 Return cmd -> (aliases, command table entry)
407 407 for each matching command.
408 408 Return debug commands (or their aliases) only if no normal command matches.
409 409 """
410 410 choice = {}
411 411 debugchoice = {}
412 412
413 413 if cmd in table:
414 414 # short-circuit exact matches, "log" alias beats "^log|history"
415 415 keys = [cmd]
416 416 else:
417 417 keys = table.keys()
418 418
419 419 allcmds = []
420 420 for e in keys:
421 421 aliases = parsealiases(e)
422 422 allcmds.extend(aliases)
423 423 found = None
424 424 if cmd in aliases:
425 425 found = cmd
426 426 elif not strict:
427 427 for a in aliases:
428 428 if a.startswith(cmd):
429 429 found = a
430 430 break
431 431 if found is not None:
432 432 if aliases[0].startswith("debug") or found.startswith("debug"):
433 433 debugchoice[found] = (aliases, table[e])
434 434 else:
435 435 choice[found] = (aliases, table[e])
436 436
437 437 if not choice and debugchoice:
438 438 choice = debugchoice
439 439
440 440 return choice, allcmds
441 441
442 442 def findcmd(cmd, table, strict=True):
443 443 """Return (aliases, command table entry) for command string."""
444 444 choice, allcmds = findpossible(cmd, table, strict)
445 445
446 446 if cmd in choice:
447 447 return choice[cmd]
448 448
449 449 if len(choice) > 1:
450 450 clist = sorted(choice)
451 451 raise error.AmbiguousCommand(cmd, clist)
452 452
453 453 if choice:
454 454 return list(choice.values())[0]
455 455
456 456 raise error.UnknownCommand(cmd, allcmds)
457 457
458 458 def findrepo(p):
459 459 while not os.path.isdir(os.path.join(p, ".hg")):
460 460 oldp, p = p, os.path.dirname(p)
461 461 if p == oldp:
462 462 return None
463 463
464 464 return p
465 465
466 466 def bailifchanged(repo, merge=True, hint=None):
467 467 """ enforce the precondition that working directory must be clean.
468 468
469 469 'merge' can be set to false if a pending uncommitted merge should be
470 470 ignored (such as when 'update --check' runs).
471 471
472 472 'hint' is the usual hint given to Abort exception.
473 473 """
474 474
475 475 if merge and repo.dirstate.p2() != nullid:
476 476 raise error.Abort(_('outstanding uncommitted merge'), hint=hint)
477 477 modified, added, removed, deleted = repo.status()[:4]
478 478 if modified or added or removed or deleted:
479 479 raise error.Abort(_('uncommitted changes'), hint=hint)
480 480 ctx = repo[None]
481 481 for s in sorted(ctx.substate):
482 482 ctx.sub(s).bailifchanged(hint=hint)
483 483
484 484 def logmessage(ui, opts):
485 485 """ get the log message according to -m and -l option """
486 486 message = opts.get('message')
487 487 logfile = opts.get('logfile')
488 488
489 489 if message and logfile:
490 490 raise error.Abort(_('options --message and --logfile are mutually '
491 491 'exclusive'))
492 492 if not message and logfile:
493 493 try:
494 494 if isstdiofilename(logfile):
495 495 message = ui.fin.read()
496 496 else:
497 497 message = '\n'.join(util.readfile(logfile).splitlines())
498 498 except IOError as inst:
499 499 raise error.Abort(_("can't read commit message '%s': %s") %
500 500 (logfile, inst.strerror))
501 501 return message
502 502
503 503 def mergeeditform(ctxorbool, baseformname):
504 504 """return appropriate editform name (referencing a committemplate)
505 505
506 506 'ctxorbool' is either a ctx to be committed, or a bool indicating whether
507 507 merging is committed.
508 508
509 509 This returns baseformname with '.merge' appended if it is a merge,
510 510 otherwise '.normal' is appended.
511 511 """
512 512 if isinstance(ctxorbool, bool):
513 513 if ctxorbool:
514 514 return baseformname + ".merge"
515 515 elif 1 < len(ctxorbool.parents()):
516 516 return baseformname + ".merge"
517 517
518 518 return baseformname + ".normal"
519 519
520 520 def getcommiteditor(edit=False, finishdesc=None, extramsg=None,
521 521 editform='', **opts):
522 522 """get appropriate commit message editor according to '--edit' option
523 523
524 524 'finishdesc' is a function to be called with edited commit message
525 525 (= 'description' of the new changeset) just after editing, but
526 526 before checking empty-ness. It should return actual text to be
527 527 stored into history. This allows to change description before
528 528 storing.
529 529
530 530 'extramsg' is a extra message to be shown in the editor instead of
531 531 'Leave message empty to abort commit' line. 'HG: ' prefix and EOL
532 532 is automatically added.
533 533
534 534 'editform' is a dot-separated list of names, to distinguish
535 535 the purpose of commit text editing.
536 536
537 537 'getcommiteditor' returns 'commitforceeditor' regardless of
538 538 'edit', if one of 'finishdesc' or 'extramsg' is specified, because
539 539 they are specific for usage in MQ.
540 540 """
541 541 if edit or finishdesc or extramsg:
542 542 return lambda r, c, s: commitforceeditor(r, c, s,
543 543 finishdesc=finishdesc,
544 544 extramsg=extramsg,
545 545 editform=editform)
546 546 elif editform:
547 547 return lambda r, c, s: commiteditor(r, c, s, editform=editform)
548 548 else:
549 549 return commiteditor
550 550
551 551 def loglimit(opts):
552 552 """get the log limit according to option -l/--limit"""
553 553 limit = opts.get('limit')
554 554 if limit:
555 555 try:
556 556 limit = int(limit)
557 557 except ValueError:
558 558 raise error.Abort(_('limit must be a positive integer'))
559 559 if limit <= 0:
560 560 raise error.Abort(_('limit must be positive'))
561 561 else:
562 562 limit = None
563 563 return limit
564 564
565 565 def makefilename(repo, pat, node, desc=None,
566 566 total=None, seqno=None, revwidth=None, pathname=None):
567 567 node_expander = {
568 568 'H': lambda: hex(node),
569 569 'R': lambda: str(repo.changelog.rev(node)),
570 570 'h': lambda: short(node),
571 571 'm': lambda: re.sub('[^\w]', '_', str(desc))
572 572 }
573 573 expander = {
574 574 '%': lambda: '%',
575 575 'b': lambda: os.path.basename(repo.root),
576 576 }
577 577
578 578 try:
579 579 if node:
580 580 expander.update(node_expander)
581 581 if node:
582 582 expander['r'] = (lambda:
583 583 str(repo.changelog.rev(node)).zfill(revwidth or 0))
584 584 if total is not None:
585 585 expander['N'] = lambda: str(total)
586 586 if seqno is not None:
587 587 expander['n'] = lambda: str(seqno)
588 588 if total is not None and seqno is not None:
589 589 expander['n'] = lambda: str(seqno).zfill(len(str(total)))
590 590 if pathname is not None:
591 591 expander['s'] = lambda: os.path.basename(pathname)
592 592 expander['d'] = lambda: os.path.dirname(pathname) or '.'
593 593 expander['p'] = lambda: pathname
594 594
595 595 newname = []
596 596 patlen = len(pat)
597 597 i = 0
598 598 while i < patlen:
599 599 c = pat[i:i + 1]
600 600 if c == '%':
601 601 i += 1
602 602 c = pat[i:i + 1]
603 603 c = expander[c]()
604 604 newname.append(c)
605 605 i += 1
606 606 return ''.join(newname)
607 607 except KeyError as inst:
608 608 raise error.Abort(_("invalid format spec '%%%s' in output filename") %
609 609 inst.args[0])
610 610
611 611 def isstdiofilename(pat):
612 612 """True if the given pat looks like a filename denoting stdin/stdout"""
613 613 return not pat or pat == '-'
614 614
615 615 class _unclosablefile(object):
616 616 def __init__(self, fp):
617 617 self._fp = fp
618 618
619 619 def close(self):
620 620 pass
621 621
622 622 def __iter__(self):
623 623 return iter(self._fp)
624 624
625 625 def __getattr__(self, attr):
626 626 return getattr(self._fp, attr)
627 627
628 628 def __enter__(self):
629 629 return self
630 630
631 631 def __exit__(self, exc_type, exc_value, exc_tb):
632 632 pass
633 633
634 634 def makefileobj(repo, pat, node=None, desc=None, total=None,
635 635 seqno=None, revwidth=None, mode='wb', modemap=None,
636 636 pathname=None):
637 637
638 638 writable = mode not in ('r', 'rb')
639 639
640 640 if isstdiofilename(pat):
641 641 if writable:
642 642 fp = repo.ui.fout
643 643 else:
644 644 fp = repo.ui.fin
645 645 return _unclosablefile(fp)
646 646 fn = makefilename(repo, pat, node, desc, total, seqno, revwidth, pathname)
647 647 if modemap is not None:
648 648 mode = modemap.get(fn, mode)
649 649 if mode == 'wb':
650 650 modemap[fn] = 'ab'
651 651 return open(fn, mode)
652 652
653 653 def openrevlog(repo, cmd, file_, opts):
654 654 """opens the changelog, manifest, a filelog or a given revlog"""
655 655 cl = opts['changelog']
656 656 mf = opts['manifest']
657 657 dir = opts['dir']
658 658 msg = None
659 659 if cl and mf:
660 660 msg = _('cannot specify --changelog and --manifest at the same time')
661 661 elif cl and dir:
662 662 msg = _('cannot specify --changelog and --dir at the same time')
663 663 elif cl or mf or dir:
664 664 if file_:
665 665 msg = _('cannot specify filename with --changelog or --manifest')
666 666 elif not repo:
667 667 msg = _('cannot specify --changelog or --manifest or --dir '
668 668 'without a repository')
669 669 if msg:
670 670 raise error.Abort(msg)
671 671
672 672 r = None
673 673 if repo:
674 674 if cl:
675 675 r = repo.unfiltered().changelog
676 676 elif dir:
677 677 if 'treemanifest' not in repo.requirements:
678 678 raise error.Abort(_("--dir can only be used on repos with "
679 679 "treemanifest enabled"))
680 680 dirlog = repo.manifestlog._revlog.dirlog(dir)
681 681 if len(dirlog):
682 682 r = dirlog
683 683 elif mf:
684 684 r = repo.manifestlog._revlog
685 685 elif file_:
686 686 filelog = repo.file(file_)
687 687 if len(filelog):
688 688 r = filelog
689 689 if not r:
690 690 if not file_:
691 691 raise error.CommandError(cmd, _('invalid arguments'))
692 692 if not os.path.isfile(file_):
693 693 raise error.Abort(_("revlog '%s' not found") % file_)
694 694 r = revlog.revlog(vfsmod.vfs(pycompat.getcwd(), audit=False),
695 695 file_[:-2] + ".i")
696 696 return r
697 697
698 698 def copy(ui, repo, pats, opts, rename=False):
699 699 # called with the repo lock held
700 700 #
701 701 # hgsep => pathname that uses "/" to separate directories
702 702 # ossep => pathname that uses os.sep to separate directories
703 703 cwd = repo.getcwd()
704 704 targets = {}
705 705 after = opts.get("after")
706 706 dryrun = opts.get("dry_run")
707 707 wctx = repo[None]
708 708
709 709 def walkpat(pat):
710 710 srcs = []
711 711 if after:
712 712 badstates = '?'
713 713 else:
714 714 badstates = '?r'
715 715 m = scmutil.match(wctx, [pat], opts, globbed=True)
716 716 for abs in wctx.walk(m):
717 717 state = repo.dirstate[abs]
718 718 rel = m.rel(abs)
719 719 exact = m.exact(abs)
720 720 if state in badstates:
721 721 if exact and state == '?':
722 722 ui.warn(_('%s: not copying - file is not managed\n') % rel)
723 723 if exact and state == 'r':
724 724 ui.warn(_('%s: not copying - file has been marked for'
725 725 ' remove\n') % rel)
726 726 continue
727 727 # abs: hgsep
728 728 # rel: ossep
729 729 srcs.append((abs, rel, exact))
730 730 return srcs
731 731
732 732 # abssrc: hgsep
733 733 # relsrc: ossep
734 734 # otarget: ossep
735 735 def copyfile(abssrc, relsrc, otarget, exact):
736 736 abstarget = pathutil.canonpath(repo.root, cwd, otarget)
737 737 if '/' in abstarget:
738 738 # We cannot normalize abstarget itself, this would prevent
739 739 # case only renames, like a => A.
740 740 abspath, absname = abstarget.rsplit('/', 1)
741 741 abstarget = repo.dirstate.normalize(abspath) + '/' + absname
742 742 reltarget = repo.pathto(abstarget, cwd)
743 743 target = repo.wjoin(abstarget)
744 744 src = repo.wjoin(abssrc)
745 745 state = repo.dirstate[abstarget]
746 746
747 747 scmutil.checkportable(ui, abstarget)
748 748
749 749 # check for collisions
750 750 prevsrc = targets.get(abstarget)
751 751 if prevsrc is not None:
752 752 ui.warn(_('%s: not overwriting - %s collides with %s\n') %
753 753 (reltarget, repo.pathto(abssrc, cwd),
754 754 repo.pathto(prevsrc, cwd)))
755 755 return
756 756
757 757 # check for overwrites
758 758 exists = os.path.lexists(target)
759 759 samefile = False
760 760 if exists and abssrc != abstarget:
761 761 if (repo.dirstate.normalize(abssrc) ==
762 762 repo.dirstate.normalize(abstarget)):
763 763 if not rename:
764 764 ui.warn(_("%s: can't copy - same file\n") % reltarget)
765 765 return
766 766 exists = False
767 767 samefile = True
768 768
769 769 if not after and exists or after and state in 'mn':
770 770 if not opts['force']:
771 771 if state in 'mn':
772 772 msg = _('%s: not overwriting - file already committed\n')
773 773 if after:
774 774 flags = '--after --force'
775 775 else:
776 776 flags = '--force'
777 777 if rename:
778 778 hint = _('(hg rename %s to replace the file by '
779 779 'recording a rename)\n') % flags
780 780 else:
781 781 hint = _('(hg copy %s to replace the file by '
782 782 'recording a copy)\n') % flags
783 783 else:
784 784 msg = _('%s: not overwriting - file exists\n')
785 785 if rename:
786 786 hint = _('(hg rename --after to record the rename)\n')
787 787 else:
788 788 hint = _('(hg copy --after to record the copy)\n')
789 789 ui.warn(msg % reltarget)
790 790 ui.warn(hint)
791 791 return
792 792
793 793 if after:
794 794 if not exists:
795 795 if rename:
796 796 ui.warn(_('%s: not recording move - %s does not exist\n') %
797 797 (relsrc, reltarget))
798 798 else:
799 799 ui.warn(_('%s: not recording copy - %s does not exist\n') %
800 800 (relsrc, reltarget))
801 801 return
802 802 elif not dryrun:
803 803 try:
804 804 if exists:
805 805 os.unlink(target)
806 806 targetdir = os.path.dirname(target) or '.'
807 807 if not os.path.isdir(targetdir):
808 808 os.makedirs(targetdir)
809 809 if samefile:
810 810 tmp = target + "~hgrename"
811 811 os.rename(src, tmp)
812 812 os.rename(tmp, target)
813 813 else:
814 814 util.copyfile(src, target)
815 815 srcexists = True
816 816 except IOError as inst:
817 817 if inst.errno == errno.ENOENT:
818 818 ui.warn(_('%s: deleted in working directory\n') % relsrc)
819 819 srcexists = False
820 820 else:
821 821 ui.warn(_('%s: cannot copy - %s\n') %
822 822 (relsrc, inst.strerror))
823 823 return True # report a failure
824 824
825 825 if ui.verbose or not exact:
826 826 if rename:
827 827 ui.status(_('moving %s to %s\n') % (relsrc, reltarget))
828 828 else:
829 829 ui.status(_('copying %s to %s\n') % (relsrc, reltarget))
830 830
831 831 targets[abstarget] = abssrc
832 832
833 833 # fix up dirstate
834 834 scmutil.dirstatecopy(ui, repo, wctx, abssrc, abstarget,
835 835 dryrun=dryrun, cwd=cwd)
836 836 if rename and not dryrun:
837 837 if not after and srcexists and not samefile:
838 838 repo.wvfs.unlinkpath(abssrc)
839 839 wctx.forget([abssrc])
840 840
841 841 # pat: ossep
842 842 # dest ossep
843 843 # srcs: list of (hgsep, hgsep, ossep, bool)
844 844 # return: function that takes hgsep and returns ossep
845 845 def targetpathfn(pat, dest, srcs):
846 846 if os.path.isdir(pat):
847 847 abspfx = pathutil.canonpath(repo.root, cwd, pat)
848 848 abspfx = util.localpath(abspfx)
849 849 if destdirexists:
850 850 striplen = len(os.path.split(abspfx)[0])
851 851 else:
852 852 striplen = len(abspfx)
853 853 if striplen:
854 854 striplen += len(pycompat.ossep)
855 855 res = lambda p: os.path.join(dest, util.localpath(p)[striplen:])
856 856 elif destdirexists:
857 857 res = lambda p: os.path.join(dest,
858 858 os.path.basename(util.localpath(p)))
859 859 else:
860 860 res = lambda p: dest
861 861 return res
862 862
863 863 # pat: ossep
864 864 # dest ossep
865 865 # srcs: list of (hgsep, hgsep, ossep, bool)
866 866 # return: function that takes hgsep and returns ossep
867 867 def targetpathafterfn(pat, dest, srcs):
868 868 if matchmod.patkind(pat):
869 869 # a mercurial pattern
870 870 res = lambda p: os.path.join(dest,
871 871 os.path.basename(util.localpath(p)))
872 872 else:
873 873 abspfx = pathutil.canonpath(repo.root, cwd, pat)
874 874 if len(abspfx) < len(srcs[0][0]):
875 875 # A directory. Either the target path contains the last
876 876 # component of the source path or it does not.
877 877 def evalpath(striplen):
878 878 score = 0
879 879 for s in srcs:
880 880 t = os.path.join(dest, util.localpath(s[0])[striplen:])
881 881 if os.path.lexists(t):
882 882 score += 1
883 883 return score
884 884
885 885 abspfx = util.localpath(abspfx)
886 886 striplen = len(abspfx)
887 887 if striplen:
888 888 striplen += len(pycompat.ossep)
889 889 if os.path.isdir(os.path.join(dest, os.path.split(abspfx)[1])):
890 890 score = evalpath(striplen)
891 891 striplen1 = len(os.path.split(abspfx)[0])
892 892 if striplen1:
893 893 striplen1 += len(pycompat.ossep)
894 894 if evalpath(striplen1) > score:
895 895 striplen = striplen1
896 896 res = lambda p: os.path.join(dest,
897 897 util.localpath(p)[striplen:])
898 898 else:
899 899 # a file
900 900 if destdirexists:
901 901 res = lambda p: os.path.join(dest,
902 902 os.path.basename(util.localpath(p)))
903 903 else:
904 904 res = lambda p: dest
905 905 return res
906 906
907 907 pats = scmutil.expandpats(pats)
908 908 if not pats:
909 909 raise error.Abort(_('no source or destination specified'))
910 910 if len(pats) == 1:
911 911 raise error.Abort(_('no destination specified'))
912 912 dest = pats.pop()
913 913 destdirexists = os.path.isdir(dest) and not os.path.islink(dest)
914 914 if not destdirexists:
915 915 if len(pats) > 1 or matchmod.patkind(pats[0]):
916 916 raise error.Abort(_('with multiple sources, destination must be an '
917 917 'existing directory'))
918 918 if util.endswithsep(dest):
919 919 raise error.Abort(_('destination %s is not a directory') % dest)
920 920
921 921 tfn = targetpathfn
922 922 if after:
923 923 tfn = targetpathafterfn
924 924 copylist = []
925 925 for pat in pats:
926 926 srcs = walkpat(pat)
927 927 if not srcs:
928 928 continue
929 929 copylist.append((tfn(pat, dest, srcs), srcs))
930 930 if not copylist:
931 931 raise error.Abort(_('no files to copy'))
932 932
933 933 errors = 0
934 934 for targetpath, srcs in copylist:
935 935 for abssrc, relsrc, exact in srcs:
936 936 if copyfile(abssrc, relsrc, targetpath(abssrc), exact):
937 937 errors += 1
938 938
939 939 if errors:
940 940 ui.warn(_('(consider using --after)\n'))
941 941
942 942 return errors != 0
943 943
944 944 ## facility to let extension process additional data into an import patch
945 945 # list of identifier to be executed in order
946 946 extrapreimport = [] # run before commit
947 947 extrapostimport = [] # run after commit
948 948 # mapping from identifier to actual import function
949 949 #
950 950 # 'preimport' are run before the commit is made and are provided the following
951 951 # arguments:
952 952 # - repo: the localrepository instance,
953 953 # - patchdata: data extracted from patch header (cf m.patch.patchheadermap),
954 954 # - extra: the future extra dictionary of the changeset, please mutate it,
955 955 # - opts: the import options.
956 956 # XXX ideally, we would just pass an ctx ready to be computed, that would allow
957 957 # mutation of in memory commit and more. Feel free to rework the code to get
958 958 # there.
959 959 extrapreimportmap = {}
960 960 # 'postimport' are run after the commit is made and are provided the following
961 961 # argument:
962 962 # - ctx: the changectx created by import.
963 963 extrapostimportmap = {}
964 964
965 965 def tryimportone(ui, repo, hunk, parents, opts, msgs, updatefunc):
966 966 """Utility function used by commands.import to import a single patch
967 967
968 968 This function is explicitly defined here to help the evolve extension to
969 969 wrap this part of the import logic.
970 970
971 971 The API is currently a bit ugly because it a simple code translation from
972 972 the import command. Feel free to make it better.
973 973
974 974 :hunk: a patch (as a binary string)
975 975 :parents: nodes that will be parent of the created commit
976 976 :opts: the full dict of option passed to the import command
977 977 :msgs: list to save commit message to.
978 978 (used in case we need to save it when failing)
979 979 :updatefunc: a function that update a repo to a given node
980 980 updatefunc(<repo>, <node>)
981 981 """
982 982 # avoid cycle context -> subrepo -> cmdutil
983 983 from . import context
984 984 extractdata = patch.extract(ui, hunk)
985 985 tmpname = extractdata.get('filename')
986 986 message = extractdata.get('message')
987 987 user = opts.get('user') or extractdata.get('user')
988 988 date = opts.get('date') or extractdata.get('date')
989 989 branch = extractdata.get('branch')
990 990 nodeid = extractdata.get('nodeid')
991 991 p1 = extractdata.get('p1')
992 992 p2 = extractdata.get('p2')
993 993
994 994 nocommit = opts.get('no_commit')
995 995 importbranch = opts.get('import_branch')
996 996 update = not opts.get('bypass')
997 997 strip = opts["strip"]
998 998 prefix = opts["prefix"]
999 999 sim = float(opts.get('similarity') or 0)
1000 1000 if not tmpname:
1001 1001 return (None, None, False)
1002 1002
1003 1003 rejects = False
1004 1004
1005 1005 try:
1006 1006 cmdline_message = logmessage(ui, opts)
1007 1007 if cmdline_message:
1008 1008 # pickup the cmdline msg
1009 1009 message = cmdline_message
1010 1010 elif message:
1011 1011 # pickup the patch msg
1012 1012 message = message.strip()
1013 1013 else:
1014 1014 # launch the editor
1015 1015 message = None
1016 1016 ui.debug('message:\n%s\n' % message)
1017 1017
1018 1018 if len(parents) == 1:
1019 1019 parents.append(repo[nullid])
1020 1020 if opts.get('exact'):
1021 1021 if not nodeid or not p1:
1022 1022 raise error.Abort(_('not a Mercurial patch'))
1023 1023 p1 = repo[p1]
1024 1024 p2 = repo[p2 or nullid]
1025 1025 elif p2:
1026 1026 try:
1027 1027 p1 = repo[p1]
1028 1028 p2 = repo[p2]
1029 1029 # Without any options, consider p2 only if the
1030 1030 # patch is being applied on top of the recorded
1031 1031 # first parent.
1032 1032 if p1 != parents[0]:
1033 1033 p1 = parents[0]
1034 1034 p2 = repo[nullid]
1035 1035 except error.RepoError:
1036 1036 p1, p2 = parents
1037 1037 if p2.node() == nullid:
1038 1038 ui.warn(_("warning: import the patch as a normal revision\n"
1039 1039 "(use --exact to import the patch as a merge)\n"))
1040 1040 else:
1041 1041 p1, p2 = parents
1042 1042
1043 1043 n = None
1044 1044 if update:
1045 1045 if p1 != parents[0]:
1046 1046 updatefunc(repo, p1.node())
1047 1047 if p2 != parents[1]:
1048 1048 repo.setparents(p1.node(), p2.node())
1049 1049
1050 1050 if opts.get('exact') or importbranch:
1051 1051 repo.dirstate.setbranch(branch or 'default')
1052 1052
1053 1053 partial = opts.get('partial', False)
1054 1054 files = set()
1055 1055 try:
1056 1056 patch.patch(ui, repo, tmpname, strip=strip, prefix=prefix,
1057 1057 files=files, eolmode=None, similarity=sim / 100.0)
1058 1058 except patch.PatchError as e:
1059 1059 if not partial:
1060 1060 raise error.Abort(str(e))
1061 1061 if partial:
1062 1062 rejects = True
1063 1063
1064 1064 files = list(files)
1065 1065 if nocommit:
1066 1066 if message:
1067 1067 msgs.append(message)
1068 1068 else:
1069 1069 if opts.get('exact') or p2:
1070 1070 # If you got here, you either use --force and know what
1071 1071 # you are doing or used --exact or a merge patch while
1072 1072 # being updated to its first parent.
1073 1073 m = None
1074 1074 else:
1075 1075 m = scmutil.matchfiles(repo, files or [])
1076 1076 editform = mergeeditform(repo[None], 'import.normal')
1077 1077 if opts.get('exact'):
1078 1078 editor = None
1079 1079 else:
1080 1080 editor = getcommiteditor(editform=editform, **opts)
1081 1081 extra = {}
1082 1082 for idfunc in extrapreimport:
1083 1083 extrapreimportmap[idfunc](repo, extractdata, extra, opts)
1084 1084 overrides = {}
1085 1085 if partial:
1086 1086 overrides[('ui', 'allowemptycommit')] = True
1087 1087 with repo.ui.configoverride(overrides, 'import'):
1088 1088 n = repo.commit(message, user,
1089 1089 date, match=m,
1090 1090 editor=editor, extra=extra)
1091 1091 for idfunc in extrapostimport:
1092 1092 extrapostimportmap[idfunc](repo[n])
1093 1093 else:
1094 1094 if opts.get('exact') or importbranch:
1095 1095 branch = branch or 'default'
1096 1096 else:
1097 1097 branch = p1.branch()
1098 1098 store = patch.filestore()
1099 1099 try:
1100 1100 files = set()
1101 1101 try:
1102 1102 patch.patchrepo(ui, repo, p1, store, tmpname, strip, prefix,
1103 1103 files, eolmode=None)
1104 1104 except patch.PatchError as e:
1105 1105 raise error.Abort(str(e))
1106 1106 if opts.get('exact'):
1107 1107 editor = None
1108 1108 else:
1109 1109 editor = getcommiteditor(editform='import.bypass')
1110 1110 memctx = context.memctx(repo, (p1.node(), p2.node()),
1111 1111 message,
1112 1112 files=files,
1113 1113 filectxfn=store,
1114 1114 user=user,
1115 1115 date=date,
1116 1116 branch=branch,
1117 1117 editor=editor)
1118 1118 n = memctx.commit()
1119 1119 finally:
1120 1120 store.close()
1121 1121 if opts.get('exact') and nocommit:
1122 1122 # --exact with --no-commit is still useful in that it does merge
1123 1123 # and branch bits
1124 1124 ui.warn(_("warning: can't check exact import with --no-commit\n"))
1125 1125 elif opts.get('exact') and hex(n) != nodeid:
1126 1126 raise error.Abort(_('patch is damaged or loses information'))
1127 1127 msg = _('applied to working directory')
1128 1128 if n:
1129 1129 # i18n: refers to a short changeset id
1130 1130 msg = _('created %s') % short(n)
1131 1131 return (msg, n, rejects)
1132 1132 finally:
1133 1133 os.unlink(tmpname)
1134 1134
1135 1135 # facility to let extensions include additional data in an exported patch
1136 1136 # list of identifiers to be executed in order
1137 1137 extraexport = []
1138 1138 # mapping from identifier to actual export function
1139 1139 # function as to return a string to be added to the header or None
1140 1140 # it is given two arguments (sequencenumber, changectx)
1141 1141 extraexportmap = {}
1142 1142
1143 1143 def _exportsingle(repo, ctx, match, switch_parent, rev, seqno, write, diffopts):
1144 1144 node = scmutil.binnode(ctx)
1145 1145 parents = [p.node() for p in ctx.parents() if p]
1146 1146 branch = ctx.branch()
1147 1147 if switch_parent:
1148 1148 parents.reverse()
1149 1149
1150 1150 if parents:
1151 1151 prev = parents[0]
1152 1152 else:
1153 1153 prev = nullid
1154 1154
1155 1155 write("# HG changeset patch\n")
1156 1156 write("# User %s\n" % ctx.user())
1157 1157 write("# Date %d %d\n" % ctx.date())
1158 1158 write("# %s\n" % util.datestr(ctx.date()))
1159 1159 if branch and branch != 'default':
1160 1160 write("# Branch %s\n" % branch)
1161 1161 write("# Node ID %s\n" % hex(node))
1162 1162 write("# Parent %s\n" % hex(prev))
1163 1163 if len(parents) > 1:
1164 1164 write("# Parent %s\n" % hex(parents[1]))
1165 1165
1166 1166 for headerid in extraexport:
1167 1167 header = extraexportmap[headerid](seqno, ctx)
1168 1168 if header is not None:
1169 1169 write('# %s\n' % header)
1170 1170 write(ctx.description().rstrip())
1171 1171 write("\n\n")
1172 1172
1173 1173 for chunk, label in patch.diffui(repo, prev, node, match, opts=diffopts):
1174 1174 write(chunk, label=label)
1175 1175
1176 1176 def export(repo, revs, fntemplate='hg-%h.patch', fp=None, switch_parent=False,
1177 1177 opts=None, match=None):
1178 1178 '''export changesets as hg patches
1179 1179
1180 1180 Args:
1181 1181 repo: The repository from which we're exporting revisions.
1182 1182 revs: A list of revisions to export as revision numbers.
1183 1183 fntemplate: An optional string to use for generating patch file names.
1184 1184 fp: An optional file-like object to which patches should be written.
1185 1185 switch_parent: If True, show diffs against second parent when not nullid.
1186 1186 Default is false, which always shows diff against p1.
1187 1187 opts: diff options to use for generating the patch.
1188 1188 match: If specified, only export changes to files matching this matcher.
1189 1189
1190 1190 Returns:
1191 1191 Nothing.
1192 1192
1193 1193 Side Effect:
1194 1194 "HG Changeset Patch" data is emitted to one of the following
1195 1195 destinations:
1196 1196 fp is specified: All revs are written to the specified
1197 1197 file-like object.
1198 1198 fntemplate specified: Each rev is written to a unique file named using
1199 1199 the given template.
1200 1200 Neither fp nor template specified: All revs written to repo.ui.write()
1201 1201 '''
1202 1202
1203 1203 total = len(revs)
1204 1204 revwidth = max(len(str(rev)) for rev in revs)
1205 1205 filemode = {}
1206 1206
1207 1207 write = None
1208 1208 dest = '<unnamed>'
1209 1209 if fp:
1210 1210 dest = getattr(fp, 'name', dest)
1211 1211 def write(s, **kw):
1212 1212 fp.write(s)
1213 1213 elif not fntemplate:
1214 1214 write = repo.ui.write
1215 1215
1216 1216 for seqno, rev in enumerate(revs, 1):
1217 1217 ctx = repo[rev]
1218 1218 fo = None
1219 1219 if not fp and fntemplate:
1220 1220 desc_lines = ctx.description().rstrip().split('\n')
1221 1221 desc = desc_lines[0] #Commit always has a first line.
1222 1222 fo = makefileobj(repo, fntemplate, ctx.node(), desc=desc,
1223 1223 total=total, seqno=seqno, revwidth=revwidth,
1224 1224 mode='wb', modemap=filemode)
1225 1225 dest = fo.name
1226 1226 def write(s, **kw):
1227 1227 fo.write(s)
1228 1228 if not dest.startswith('<'):
1229 1229 repo.ui.note("%s\n" % dest)
1230 1230 _exportsingle(
1231 1231 repo, ctx, match, switch_parent, rev, seqno, write, opts)
1232 1232 if fo is not None:
1233 1233 fo.close()
1234 1234
1235 1235 def diffordiffstat(ui, repo, diffopts, node1, node2, match,
1236 1236 changes=None, stat=False, fp=None, prefix='',
1237 1237 root='', listsubrepos=False):
1238 1238 '''show diff or diffstat.'''
1239 1239 if fp is None:
1240 1240 write = ui.write
1241 1241 else:
1242 1242 def write(s, **kw):
1243 1243 fp.write(s)
1244 1244
1245 1245 if root:
1246 1246 relroot = pathutil.canonpath(repo.root, repo.getcwd(), root)
1247 1247 else:
1248 1248 relroot = ''
1249 1249 if relroot != '':
1250 1250 # XXX relative roots currently don't work if the root is within a
1251 1251 # subrepo
1252 1252 uirelroot = match.uipath(relroot)
1253 1253 relroot += '/'
1254 1254 for matchroot in match.files():
1255 1255 if not matchroot.startswith(relroot):
1256 1256 ui.warn(_('warning: %s not inside relative root %s\n') % (
1257 1257 match.uipath(matchroot), uirelroot))
1258 1258
1259 1259 if stat:
1260 1260 diffopts = diffopts.copy(context=0)
1261 1261 width = 80
1262 1262 if not ui.plain():
1263 1263 width = ui.termwidth()
1264 1264 chunks = patch.diff(repo, node1, node2, match, changes, diffopts,
1265 1265 prefix=prefix, relroot=relroot)
1266 1266 for chunk, label in patch.diffstatui(util.iterlines(chunks),
1267 1267 width=width):
1268 1268 write(chunk, label=label)
1269 1269 else:
1270 1270 for chunk, label in patch.diffui(repo, node1, node2, match,
1271 1271 changes, diffopts, prefix=prefix,
1272 1272 relroot=relroot):
1273 1273 write(chunk, label=label)
1274 1274
1275 1275 if listsubrepos:
1276 1276 ctx1 = repo[node1]
1277 1277 ctx2 = repo[node2]
1278 1278 for subpath, sub in scmutil.itersubrepos(ctx1, ctx2):
1279 1279 tempnode2 = node2
1280 1280 try:
1281 1281 if node2 is not None:
1282 1282 tempnode2 = ctx2.substate[subpath][1]
1283 1283 except KeyError:
1284 1284 # A subrepo that existed in node1 was deleted between node1 and
1285 1285 # node2 (inclusive). Thus, ctx2's substate won't contain that
1286 1286 # subpath. The best we can do is to ignore it.
1287 1287 tempnode2 = None
1288 1288 submatch = matchmod.subdirmatcher(subpath, match)
1289 1289 sub.diff(ui, diffopts, tempnode2, submatch, changes=changes,
1290 1290 stat=stat, fp=fp, prefix=prefix)
1291 1291
1292 1292 def _changesetlabels(ctx):
1293 1293 labels = ['log.changeset', 'changeset.%s' % ctx.phasestr()]
1294 1294 if ctx.obsolete():
1295 1295 labels.append('changeset.obsolete')
1296 1296 if ctx.troubled():
1297 1297 labels.append('changeset.troubled')
1298 1298 for trouble in ctx.troubles():
1299 1299 labels.append('trouble.%s' % trouble)
1300 1300 return ' '.join(labels)
1301 1301
1302 1302 class changeset_printer(object):
1303 1303 '''show changeset information when templating not requested.'''
1304 1304
1305 1305 def __init__(self, ui, repo, matchfn, diffopts, buffered):
1306 1306 self.ui = ui
1307 1307 self.repo = repo
1308 1308 self.buffered = buffered
1309 1309 self.matchfn = matchfn
1310 1310 self.diffopts = diffopts
1311 1311 self.header = {}
1312 1312 self.hunk = {}
1313 1313 self.lastheader = None
1314 1314 self.footer = None
1315 1315
1316 1316 def flush(self, ctx):
1317 1317 rev = ctx.rev()
1318 1318 if rev in self.header:
1319 1319 h = self.header[rev]
1320 1320 if h != self.lastheader:
1321 1321 self.lastheader = h
1322 1322 self.ui.write(h)
1323 1323 del self.header[rev]
1324 1324 if rev in self.hunk:
1325 1325 self.ui.write(self.hunk[rev])
1326 1326 del self.hunk[rev]
1327 1327 return 1
1328 1328 return 0
1329 1329
1330 1330 def close(self):
1331 1331 if self.footer:
1332 1332 self.ui.write(self.footer)
1333 1333
1334 1334 def show(self, ctx, copies=None, matchfn=None, **props):
1335 1335 props = pycompat.byteskwargs(props)
1336 1336 if self.buffered:
1337 1337 self.ui.pushbuffer(labeled=True)
1338 1338 self._show(ctx, copies, matchfn, props)
1339 1339 self.hunk[ctx.rev()] = self.ui.popbuffer()
1340 1340 else:
1341 1341 self._show(ctx, copies, matchfn, props)
1342 1342
1343 1343 def _show(self, ctx, copies, matchfn, props):
1344 1344 '''show a single changeset or file revision'''
1345 1345 changenode = ctx.node()
1346 1346 rev = ctx.rev()
1347 1347 if self.ui.debugflag:
1348 1348 hexfunc = hex
1349 1349 else:
1350 1350 hexfunc = short
1351 1351 # as of now, wctx.node() and wctx.rev() return None, but we want to
1352 1352 # show the same values as {node} and {rev} templatekw
1353 1353 revnode = (scmutil.intrev(ctx), hexfunc(scmutil.binnode(ctx)))
1354 1354
1355 1355 if self.ui.quiet:
1356 1356 self.ui.write("%d:%s\n" % revnode, label='log.node')
1357 1357 return
1358 1358
1359 1359 date = util.datestr(ctx.date())
1360 1360
1361 1361 # i18n: column positioning for "hg log"
1362 1362 self.ui.write(_("changeset: %d:%s\n") % revnode,
1363 1363 label=_changesetlabels(ctx))
1364 1364
1365 1365 # branches are shown first before any other names due to backwards
1366 1366 # compatibility
1367 1367 branch = ctx.branch()
1368 1368 # don't show the default branch name
1369 1369 if branch != 'default':
1370 1370 # i18n: column positioning for "hg log"
1371 1371 self.ui.write(_("branch: %s\n") % branch,
1372 1372 label='log.branch')
1373 1373
1374 1374 for nsname, ns in self.repo.names.iteritems():
1375 1375 # branches has special logic already handled above, so here we just
1376 1376 # skip it
1377 1377 if nsname == 'branches':
1378 1378 continue
1379 1379 # we will use the templatename as the color name since those two
1380 1380 # should be the same
1381 1381 for name in ns.names(self.repo, changenode):
1382 1382 self.ui.write(ns.logfmt % name,
1383 1383 label='log.%s' % ns.colorname)
1384 1384 if self.ui.debugflag:
1385 1385 # i18n: column positioning for "hg log"
1386 1386 self.ui.write(_("phase: %s\n") % ctx.phasestr(),
1387 1387 label='log.phase')
1388 1388 for pctx in scmutil.meaningfulparents(self.repo, ctx):
1389 1389 label = 'log.parent changeset.%s' % pctx.phasestr()
1390 1390 # i18n: column positioning for "hg log"
1391 1391 self.ui.write(_("parent: %d:%s\n")
1392 1392 % (pctx.rev(), hexfunc(pctx.node())),
1393 1393 label=label)
1394 1394
1395 1395 if self.ui.debugflag and rev is not None:
1396 1396 mnode = ctx.manifestnode()
1397 1397 # i18n: column positioning for "hg log"
1398 1398 self.ui.write(_("manifest: %d:%s\n") %
1399 1399 (self.repo.manifestlog._revlog.rev(mnode),
1400 1400 hex(mnode)),
1401 1401 label='ui.debug log.manifest')
1402 1402 # i18n: column positioning for "hg log"
1403 1403 self.ui.write(_("user: %s\n") % ctx.user(),
1404 1404 label='log.user')
1405 1405 # i18n: column positioning for "hg log"
1406 1406 self.ui.write(_("date: %s\n") % date,
1407 1407 label='log.date')
1408 1408
1409 1409 if ctx.troubled():
1410 1410 # i18n: column positioning for "hg log"
1411 1411 self.ui.write(_("trouble: %s\n") % ', '.join(ctx.troubles()),
1412 1412 label='log.trouble')
1413 1413
1414 self._exthook(ctx)
1415
1414 1416 if self.ui.debugflag:
1415 1417 files = ctx.p1().status(ctx)[:3]
1416 1418 for key, value in zip([# i18n: column positioning for "hg log"
1417 1419 _("files:"),
1418 1420 # i18n: column positioning for "hg log"
1419 1421 _("files+:"),
1420 1422 # i18n: column positioning for "hg log"
1421 1423 _("files-:")], files):
1422 1424 if value:
1423 1425 self.ui.write("%-12s %s\n" % (key, " ".join(value)),
1424 1426 label='ui.debug log.files')
1425 1427 elif ctx.files() and self.ui.verbose:
1426 1428 # i18n: column positioning for "hg log"
1427 1429 self.ui.write(_("files: %s\n") % " ".join(ctx.files()),
1428 1430 label='ui.note log.files')
1429 1431 if copies and self.ui.verbose:
1430 1432 copies = ['%s (%s)' % c for c in copies]
1431 1433 # i18n: column positioning for "hg log"
1432 1434 self.ui.write(_("copies: %s\n") % ' '.join(copies),
1433 1435 label='ui.note log.copies')
1434 1436
1435 1437 extra = ctx.extra()
1436 1438 if extra and self.ui.debugflag:
1437 1439 for key, value in sorted(extra.items()):
1438 1440 # i18n: column positioning for "hg log"
1439 1441 self.ui.write(_("extra: %s=%s\n")
1440 1442 % (key, util.escapestr(value)),
1441 1443 label='ui.debug log.extra')
1442 1444
1443 1445 description = ctx.description().strip()
1444 1446 if description:
1445 1447 if self.ui.verbose:
1446 1448 self.ui.write(_("description:\n"),
1447 1449 label='ui.note log.description')
1448 1450 self.ui.write(description,
1449 1451 label='ui.note log.description')
1450 1452 self.ui.write("\n\n")
1451 1453 else:
1452 1454 # i18n: column positioning for "hg log"
1453 1455 self.ui.write(_("summary: %s\n") %
1454 1456 description.splitlines()[0],
1455 1457 label='log.summary')
1456 1458 self.ui.write("\n")
1457 1459
1458 1460 self.showpatch(ctx, matchfn)
1459 1461
1462 def _exthook(self, ctx):
1463 '''empty method used by extension as a hook point
1464 '''
1465 pass
1466
1460 1467 def showpatch(self, ctx, matchfn):
1461 1468 if not matchfn:
1462 1469 matchfn = self.matchfn
1463 1470 if matchfn:
1464 1471 stat = self.diffopts.get('stat')
1465 1472 diff = self.diffopts.get('patch')
1466 1473 diffopts = patch.diffallopts(self.ui, self.diffopts)
1467 1474 node = ctx.node()
1468 1475 prev = ctx.p1().node()
1469 1476 if stat:
1470 1477 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
1471 1478 match=matchfn, stat=True)
1472 1479 if diff:
1473 1480 if stat:
1474 1481 self.ui.write("\n")
1475 1482 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
1476 1483 match=matchfn, stat=False)
1477 1484 self.ui.write("\n")
1478 1485
1479 1486 class jsonchangeset(changeset_printer):
1480 1487 '''format changeset information.'''
1481 1488
1482 1489 def __init__(self, ui, repo, matchfn, diffopts, buffered):
1483 1490 changeset_printer.__init__(self, ui, repo, matchfn, diffopts, buffered)
1484 1491 self.cache = {}
1485 1492 self._first = True
1486 1493
1487 1494 def close(self):
1488 1495 if not self._first:
1489 1496 self.ui.write("\n]\n")
1490 1497 else:
1491 1498 self.ui.write("[]\n")
1492 1499
1493 1500 def _show(self, ctx, copies, matchfn, props):
1494 1501 '''show a single changeset or file revision'''
1495 1502 rev = ctx.rev()
1496 1503 if rev is None:
1497 1504 jrev = jnode = 'null'
1498 1505 else:
1499 1506 jrev = '%d' % rev
1500 1507 jnode = '"%s"' % hex(ctx.node())
1501 1508 j = encoding.jsonescape
1502 1509
1503 1510 if self._first:
1504 1511 self.ui.write("[\n {")
1505 1512 self._first = False
1506 1513 else:
1507 1514 self.ui.write(",\n {")
1508 1515
1509 1516 if self.ui.quiet:
1510 1517 self.ui.write(('\n "rev": %s') % jrev)
1511 1518 self.ui.write((',\n "node": %s') % jnode)
1512 1519 self.ui.write('\n }')
1513 1520 return
1514 1521
1515 1522 self.ui.write(('\n "rev": %s') % jrev)
1516 1523 self.ui.write((',\n "node": %s') % jnode)
1517 1524 self.ui.write((',\n "branch": "%s"') % j(ctx.branch()))
1518 1525 self.ui.write((',\n "phase": "%s"') % ctx.phasestr())
1519 1526 self.ui.write((',\n "user": "%s"') % j(ctx.user()))
1520 1527 self.ui.write((',\n "date": [%d, %d]') % ctx.date())
1521 1528 self.ui.write((',\n "desc": "%s"') % j(ctx.description()))
1522 1529
1523 1530 self.ui.write((',\n "bookmarks": [%s]') %
1524 1531 ", ".join('"%s"' % j(b) for b in ctx.bookmarks()))
1525 1532 self.ui.write((',\n "tags": [%s]') %
1526 1533 ", ".join('"%s"' % j(t) for t in ctx.tags()))
1527 1534 self.ui.write((',\n "parents": [%s]') %
1528 1535 ", ".join('"%s"' % c.hex() for c in ctx.parents()))
1529 1536
1530 1537 if self.ui.debugflag:
1531 1538 if rev is None:
1532 1539 jmanifestnode = 'null'
1533 1540 else:
1534 1541 jmanifestnode = '"%s"' % hex(ctx.manifestnode())
1535 1542 self.ui.write((',\n "manifest": %s') % jmanifestnode)
1536 1543
1537 1544 self.ui.write((',\n "extra": {%s}') %
1538 1545 ", ".join('"%s": "%s"' % (j(k), j(v))
1539 1546 for k, v in ctx.extra().items()))
1540 1547
1541 1548 files = ctx.p1().status(ctx)
1542 1549 self.ui.write((',\n "modified": [%s]') %
1543 1550 ", ".join('"%s"' % j(f) for f in files[0]))
1544 1551 self.ui.write((',\n "added": [%s]') %
1545 1552 ", ".join('"%s"' % j(f) for f in files[1]))
1546 1553 self.ui.write((',\n "removed": [%s]') %
1547 1554 ", ".join('"%s"' % j(f) for f in files[2]))
1548 1555
1549 1556 elif self.ui.verbose:
1550 1557 self.ui.write((',\n "files": [%s]') %
1551 1558 ", ".join('"%s"' % j(f) for f in ctx.files()))
1552 1559
1553 1560 if copies:
1554 1561 self.ui.write((',\n "copies": {%s}') %
1555 1562 ", ".join('"%s": "%s"' % (j(k), j(v))
1556 1563 for k, v in copies))
1557 1564
1558 1565 matchfn = self.matchfn
1559 1566 if matchfn:
1560 1567 stat = self.diffopts.get('stat')
1561 1568 diff = self.diffopts.get('patch')
1562 1569 diffopts = patch.difffeatureopts(self.ui, self.diffopts, git=True)
1563 1570 node, prev = ctx.node(), ctx.p1().node()
1564 1571 if stat:
1565 1572 self.ui.pushbuffer()
1566 1573 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
1567 1574 match=matchfn, stat=True)
1568 1575 self.ui.write((',\n "diffstat": "%s"')
1569 1576 % j(self.ui.popbuffer()))
1570 1577 if diff:
1571 1578 self.ui.pushbuffer()
1572 1579 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
1573 1580 match=matchfn, stat=False)
1574 1581 self.ui.write((',\n "diff": "%s"') % j(self.ui.popbuffer()))
1575 1582
1576 1583 self.ui.write("\n }")
1577 1584
1578 1585 class changeset_templater(changeset_printer):
1579 1586 '''format changeset information.'''
1580 1587
1581 1588 # Arguments before "buffered" used to be positional. Consider not
1582 1589 # adding/removing arguments before "buffered" to not break callers.
1583 1590 def __init__(self, ui, repo, tmplspec, matchfn=None, diffopts=None,
1584 1591 buffered=False):
1585 1592 diffopts = diffopts or {}
1586 1593
1587 1594 changeset_printer.__init__(self, ui, repo, matchfn, diffopts, buffered)
1588 1595 self.t = formatter.loadtemplater(ui, tmplspec,
1589 1596 cache=templatekw.defaulttempl)
1590 1597 self._counter = itertools.count()
1591 1598 self.cache = {}
1592 1599
1593 1600 self._tref = tmplspec.ref
1594 1601 self._parts = {'header': '', 'footer': '',
1595 1602 tmplspec.ref: tmplspec.ref,
1596 1603 'docheader': '', 'docfooter': '',
1597 1604 'separator': ''}
1598 1605 if tmplspec.mapfile:
1599 1606 # find correct templates for current mode, for backward
1600 1607 # compatibility with 'log -v/-q/--debug' using a mapfile
1601 1608 tmplmodes = [
1602 1609 (True, ''),
1603 1610 (self.ui.verbose, '_verbose'),
1604 1611 (self.ui.quiet, '_quiet'),
1605 1612 (self.ui.debugflag, '_debug'),
1606 1613 ]
1607 1614 for mode, postfix in tmplmodes:
1608 1615 for t in self._parts:
1609 1616 cur = t + postfix
1610 1617 if mode and cur in self.t:
1611 1618 self._parts[t] = cur
1612 1619 else:
1613 1620 partnames = [p for p in self._parts.keys() if p != tmplspec.ref]
1614 1621 m = formatter.templatepartsmap(tmplspec, self.t, partnames)
1615 1622 self._parts.update(m)
1616 1623
1617 1624 if self._parts['docheader']:
1618 1625 self.ui.write(templater.stringify(self.t(self._parts['docheader'])))
1619 1626
1620 1627 def close(self):
1621 1628 if self._parts['docfooter']:
1622 1629 if not self.footer:
1623 1630 self.footer = ""
1624 1631 self.footer += templater.stringify(self.t(self._parts['docfooter']))
1625 1632 return super(changeset_templater, self).close()
1626 1633
1627 1634 def _show(self, ctx, copies, matchfn, props):
1628 1635 '''show a single changeset or file revision'''
1629 1636 props = props.copy()
1630 1637 props.update(templatekw.keywords)
1631 1638 props['templ'] = self.t
1632 1639 props['ctx'] = ctx
1633 1640 props['repo'] = self.repo
1634 1641 props['ui'] = self.repo.ui
1635 1642 props['index'] = index = next(self._counter)
1636 1643 props['revcache'] = {'copies': copies}
1637 1644 props['cache'] = self.cache
1638 1645 props = pycompat.strkwargs(props)
1639 1646
1640 1647 # write separator, which wouldn't work well with the header part below
1641 1648 # since there's inherently a conflict between header (across items) and
1642 1649 # separator (per item)
1643 1650 if self._parts['separator'] and index > 0:
1644 1651 self.ui.write(templater.stringify(self.t(self._parts['separator'])))
1645 1652
1646 1653 # write header
1647 1654 if self._parts['header']:
1648 1655 h = templater.stringify(self.t(self._parts['header'], **props))
1649 1656 if self.buffered:
1650 1657 self.header[ctx.rev()] = h
1651 1658 else:
1652 1659 if self.lastheader != h:
1653 1660 self.lastheader = h
1654 1661 self.ui.write(h)
1655 1662
1656 1663 # write changeset metadata, then patch if requested
1657 1664 key = self._parts[self._tref]
1658 1665 self.ui.write(templater.stringify(self.t(key, **props)))
1659 1666 self.showpatch(ctx, matchfn)
1660 1667
1661 1668 if self._parts['footer']:
1662 1669 if not self.footer:
1663 1670 self.footer = templater.stringify(
1664 1671 self.t(self._parts['footer'], **props))
1665 1672
1666 1673 def logtemplatespec(tmpl, mapfile):
1667 1674 if mapfile:
1668 1675 return formatter.templatespec('changeset', tmpl, mapfile)
1669 1676 else:
1670 1677 return formatter.templatespec('', tmpl, None)
1671 1678
1672 1679 def _lookuplogtemplate(ui, tmpl, style):
1673 1680 """Find the template matching the given template spec or style
1674 1681
1675 1682 See formatter.lookuptemplate() for details.
1676 1683 """
1677 1684
1678 1685 # ui settings
1679 1686 if not tmpl and not style: # template are stronger than style
1680 1687 tmpl = ui.config('ui', 'logtemplate')
1681 1688 if tmpl:
1682 1689 return logtemplatespec(templater.unquotestring(tmpl), None)
1683 1690 else:
1684 1691 style = util.expandpath(ui.config('ui', 'style', ''))
1685 1692
1686 1693 if not tmpl and style:
1687 1694 mapfile = style
1688 1695 if not os.path.split(mapfile)[0]:
1689 1696 mapname = (templater.templatepath('map-cmdline.' + mapfile)
1690 1697 or templater.templatepath(mapfile))
1691 1698 if mapname:
1692 1699 mapfile = mapname
1693 1700 return logtemplatespec(None, mapfile)
1694 1701
1695 1702 if not tmpl:
1696 1703 return logtemplatespec(None, None)
1697 1704
1698 1705 return formatter.lookuptemplate(ui, 'changeset', tmpl)
1699 1706
1700 1707 def makelogtemplater(ui, repo, tmpl, buffered=False):
1701 1708 """Create a changeset_templater from a literal template 'tmpl'"""
1702 1709 spec = logtemplatespec(tmpl, None)
1703 1710 return changeset_templater(ui, repo, spec, buffered=buffered)
1704 1711
1705 1712 def show_changeset(ui, repo, opts, buffered=False):
1706 1713 """show one changeset using template or regular display.
1707 1714
1708 1715 Display format will be the first non-empty hit of:
1709 1716 1. option 'template'
1710 1717 2. option 'style'
1711 1718 3. [ui] setting 'logtemplate'
1712 1719 4. [ui] setting 'style'
1713 1720 If all of these values are either the unset or the empty string,
1714 1721 regular display via changeset_printer() is done.
1715 1722 """
1716 1723 # options
1717 1724 matchfn = None
1718 1725 if opts.get('patch') or opts.get('stat'):
1719 1726 matchfn = scmutil.matchall(repo)
1720 1727
1721 1728 if opts.get('template') == 'json':
1722 1729 return jsonchangeset(ui, repo, matchfn, opts, buffered)
1723 1730
1724 1731 spec = _lookuplogtemplate(ui, opts.get('template'), opts.get('style'))
1725 1732
1726 1733 if not spec.ref and not spec.tmpl and not spec.mapfile:
1727 1734 return changeset_printer(ui, repo, matchfn, opts, buffered)
1728 1735
1729 1736 return changeset_templater(ui, repo, spec, matchfn, opts, buffered)
1730 1737
1731 1738 def showmarker(fm, marker, index=None):
1732 1739 """utility function to display obsolescence marker in a readable way
1733 1740
1734 1741 To be used by debug function."""
1735 1742 if index is not None:
1736 1743 fm.write('index', '%i ', index)
1737 1744 fm.write('precnode', '%s ', hex(marker.precnode()))
1738 1745 succs = marker.succnodes()
1739 1746 fm.condwrite(succs, 'succnodes', '%s ',
1740 1747 fm.formatlist(map(hex, succs), name='node'))
1741 1748 fm.write('flag', '%X ', marker.flags())
1742 1749 parents = marker.parentnodes()
1743 1750 if parents is not None:
1744 1751 fm.write('parentnodes', '{%s} ',
1745 1752 fm.formatlist(map(hex, parents), name='node', sep=', '))
1746 1753 fm.write('date', '(%s) ', fm.formatdate(marker.date()))
1747 1754 meta = marker.metadata().copy()
1748 1755 meta.pop('date', None)
1749 1756 fm.write('metadata', '{%s}', fm.formatdict(meta, fmt='%r: %r', sep=', '))
1750 1757 fm.plain('\n')
1751 1758
1752 1759 def finddate(ui, repo, date):
1753 1760 """Find the tipmost changeset that matches the given date spec"""
1754 1761
1755 1762 df = util.matchdate(date)
1756 1763 m = scmutil.matchall(repo)
1757 1764 results = {}
1758 1765
1759 1766 def prep(ctx, fns):
1760 1767 d = ctx.date()
1761 1768 if df(d[0]):
1762 1769 results[ctx.rev()] = d
1763 1770
1764 1771 for ctx in walkchangerevs(repo, m, {'rev': None}, prep):
1765 1772 rev = ctx.rev()
1766 1773 if rev in results:
1767 1774 ui.status(_("found revision %s from %s\n") %
1768 1775 (rev, util.datestr(results[rev])))
1769 1776 return '%d' % rev
1770 1777
1771 1778 raise error.Abort(_("revision matching date not found"))
1772 1779
1773 1780 def increasingwindows(windowsize=8, sizelimit=512):
1774 1781 while True:
1775 1782 yield windowsize
1776 1783 if windowsize < sizelimit:
1777 1784 windowsize *= 2
1778 1785
1779 1786 class FileWalkError(Exception):
1780 1787 pass
1781 1788
1782 1789 def walkfilerevs(repo, match, follow, revs, fncache):
1783 1790 '''Walks the file history for the matched files.
1784 1791
1785 1792 Returns the changeset revs that are involved in the file history.
1786 1793
1787 1794 Throws FileWalkError if the file history can't be walked using
1788 1795 filelogs alone.
1789 1796 '''
1790 1797 wanted = set()
1791 1798 copies = []
1792 1799 minrev, maxrev = min(revs), max(revs)
1793 1800 def filerevgen(filelog, last):
1794 1801 """
1795 1802 Only files, no patterns. Check the history of each file.
1796 1803
1797 1804 Examines filelog entries within minrev, maxrev linkrev range
1798 1805 Returns an iterator yielding (linkrev, parentlinkrevs, copied)
1799 1806 tuples in backwards order
1800 1807 """
1801 1808 cl_count = len(repo)
1802 1809 revs = []
1803 1810 for j in xrange(0, last + 1):
1804 1811 linkrev = filelog.linkrev(j)
1805 1812 if linkrev < minrev:
1806 1813 continue
1807 1814 # only yield rev for which we have the changelog, it can
1808 1815 # happen while doing "hg log" during a pull or commit
1809 1816 if linkrev >= cl_count:
1810 1817 break
1811 1818
1812 1819 parentlinkrevs = []
1813 1820 for p in filelog.parentrevs(j):
1814 1821 if p != nullrev:
1815 1822 parentlinkrevs.append(filelog.linkrev(p))
1816 1823 n = filelog.node(j)
1817 1824 revs.append((linkrev, parentlinkrevs,
1818 1825 follow and filelog.renamed(n)))
1819 1826
1820 1827 return reversed(revs)
1821 1828 def iterfiles():
1822 1829 pctx = repo['.']
1823 1830 for filename in match.files():
1824 1831 if follow:
1825 1832 if filename not in pctx:
1826 1833 raise error.Abort(_('cannot follow file not in parent '
1827 1834 'revision: "%s"') % filename)
1828 1835 yield filename, pctx[filename].filenode()
1829 1836 else:
1830 1837 yield filename, None
1831 1838 for filename_node in copies:
1832 1839 yield filename_node
1833 1840
1834 1841 for file_, node in iterfiles():
1835 1842 filelog = repo.file(file_)
1836 1843 if not len(filelog):
1837 1844 if node is None:
1838 1845 # A zero count may be a directory or deleted file, so
1839 1846 # try to find matching entries on the slow path.
1840 1847 if follow:
1841 1848 raise error.Abort(
1842 1849 _('cannot follow nonexistent file: "%s"') % file_)
1843 1850 raise FileWalkError("Cannot walk via filelog")
1844 1851 else:
1845 1852 continue
1846 1853
1847 1854 if node is None:
1848 1855 last = len(filelog) - 1
1849 1856 else:
1850 1857 last = filelog.rev(node)
1851 1858
1852 1859 # keep track of all ancestors of the file
1853 1860 ancestors = {filelog.linkrev(last)}
1854 1861
1855 1862 # iterate from latest to oldest revision
1856 1863 for rev, flparentlinkrevs, copied in filerevgen(filelog, last):
1857 1864 if not follow:
1858 1865 if rev > maxrev:
1859 1866 continue
1860 1867 else:
1861 1868 # Note that last might not be the first interesting
1862 1869 # rev to us:
1863 1870 # if the file has been changed after maxrev, we'll
1864 1871 # have linkrev(last) > maxrev, and we still need
1865 1872 # to explore the file graph
1866 1873 if rev not in ancestors:
1867 1874 continue
1868 1875 # XXX insert 1327 fix here
1869 1876 if flparentlinkrevs:
1870 1877 ancestors.update(flparentlinkrevs)
1871 1878
1872 1879 fncache.setdefault(rev, []).append(file_)
1873 1880 wanted.add(rev)
1874 1881 if copied:
1875 1882 copies.append(copied)
1876 1883
1877 1884 return wanted
1878 1885
1879 1886 class _followfilter(object):
1880 1887 def __init__(self, repo, onlyfirst=False):
1881 1888 self.repo = repo
1882 1889 self.startrev = nullrev
1883 1890 self.roots = set()
1884 1891 self.onlyfirst = onlyfirst
1885 1892
1886 1893 def match(self, rev):
1887 1894 def realparents(rev):
1888 1895 if self.onlyfirst:
1889 1896 return self.repo.changelog.parentrevs(rev)[0:1]
1890 1897 else:
1891 1898 return filter(lambda x: x != nullrev,
1892 1899 self.repo.changelog.parentrevs(rev))
1893 1900
1894 1901 if self.startrev == nullrev:
1895 1902 self.startrev = rev
1896 1903 return True
1897 1904
1898 1905 if rev > self.startrev:
1899 1906 # forward: all descendants
1900 1907 if not self.roots:
1901 1908 self.roots.add(self.startrev)
1902 1909 for parent in realparents(rev):
1903 1910 if parent in self.roots:
1904 1911 self.roots.add(rev)
1905 1912 return True
1906 1913 else:
1907 1914 # backwards: all parents
1908 1915 if not self.roots:
1909 1916 self.roots.update(realparents(self.startrev))
1910 1917 if rev in self.roots:
1911 1918 self.roots.remove(rev)
1912 1919 self.roots.update(realparents(rev))
1913 1920 return True
1914 1921
1915 1922 return False
1916 1923
1917 1924 def walkchangerevs(repo, match, opts, prepare):
1918 1925 '''Iterate over files and the revs in which they changed.
1919 1926
1920 1927 Callers most commonly need to iterate backwards over the history
1921 1928 in which they are interested. Doing so has awful (quadratic-looking)
1922 1929 performance, so we use iterators in a "windowed" way.
1923 1930
1924 1931 We walk a window of revisions in the desired order. Within the
1925 1932 window, we first walk forwards to gather data, then in the desired
1926 1933 order (usually backwards) to display it.
1927 1934
1928 1935 This function returns an iterator yielding contexts. Before
1929 1936 yielding each context, the iterator will first call the prepare
1930 1937 function on each context in the window in forward order.'''
1931 1938
1932 1939 follow = opts.get('follow') or opts.get('follow_first')
1933 1940 revs = _logrevs(repo, opts)
1934 1941 if not revs:
1935 1942 return []
1936 1943 wanted = set()
1937 1944 slowpath = match.anypats() or ((match.isexact() or match.prefix()) and
1938 1945 opts.get('removed'))
1939 1946 fncache = {}
1940 1947 change = repo.changectx
1941 1948
1942 1949 # First step is to fill wanted, the set of revisions that we want to yield.
1943 1950 # When it does not induce extra cost, we also fill fncache for revisions in
1944 1951 # wanted: a cache of filenames that were changed (ctx.files()) and that
1945 1952 # match the file filtering conditions.
1946 1953
1947 1954 if match.always():
1948 1955 # No files, no patterns. Display all revs.
1949 1956 wanted = revs
1950 1957 elif not slowpath:
1951 1958 # We only have to read through the filelog to find wanted revisions
1952 1959
1953 1960 try:
1954 1961 wanted = walkfilerevs(repo, match, follow, revs, fncache)
1955 1962 except FileWalkError:
1956 1963 slowpath = True
1957 1964
1958 1965 # We decided to fall back to the slowpath because at least one
1959 1966 # of the paths was not a file. Check to see if at least one of them
1960 1967 # existed in history, otherwise simply return
1961 1968 for path in match.files():
1962 1969 if path == '.' or path in repo.store:
1963 1970 break
1964 1971 else:
1965 1972 return []
1966 1973
1967 1974 if slowpath:
1968 1975 # We have to read the changelog to match filenames against
1969 1976 # changed files
1970 1977
1971 1978 if follow:
1972 1979 raise error.Abort(_('can only follow copies/renames for explicit '
1973 1980 'filenames'))
1974 1981
1975 1982 # The slow path checks files modified in every changeset.
1976 1983 # This is really slow on large repos, so compute the set lazily.
1977 1984 class lazywantedset(object):
1978 1985 def __init__(self):
1979 1986 self.set = set()
1980 1987 self.revs = set(revs)
1981 1988
1982 1989 # No need to worry about locality here because it will be accessed
1983 1990 # in the same order as the increasing window below.
1984 1991 def __contains__(self, value):
1985 1992 if value in self.set:
1986 1993 return True
1987 1994 elif not value in self.revs:
1988 1995 return False
1989 1996 else:
1990 1997 self.revs.discard(value)
1991 1998 ctx = change(value)
1992 1999 matches = filter(match, ctx.files())
1993 2000 if matches:
1994 2001 fncache[value] = matches
1995 2002 self.set.add(value)
1996 2003 return True
1997 2004 return False
1998 2005
1999 2006 def discard(self, value):
2000 2007 self.revs.discard(value)
2001 2008 self.set.discard(value)
2002 2009
2003 2010 wanted = lazywantedset()
2004 2011
2005 2012 # it might be worthwhile to do this in the iterator if the rev range
2006 2013 # is descending and the prune args are all within that range
2007 2014 for rev in opts.get('prune', ()):
2008 2015 rev = repo[rev].rev()
2009 2016 ff = _followfilter(repo)
2010 2017 stop = min(revs[0], revs[-1])
2011 2018 for x in xrange(rev, stop - 1, -1):
2012 2019 if ff.match(x):
2013 2020 wanted = wanted - [x]
2014 2021
2015 2022 # Now that wanted is correctly initialized, we can iterate over the
2016 2023 # revision range, yielding only revisions in wanted.
2017 2024 def iterate():
2018 2025 if follow and match.always():
2019 2026 ff = _followfilter(repo, onlyfirst=opts.get('follow_first'))
2020 2027 def want(rev):
2021 2028 return ff.match(rev) and rev in wanted
2022 2029 else:
2023 2030 def want(rev):
2024 2031 return rev in wanted
2025 2032
2026 2033 it = iter(revs)
2027 2034 stopiteration = False
2028 2035 for windowsize in increasingwindows():
2029 2036 nrevs = []
2030 2037 for i in xrange(windowsize):
2031 2038 rev = next(it, None)
2032 2039 if rev is None:
2033 2040 stopiteration = True
2034 2041 break
2035 2042 elif want(rev):
2036 2043 nrevs.append(rev)
2037 2044 for rev in sorted(nrevs):
2038 2045 fns = fncache.get(rev)
2039 2046 ctx = change(rev)
2040 2047 if not fns:
2041 2048 def fns_generator():
2042 2049 for f in ctx.files():
2043 2050 if match(f):
2044 2051 yield f
2045 2052 fns = fns_generator()
2046 2053 prepare(ctx, fns)
2047 2054 for rev in nrevs:
2048 2055 yield change(rev)
2049 2056
2050 2057 if stopiteration:
2051 2058 break
2052 2059
2053 2060 return iterate()
2054 2061
2055 2062 def _makefollowlogfilematcher(repo, files, followfirst):
2056 2063 # When displaying a revision with --patch --follow FILE, we have
2057 2064 # to know which file of the revision must be diffed. With
2058 2065 # --follow, we want the names of the ancestors of FILE in the
2059 2066 # revision, stored in "fcache". "fcache" is populated by
2060 2067 # reproducing the graph traversal already done by --follow revset
2061 2068 # and relating revs to file names (which is not "correct" but
2062 2069 # good enough).
2063 2070 fcache = {}
2064 2071 fcacheready = [False]
2065 2072 pctx = repo['.']
2066 2073
2067 2074 def populate():
2068 2075 for fn in files:
2069 2076 fctx = pctx[fn]
2070 2077 fcache.setdefault(fctx.introrev(), set()).add(fctx.path())
2071 2078 for c in fctx.ancestors(followfirst=followfirst):
2072 2079 fcache.setdefault(c.rev(), set()).add(c.path())
2073 2080
2074 2081 def filematcher(rev):
2075 2082 if not fcacheready[0]:
2076 2083 # Lazy initialization
2077 2084 fcacheready[0] = True
2078 2085 populate()
2079 2086 return scmutil.matchfiles(repo, fcache.get(rev, []))
2080 2087
2081 2088 return filematcher
2082 2089
2083 2090 def _makenofollowlogfilematcher(repo, pats, opts):
2084 2091 '''hook for extensions to override the filematcher for non-follow cases'''
2085 2092 return None
2086 2093
2087 2094 def _makelogrevset(repo, pats, opts, revs):
2088 2095 """Return (expr, filematcher) where expr is a revset string built
2089 2096 from log options and file patterns or None. If --stat or --patch
2090 2097 are not passed filematcher is None. Otherwise it is a callable
2091 2098 taking a revision number and returning a match objects filtering
2092 2099 the files to be detailed when displaying the revision.
2093 2100 """
2094 2101 opt2revset = {
2095 2102 'no_merges': ('not merge()', None),
2096 2103 'only_merges': ('merge()', None),
2097 2104 '_ancestors': ('ancestors(%(val)s)', None),
2098 2105 '_fancestors': ('_firstancestors(%(val)s)', None),
2099 2106 '_descendants': ('descendants(%(val)s)', None),
2100 2107 '_fdescendants': ('_firstdescendants(%(val)s)', None),
2101 2108 '_matchfiles': ('_matchfiles(%(val)s)', None),
2102 2109 'date': ('date(%(val)r)', None),
2103 2110 'branch': ('branch(%(val)r)', ' or '),
2104 2111 '_patslog': ('filelog(%(val)r)', ' or '),
2105 2112 '_patsfollow': ('follow(%(val)r)', ' or '),
2106 2113 '_patsfollowfirst': ('_followfirst(%(val)r)', ' or '),
2107 2114 'keyword': ('keyword(%(val)r)', ' or '),
2108 2115 'prune': ('not (%(val)r or ancestors(%(val)r))', ' and '),
2109 2116 'user': ('user(%(val)r)', ' or '),
2110 2117 }
2111 2118
2112 2119 opts = dict(opts)
2113 2120 # follow or not follow?
2114 2121 follow = opts.get('follow') or opts.get('follow_first')
2115 2122 if opts.get('follow_first'):
2116 2123 followfirst = 1
2117 2124 else:
2118 2125 followfirst = 0
2119 2126 # --follow with FILE behavior depends on revs...
2120 2127 it = iter(revs)
2121 2128 startrev = next(it)
2122 2129 followdescendants = startrev < next(it, startrev)
2123 2130
2124 2131 # branch and only_branch are really aliases and must be handled at
2125 2132 # the same time
2126 2133 opts['branch'] = opts.get('branch', []) + opts.get('only_branch', [])
2127 2134 opts['branch'] = [repo.lookupbranch(b) for b in opts['branch']]
2128 2135 # pats/include/exclude are passed to match.match() directly in
2129 2136 # _matchfiles() revset but walkchangerevs() builds its matcher with
2130 2137 # scmutil.match(). The difference is input pats are globbed on
2131 2138 # platforms without shell expansion (windows).
2132 2139 wctx = repo[None]
2133 2140 match, pats = scmutil.matchandpats(wctx, pats, opts)
2134 2141 slowpath = match.anypats() or ((match.isexact() or match.prefix()) and
2135 2142 opts.get('removed'))
2136 2143 if not slowpath:
2137 2144 for f in match.files():
2138 2145 if follow and f not in wctx:
2139 2146 # If the file exists, it may be a directory, so let it
2140 2147 # take the slow path.
2141 2148 if os.path.exists(repo.wjoin(f)):
2142 2149 slowpath = True
2143 2150 continue
2144 2151 else:
2145 2152 raise error.Abort(_('cannot follow file not in parent '
2146 2153 'revision: "%s"') % f)
2147 2154 filelog = repo.file(f)
2148 2155 if not filelog:
2149 2156 # A zero count may be a directory or deleted file, so
2150 2157 # try to find matching entries on the slow path.
2151 2158 if follow:
2152 2159 raise error.Abort(
2153 2160 _('cannot follow nonexistent file: "%s"') % f)
2154 2161 slowpath = True
2155 2162
2156 2163 # We decided to fall back to the slowpath because at least one
2157 2164 # of the paths was not a file. Check to see if at least one of them
2158 2165 # existed in history - in that case, we'll continue down the
2159 2166 # slowpath; otherwise, we can turn off the slowpath
2160 2167 if slowpath:
2161 2168 for path in match.files():
2162 2169 if path == '.' or path in repo.store:
2163 2170 break
2164 2171 else:
2165 2172 slowpath = False
2166 2173
2167 2174 fpats = ('_patsfollow', '_patsfollowfirst')
2168 2175 fnopats = (('_ancestors', '_fancestors'),
2169 2176 ('_descendants', '_fdescendants'))
2170 2177 if slowpath:
2171 2178 # See walkchangerevs() slow path.
2172 2179 #
2173 2180 # pats/include/exclude cannot be represented as separate
2174 2181 # revset expressions as their filtering logic applies at file
2175 2182 # level. For instance "-I a -X a" matches a revision touching
2176 2183 # "a" and "b" while "file(a) and not file(b)" does
2177 2184 # not. Besides, filesets are evaluated against the working
2178 2185 # directory.
2179 2186 matchargs = ['r:', 'd:relpath']
2180 2187 for p in pats:
2181 2188 matchargs.append('p:' + p)
2182 2189 for p in opts.get('include', []):
2183 2190 matchargs.append('i:' + p)
2184 2191 for p in opts.get('exclude', []):
2185 2192 matchargs.append('x:' + p)
2186 2193 matchargs = ','.join(('%r' % p) for p in matchargs)
2187 2194 opts['_matchfiles'] = matchargs
2188 2195 if follow:
2189 2196 opts[fnopats[0][followfirst]] = '.'
2190 2197 else:
2191 2198 if follow:
2192 2199 if pats:
2193 2200 # follow() revset interprets its file argument as a
2194 2201 # manifest entry, so use match.files(), not pats.
2195 2202 opts[fpats[followfirst]] = list(match.files())
2196 2203 else:
2197 2204 op = fnopats[followdescendants][followfirst]
2198 2205 opts[op] = 'rev(%d)' % startrev
2199 2206 else:
2200 2207 opts['_patslog'] = list(pats)
2201 2208
2202 2209 filematcher = None
2203 2210 if opts.get('patch') or opts.get('stat'):
2204 2211 # When following files, track renames via a special matcher.
2205 2212 # If we're forced to take the slowpath it means we're following
2206 2213 # at least one pattern/directory, so don't bother with rename tracking.
2207 2214 if follow and not match.always() and not slowpath:
2208 2215 # _makefollowlogfilematcher expects its files argument to be
2209 2216 # relative to the repo root, so use match.files(), not pats.
2210 2217 filematcher = _makefollowlogfilematcher(repo, match.files(),
2211 2218 followfirst)
2212 2219 else:
2213 2220 filematcher = _makenofollowlogfilematcher(repo, pats, opts)
2214 2221 if filematcher is None:
2215 2222 filematcher = lambda rev: match
2216 2223
2217 2224 expr = []
2218 2225 for op, val in sorted(opts.iteritems()):
2219 2226 if not val:
2220 2227 continue
2221 2228 if op not in opt2revset:
2222 2229 continue
2223 2230 revop, andor = opt2revset[op]
2224 2231 if '%(val)' not in revop:
2225 2232 expr.append(revop)
2226 2233 else:
2227 2234 if not isinstance(val, list):
2228 2235 e = revop % {'val': val}
2229 2236 else:
2230 2237 e = '(' + andor.join((revop % {'val': v}) for v in val) + ')'
2231 2238 expr.append(e)
2232 2239
2233 2240 if expr:
2234 2241 expr = '(' + ' and '.join(expr) + ')'
2235 2242 else:
2236 2243 expr = None
2237 2244 return expr, filematcher
2238 2245
2239 2246 def _logrevs(repo, opts):
2240 2247 # Default --rev value depends on --follow but --follow behavior
2241 2248 # depends on revisions resolved from --rev...
2242 2249 follow = opts.get('follow') or opts.get('follow_first')
2243 2250 if opts.get('rev'):
2244 2251 revs = scmutil.revrange(repo, opts['rev'])
2245 2252 elif follow and repo.dirstate.p1() == nullid:
2246 2253 revs = smartset.baseset()
2247 2254 elif follow:
2248 2255 revs = repo.revs('reverse(:.)')
2249 2256 else:
2250 2257 revs = smartset.spanset(repo)
2251 2258 revs.reverse()
2252 2259 return revs
2253 2260
2254 2261 def getgraphlogrevs(repo, pats, opts):
2255 2262 """Return (revs, expr, filematcher) where revs is an iterable of
2256 2263 revision numbers, expr is a revset string built from log options
2257 2264 and file patterns or None, and used to filter 'revs'. If --stat or
2258 2265 --patch are not passed filematcher is None. Otherwise it is a
2259 2266 callable taking a revision number and returning a match objects
2260 2267 filtering the files to be detailed when displaying the revision.
2261 2268 """
2262 2269 limit = loglimit(opts)
2263 2270 revs = _logrevs(repo, opts)
2264 2271 if not revs:
2265 2272 return smartset.baseset(), None, None
2266 2273 expr, filematcher = _makelogrevset(repo, pats, opts, revs)
2267 2274 if opts.get('rev'):
2268 2275 # User-specified revs might be unsorted, but don't sort before
2269 2276 # _makelogrevset because it might depend on the order of revs
2270 2277 if not (revs.isdescending() or revs.istopo()):
2271 2278 revs.sort(reverse=True)
2272 2279 if expr:
2273 2280 matcher = revset.match(repo.ui, expr, order=revset.followorder)
2274 2281 revs = matcher(repo, revs)
2275 2282 if limit is not None:
2276 2283 limitedrevs = []
2277 2284 for idx, rev in enumerate(revs):
2278 2285 if idx >= limit:
2279 2286 break
2280 2287 limitedrevs.append(rev)
2281 2288 revs = smartset.baseset(limitedrevs)
2282 2289
2283 2290 return revs, expr, filematcher
2284 2291
2285 2292 def getlogrevs(repo, pats, opts):
2286 2293 """Return (revs, expr, filematcher) where revs is an iterable of
2287 2294 revision numbers, expr is a revset string built from log options
2288 2295 and file patterns or None, and used to filter 'revs'. If --stat or
2289 2296 --patch are not passed filematcher is None. Otherwise it is a
2290 2297 callable taking a revision number and returning a match objects
2291 2298 filtering the files to be detailed when displaying the revision.
2292 2299 """
2293 2300 limit = loglimit(opts)
2294 2301 revs = _logrevs(repo, opts)
2295 2302 if not revs:
2296 2303 return smartset.baseset([]), None, None
2297 2304 expr, filematcher = _makelogrevset(repo, pats, opts, revs)
2298 2305 if expr:
2299 2306 matcher = revset.match(repo.ui, expr, order=revset.followorder)
2300 2307 revs = matcher(repo, revs)
2301 2308 if limit is not None:
2302 2309 limitedrevs = []
2303 2310 for idx, r in enumerate(revs):
2304 2311 if limit <= idx:
2305 2312 break
2306 2313 limitedrevs.append(r)
2307 2314 revs = smartset.baseset(limitedrevs)
2308 2315
2309 2316 return revs, expr, filematcher
2310 2317
2311 2318 def _graphnodeformatter(ui, displayer):
2312 2319 spec = ui.config('ui', 'graphnodetemplate')
2313 2320 if not spec:
2314 2321 return templatekw.showgraphnode # fast path for "{graphnode}"
2315 2322
2316 2323 spec = templater.unquotestring(spec)
2317 2324 templ = formatter.maketemplater(ui, spec)
2318 2325 cache = {}
2319 2326 if isinstance(displayer, changeset_templater):
2320 2327 cache = displayer.cache # reuse cache of slow templates
2321 2328 props = templatekw.keywords.copy()
2322 2329 props['templ'] = templ
2323 2330 props['cache'] = cache
2324 2331 def formatnode(repo, ctx):
2325 2332 props['ctx'] = ctx
2326 2333 props['repo'] = repo
2327 2334 props['ui'] = repo.ui
2328 2335 props['revcache'] = {}
2329 2336 return templ.render(props)
2330 2337 return formatnode
2331 2338
2332 2339 def displaygraph(ui, repo, dag, displayer, edgefn, getrenamed=None,
2333 2340 filematcher=None):
2334 2341 formatnode = _graphnodeformatter(ui, displayer)
2335 2342 state = graphmod.asciistate()
2336 2343 styles = state['styles']
2337 2344
2338 2345 # only set graph styling if HGPLAIN is not set.
2339 2346 if ui.plain('graph'):
2340 2347 # set all edge styles to |, the default pre-3.8 behaviour
2341 2348 styles.update(dict.fromkeys(styles, '|'))
2342 2349 else:
2343 2350 edgetypes = {
2344 2351 'parent': graphmod.PARENT,
2345 2352 'grandparent': graphmod.GRANDPARENT,
2346 2353 'missing': graphmod.MISSINGPARENT
2347 2354 }
2348 2355 for name, key in edgetypes.items():
2349 2356 # experimental config: experimental.graphstyle.*
2350 2357 styles[key] = ui.config('experimental', 'graphstyle.%s' % name,
2351 2358 styles[key])
2352 2359 if not styles[key]:
2353 2360 styles[key] = None
2354 2361
2355 2362 # experimental config: experimental.graphshorten
2356 2363 state['graphshorten'] = ui.configbool('experimental', 'graphshorten')
2357 2364
2358 2365 for rev, type, ctx, parents in dag:
2359 2366 char = formatnode(repo, ctx)
2360 2367 copies = None
2361 2368 if getrenamed and ctx.rev():
2362 2369 copies = []
2363 2370 for fn in ctx.files():
2364 2371 rename = getrenamed(fn, ctx.rev())
2365 2372 if rename:
2366 2373 copies.append((fn, rename[0]))
2367 2374 revmatchfn = None
2368 2375 if filematcher is not None:
2369 2376 revmatchfn = filematcher(ctx.rev())
2370 2377 displayer.show(ctx, copies=copies, matchfn=revmatchfn)
2371 2378 lines = displayer.hunk.pop(rev).split('\n')
2372 2379 if not lines[-1]:
2373 2380 del lines[-1]
2374 2381 displayer.flush(ctx)
2375 2382 edges = edgefn(type, char, lines, state, rev, parents)
2376 2383 for type, char, lines, coldata in edges:
2377 2384 graphmod.ascii(ui, state, type, char, lines, coldata)
2378 2385 displayer.close()
2379 2386
2380 2387 def graphlog(ui, repo, pats, opts):
2381 2388 # Parameters are identical to log command ones
2382 2389 revs, expr, filematcher = getgraphlogrevs(repo, pats, opts)
2383 2390 revdag = graphmod.dagwalker(repo, revs)
2384 2391
2385 2392 getrenamed = None
2386 2393 if opts.get('copies'):
2387 2394 endrev = None
2388 2395 if opts.get('rev'):
2389 2396 endrev = scmutil.revrange(repo, opts.get('rev')).max() + 1
2390 2397 getrenamed = templatekw.getrenamedfn(repo, endrev=endrev)
2391 2398
2392 2399 ui.pager('log')
2393 2400 displayer = show_changeset(ui, repo, opts, buffered=True)
2394 2401 displaygraph(ui, repo, revdag, displayer, graphmod.asciiedges, getrenamed,
2395 2402 filematcher)
2396 2403
2397 2404 def checkunsupportedgraphflags(pats, opts):
2398 2405 for op in ["newest_first"]:
2399 2406 if op in opts and opts[op]:
2400 2407 raise error.Abort(_("-G/--graph option is incompatible with --%s")
2401 2408 % op.replace("_", "-"))
2402 2409
2403 2410 def graphrevs(repo, nodes, opts):
2404 2411 limit = loglimit(opts)
2405 2412 nodes.reverse()
2406 2413 if limit is not None:
2407 2414 nodes = nodes[:limit]
2408 2415 return graphmod.nodes(repo, nodes)
2409 2416
2410 2417 def add(ui, repo, match, prefix, explicitonly, **opts):
2411 2418 join = lambda f: os.path.join(prefix, f)
2412 2419 bad = []
2413 2420
2414 2421 badfn = lambda x, y: bad.append(x) or match.bad(x, y)
2415 2422 names = []
2416 2423 wctx = repo[None]
2417 2424 cca = None
2418 2425 abort, warn = scmutil.checkportabilityalert(ui)
2419 2426 if abort or warn:
2420 2427 cca = scmutil.casecollisionauditor(ui, abort, repo.dirstate)
2421 2428
2422 2429 badmatch = matchmod.badmatch(match, badfn)
2423 2430 dirstate = repo.dirstate
2424 2431 # We don't want to just call wctx.walk here, since it would return a lot of
2425 2432 # clean files, which we aren't interested in and takes time.
2426 2433 for f in sorted(dirstate.walk(badmatch, sorted(wctx.substate),
2427 2434 True, False, full=False)):
2428 2435 exact = match.exact(f)
2429 2436 if exact or not explicitonly and f not in wctx and repo.wvfs.lexists(f):
2430 2437 if cca:
2431 2438 cca(f)
2432 2439 names.append(f)
2433 2440 if ui.verbose or not exact:
2434 2441 ui.status(_('adding %s\n') % match.rel(f))
2435 2442
2436 2443 for subpath in sorted(wctx.substate):
2437 2444 sub = wctx.sub(subpath)
2438 2445 try:
2439 2446 submatch = matchmod.subdirmatcher(subpath, match)
2440 2447 if opts.get(r'subrepos'):
2441 2448 bad.extend(sub.add(ui, submatch, prefix, False, **opts))
2442 2449 else:
2443 2450 bad.extend(sub.add(ui, submatch, prefix, True, **opts))
2444 2451 except error.LookupError:
2445 2452 ui.status(_("skipping missing subrepository: %s\n")
2446 2453 % join(subpath))
2447 2454
2448 2455 if not opts.get(r'dry_run'):
2449 2456 rejected = wctx.add(names, prefix)
2450 2457 bad.extend(f for f in rejected if f in match.files())
2451 2458 return bad
2452 2459
2453 2460 def addwebdirpath(repo, serverpath, webconf):
2454 2461 webconf[serverpath] = repo.root
2455 2462 repo.ui.debug('adding %s = %s\n' % (serverpath, repo.root))
2456 2463
2457 2464 for r in repo.revs('filelog("path:.hgsub")'):
2458 2465 ctx = repo[r]
2459 2466 for subpath in ctx.substate:
2460 2467 ctx.sub(subpath).addwebdirpath(serverpath, webconf)
2461 2468
2462 2469 def forget(ui, repo, match, prefix, explicitonly):
2463 2470 join = lambda f: os.path.join(prefix, f)
2464 2471 bad = []
2465 2472 badfn = lambda x, y: bad.append(x) or match.bad(x, y)
2466 2473 wctx = repo[None]
2467 2474 forgot = []
2468 2475
2469 2476 s = repo.status(match=matchmod.badmatch(match, badfn), clean=True)
2470 2477 forget = sorted(s.modified + s.added + s.deleted + s.clean)
2471 2478 if explicitonly:
2472 2479 forget = [f for f in forget if match.exact(f)]
2473 2480
2474 2481 for subpath in sorted(wctx.substate):
2475 2482 sub = wctx.sub(subpath)
2476 2483 try:
2477 2484 submatch = matchmod.subdirmatcher(subpath, match)
2478 2485 subbad, subforgot = sub.forget(submatch, prefix)
2479 2486 bad.extend([subpath + '/' + f for f in subbad])
2480 2487 forgot.extend([subpath + '/' + f for f in subforgot])
2481 2488 except error.LookupError:
2482 2489 ui.status(_("skipping missing subrepository: %s\n")
2483 2490 % join(subpath))
2484 2491
2485 2492 if not explicitonly:
2486 2493 for f in match.files():
2487 2494 if f not in repo.dirstate and not repo.wvfs.isdir(f):
2488 2495 if f not in forgot:
2489 2496 if repo.wvfs.exists(f):
2490 2497 # Don't complain if the exact case match wasn't given.
2491 2498 # But don't do this until after checking 'forgot', so
2492 2499 # that subrepo files aren't normalized, and this op is
2493 2500 # purely from data cached by the status walk above.
2494 2501 if repo.dirstate.normalize(f) in repo.dirstate:
2495 2502 continue
2496 2503 ui.warn(_('not removing %s: '
2497 2504 'file is already untracked\n')
2498 2505 % match.rel(f))
2499 2506 bad.append(f)
2500 2507
2501 2508 for f in forget:
2502 2509 if ui.verbose or not match.exact(f):
2503 2510 ui.status(_('removing %s\n') % match.rel(f))
2504 2511
2505 2512 rejected = wctx.forget(forget, prefix)
2506 2513 bad.extend(f for f in rejected if f in match.files())
2507 2514 forgot.extend(f for f in forget if f not in rejected)
2508 2515 return bad, forgot
2509 2516
2510 2517 def files(ui, ctx, m, fm, fmt, subrepos):
2511 2518 rev = ctx.rev()
2512 2519 ret = 1
2513 2520 ds = ctx.repo().dirstate
2514 2521
2515 2522 for f in ctx.matches(m):
2516 2523 if rev is None and ds[f] == 'r':
2517 2524 continue
2518 2525 fm.startitem()
2519 2526 if ui.verbose:
2520 2527 fc = ctx[f]
2521 2528 fm.write('size flags', '% 10d % 1s ', fc.size(), fc.flags())
2522 2529 fm.data(abspath=f)
2523 2530 fm.write('path', fmt, m.rel(f))
2524 2531 ret = 0
2525 2532
2526 2533 for subpath in sorted(ctx.substate):
2527 2534 submatch = matchmod.subdirmatcher(subpath, m)
2528 2535 if (subrepos or m.exact(subpath) or any(submatch.files())):
2529 2536 sub = ctx.sub(subpath)
2530 2537 try:
2531 2538 recurse = m.exact(subpath) or subrepos
2532 2539 if sub.printfiles(ui, submatch, fm, fmt, recurse) == 0:
2533 2540 ret = 0
2534 2541 except error.LookupError:
2535 2542 ui.status(_("skipping missing subrepository: %s\n")
2536 2543 % m.abs(subpath))
2537 2544
2538 2545 return ret
2539 2546
2540 2547 def remove(ui, repo, m, prefix, after, force, subrepos, warnings=None):
2541 2548 join = lambda f: os.path.join(prefix, f)
2542 2549 ret = 0
2543 2550 s = repo.status(match=m, clean=True)
2544 2551 modified, added, deleted, clean = s[0], s[1], s[3], s[6]
2545 2552
2546 2553 wctx = repo[None]
2547 2554
2548 2555 if warnings is None:
2549 2556 warnings = []
2550 2557 warn = True
2551 2558 else:
2552 2559 warn = False
2553 2560
2554 2561 subs = sorted(wctx.substate)
2555 2562 total = len(subs)
2556 2563 count = 0
2557 2564 for subpath in subs:
2558 2565 count += 1
2559 2566 submatch = matchmod.subdirmatcher(subpath, m)
2560 2567 if subrepos or m.exact(subpath) or any(submatch.files()):
2561 2568 ui.progress(_('searching'), count, total=total, unit=_('subrepos'))
2562 2569 sub = wctx.sub(subpath)
2563 2570 try:
2564 2571 if sub.removefiles(submatch, prefix, after, force, subrepos,
2565 2572 warnings):
2566 2573 ret = 1
2567 2574 except error.LookupError:
2568 2575 warnings.append(_("skipping missing subrepository: %s\n")
2569 2576 % join(subpath))
2570 2577 ui.progress(_('searching'), None)
2571 2578
2572 2579 # warn about failure to delete explicit files/dirs
2573 2580 deleteddirs = util.dirs(deleted)
2574 2581 files = m.files()
2575 2582 total = len(files)
2576 2583 count = 0
2577 2584 for f in files:
2578 2585 def insubrepo():
2579 2586 for subpath in wctx.substate:
2580 2587 if f.startswith(subpath + '/'):
2581 2588 return True
2582 2589 return False
2583 2590
2584 2591 count += 1
2585 2592 ui.progress(_('deleting'), count, total=total, unit=_('files'))
2586 2593 isdir = f in deleteddirs or wctx.hasdir(f)
2587 2594 if (f in repo.dirstate or isdir or f == '.'
2588 2595 or insubrepo() or f in subs):
2589 2596 continue
2590 2597
2591 2598 if repo.wvfs.exists(f):
2592 2599 if repo.wvfs.isdir(f):
2593 2600 warnings.append(_('not removing %s: no tracked files\n')
2594 2601 % m.rel(f))
2595 2602 else:
2596 2603 warnings.append(_('not removing %s: file is untracked\n')
2597 2604 % m.rel(f))
2598 2605 # missing files will generate a warning elsewhere
2599 2606 ret = 1
2600 2607 ui.progress(_('deleting'), None)
2601 2608
2602 2609 if force:
2603 2610 list = modified + deleted + clean + added
2604 2611 elif after:
2605 2612 list = deleted
2606 2613 remaining = modified + added + clean
2607 2614 total = len(remaining)
2608 2615 count = 0
2609 2616 for f in remaining:
2610 2617 count += 1
2611 2618 ui.progress(_('skipping'), count, total=total, unit=_('files'))
2612 2619 warnings.append(_('not removing %s: file still exists\n')
2613 2620 % m.rel(f))
2614 2621 ret = 1
2615 2622 ui.progress(_('skipping'), None)
2616 2623 else:
2617 2624 list = deleted + clean
2618 2625 total = len(modified) + len(added)
2619 2626 count = 0
2620 2627 for f in modified:
2621 2628 count += 1
2622 2629 ui.progress(_('skipping'), count, total=total, unit=_('files'))
2623 2630 warnings.append(_('not removing %s: file is modified (use -f'
2624 2631 ' to force removal)\n') % m.rel(f))
2625 2632 ret = 1
2626 2633 for f in added:
2627 2634 count += 1
2628 2635 ui.progress(_('skipping'), count, total=total, unit=_('files'))
2629 2636 warnings.append(_("not removing %s: file has been marked for add"
2630 2637 " (use 'hg forget' to undo add)\n") % m.rel(f))
2631 2638 ret = 1
2632 2639 ui.progress(_('skipping'), None)
2633 2640
2634 2641 list = sorted(list)
2635 2642 total = len(list)
2636 2643 count = 0
2637 2644 for f in list:
2638 2645 count += 1
2639 2646 if ui.verbose or not m.exact(f):
2640 2647 ui.progress(_('deleting'), count, total=total, unit=_('files'))
2641 2648 ui.status(_('removing %s\n') % m.rel(f))
2642 2649 ui.progress(_('deleting'), None)
2643 2650
2644 2651 with repo.wlock():
2645 2652 if not after:
2646 2653 for f in list:
2647 2654 if f in added:
2648 2655 continue # we never unlink added files on remove
2649 2656 repo.wvfs.unlinkpath(f, ignoremissing=True)
2650 2657 repo[None].forget(list)
2651 2658
2652 2659 if warn:
2653 2660 for warning in warnings:
2654 2661 ui.warn(warning)
2655 2662
2656 2663 return ret
2657 2664
2658 2665 def cat(ui, repo, ctx, matcher, basefm, fntemplate, prefix, **opts):
2659 2666 err = 1
2660 2667
2661 2668 def write(path):
2662 2669 filename = None
2663 2670 if fntemplate:
2664 2671 filename = makefilename(repo, fntemplate, ctx.node(),
2665 2672 pathname=os.path.join(prefix, path))
2666 2673 with formatter.maybereopen(basefm, filename, opts) as fm:
2667 2674 data = ctx[path].data()
2668 2675 if opts.get('decode'):
2669 2676 data = repo.wwritedata(path, data)
2670 2677 fm.startitem()
2671 2678 fm.write('data', '%s', data)
2672 2679 fm.data(abspath=path, path=matcher.rel(path))
2673 2680
2674 2681 # Automation often uses hg cat on single files, so special case it
2675 2682 # for performance to avoid the cost of parsing the manifest.
2676 2683 if len(matcher.files()) == 1 and not matcher.anypats():
2677 2684 file = matcher.files()[0]
2678 2685 mfl = repo.manifestlog
2679 2686 mfnode = ctx.manifestnode()
2680 2687 try:
2681 2688 if mfnode and mfl[mfnode].find(file)[0]:
2682 2689 write(file)
2683 2690 return 0
2684 2691 except KeyError:
2685 2692 pass
2686 2693
2687 2694 for abs in ctx.walk(matcher):
2688 2695 write(abs)
2689 2696 err = 0
2690 2697
2691 2698 for subpath in sorted(ctx.substate):
2692 2699 sub = ctx.sub(subpath)
2693 2700 try:
2694 2701 submatch = matchmod.subdirmatcher(subpath, matcher)
2695 2702
2696 2703 if not sub.cat(submatch, basefm, fntemplate,
2697 2704 os.path.join(prefix, sub._path), **opts):
2698 2705 err = 0
2699 2706 except error.RepoLookupError:
2700 2707 ui.status(_("skipping missing subrepository: %s\n")
2701 2708 % os.path.join(prefix, subpath))
2702 2709
2703 2710 return err
2704 2711
2705 2712 def commit(ui, repo, commitfunc, pats, opts):
2706 2713 '''commit the specified files or all outstanding changes'''
2707 2714 date = opts.get('date')
2708 2715 if date:
2709 2716 opts['date'] = util.parsedate(date)
2710 2717 message = logmessage(ui, opts)
2711 2718 matcher = scmutil.match(repo[None], pats, opts)
2712 2719
2713 2720 # extract addremove carefully -- this function can be called from a command
2714 2721 # that doesn't support addremove
2715 2722 if opts.get('addremove'):
2716 2723 if scmutil.addremove(repo, matcher, "", opts) != 0:
2717 2724 raise error.Abort(
2718 2725 _("failed to mark all new/missing files as added/removed"))
2719 2726
2720 2727 return commitfunc(ui, repo, message, matcher, opts)
2721 2728
2722 2729 def samefile(f, ctx1, ctx2):
2723 2730 if f in ctx1.manifest():
2724 2731 a = ctx1.filectx(f)
2725 2732 if f in ctx2.manifest():
2726 2733 b = ctx2.filectx(f)
2727 2734 return (not a.cmp(b)
2728 2735 and a.flags() == b.flags())
2729 2736 else:
2730 2737 return False
2731 2738 else:
2732 2739 return f not in ctx2.manifest()
2733 2740
2734 2741 def amend(ui, repo, commitfunc, old, extra, pats, opts):
2735 2742 # avoid cycle context -> subrepo -> cmdutil
2736 2743 from . import context
2737 2744
2738 2745 # amend will reuse the existing user if not specified, but the obsolete
2739 2746 # marker creation requires that the current user's name is specified.
2740 2747 if obsolete.isenabled(repo, obsolete.createmarkersopt):
2741 2748 ui.username() # raise exception if username not set
2742 2749
2743 2750 ui.note(_('amending changeset %s\n') % old)
2744 2751 base = old.p1()
2745 2752 createmarkers = obsolete.isenabled(repo, obsolete.createmarkersopt)
2746 2753
2747 2754 newid = None
2748 2755 with repo.wlock(), repo.lock():
2749 2756 with repo.transaction('amend') as tr:
2750 2757 # See if we got a message from -m or -l, if not, open the editor
2751 2758 # with the message of the changeset to amend
2752 2759 message = logmessage(ui, opts)
2753 2760 # ensure logfile does not conflict with later enforcement of the
2754 2761 # message. potential logfile content has been processed by
2755 2762 # `logmessage` anyway.
2756 2763 opts.pop('logfile')
2757 2764 # First, do a regular commit to record all changes in the working
2758 2765 # directory (if there are any)
2759 2766 ui.callhooks = False
2760 2767 activebookmark = repo._bookmarks.active
2761 2768 try:
2762 2769 repo._bookmarks.active = None
2763 2770 opts['message'] = 'temporary amend commit for %s' % old
2764 2771 node = commit(ui, repo, commitfunc, pats, opts)
2765 2772 finally:
2766 2773 repo._bookmarks.active = activebookmark
2767 2774 repo._bookmarks.recordchange(tr)
2768 2775 ui.callhooks = True
2769 2776 ctx = repo[node]
2770 2777
2771 2778 # Participating changesets:
2772 2779 #
2773 2780 # node/ctx o - new (intermediate) commit that contains changes
2774 2781 # | from working dir to go into amending commit
2775 2782 # | (or a workingctx if there were no changes)
2776 2783 # |
2777 2784 # old o - changeset to amend
2778 2785 # |
2779 2786 # base o - parent of amending changeset
2780 2787
2781 2788 # Update extra dict from amended commit (e.g. to preserve graft
2782 2789 # source)
2783 2790 extra.update(old.extra())
2784 2791
2785 2792 # Also update it from the intermediate commit or from the wctx
2786 2793 extra.update(ctx.extra())
2787 2794
2788 2795 if len(old.parents()) > 1:
2789 2796 # ctx.files() isn't reliable for merges, so fall back to the
2790 2797 # slower repo.status() method
2791 2798 files = set([fn for st in repo.status(base, old)[:3]
2792 2799 for fn in st])
2793 2800 else:
2794 2801 files = set(old.files())
2795 2802
2796 2803 # Second, we use either the commit we just did, or if there were no
2797 2804 # changes the parent of the working directory as the version of the
2798 2805 # files in the final amend commit
2799 2806 if node:
2800 2807 ui.note(_('copying changeset %s to %s\n') % (ctx, base))
2801 2808
2802 2809 user = ctx.user()
2803 2810 date = ctx.date()
2804 2811 # Recompute copies (avoid recording a -> b -> a)
2805 2812 copied = copies.pathcopies(base, ctx)
2806 2813 if old.p2:
2807 2814 copied.update(copies.pathcopies(old.p2(), ctx))
2808 2815
2809 2816 # Prune files which were reverted by the updates: if old
2810 2817 # introduced file X and our intermediate commit, node,
2811 2818 # renamed that file, then those two files are the same and
2812 2819 # we can discard X from our list of files. Likewise if X
2813 2820 # was deleted, it's no longer relevant
2814 2821 files.update(ctx.files())
2815 2822 files = [f for f in files if not samefile(f, ctx, base)]
2816 2823
2817 2824 def filectxfn(repo, ctx_, path):
2818 2825 try:
2819 2826 fctx = ctx[path]
2820 2827 flags = fctx.flags()
2821 2828 mctx = context.memfilectx(repo,
2822 2829 fctx.path(), fctx.data(),
2823 2830 islink='l' in flags,
2824 2831 isexec='x' in flags,
2825 2832 copied=copied.get(path))
2826 2833 return mctx
2827 2834 except KeyError:
2828 2835 return None
2829 2836 else:
2830 2837 ui.note(_('copying changeset %s to %s\n') % (old, base))
2831 2838
2832 2839 # Use version of files as in the old cset
2833 2840 def filectxfn(repo, ctx_, path):
2834 2841 try:
2835 2842 return old.filectx(path)
2836 2843 except KeyError:
2837 2844 return None
2838 2845
2839 2846 user = opts.get('user') or old.user()
2840 2847 date = opts.get('date') or old.date()
2841 2848 editform = mergeeditform(old, 'commit.amend')
2842 2849 editor = getcommiteditor(editform=editform,
2843 2850 **pycompat.strkwargs(opts))
2844 2851 if not message:
2845 2852 editor = getcommiteditor(edit=True, editform=editform)
2846 2853 message = old.description()
2847 2854
2848 2855 pureextra = extra.copy()
2849 2856 extra['amend_source'] = old.hex()
2850 2857
2851 2858 new = context.memctx(repo,
2852 2859 parents=[base.node(), old.p2().node()],
2853 2860 text=message,
2854 2861 files=files,
2855 2862 filectxfn=filectxfn,
2856 2863 user=user,
2857 2864 date=date,
2858 2865 extra=extra,
2859 2866 editor=editor)
2860 2867
2861 2868 newdesc = changelog.stripdesc(new.description())
2862 2869 if ((not node)
2863 2870 and newdesc == old.description()
2864 2871 and user == old.user()
2865 2872 and date == old.date()
2866 2873 and pureextra == old.extra()):
2867 2874 # nothing changed. continuing here would create a new node
2868 2875 # anyway because of the amend_source noise.
2869 2876 #
2870 2877 # This not what we expect from amend.
2871 2878 return old.node()
2872 2879
2873 2880 ph = repo.ui.config('phases', 'new-commit', phases.draft)
2874 2881 try:
2875 2882 if opts.get('secret'):
2876 2883 commitphase = 'secret'
2877 2884 else:
2878 2885 commitphase = old.phase()
2879 2886 repo.ui.setconfig('phases', 'new-commit', commitphase, 'amend')
2880 2887 newid = repo.commitctx(new)
2881 2888 finally:
2882 2889 repo.ui.setconfig('phases', 'new-commit', ph, 'amend')
2883 2890 if newid != old.node():
2884 2891 # Reroute the working copy parent to the new changeset
2885 2892 repo.setparents(newid, nullid)
2886 2893
2887 2894 # Move bookmarks from old parent to amend commit
2888 2895 bms = repo.nodebookmarks(old.node())
2889 2896 if bms:
2890 2897 marks = repo._bookmarks
2891 2898 for bm in bms:
2892 2899 ui.debug('moving bookmarks %r from %s to %s\n' %
2893 2900 (marks, old.hex(), hex(newid)))
2894 2901 marks[bm] = newid
2895 2902 marks.recordchange(tr)
2896 2903 #commit the whole amend process
2897 2904 if createmarkers:
2898 2905 # mark the new changeset as successor of the rewritten one
2899 2906 new = repo[newid]
2900 2907 obs = [(old, (new,))]
2901 2908 if node:
2902 2909 obs.append((ctx, ()))
2903 2910
2904 2911 obsolete.createmarkers(repo, obs, operation='amend')
2905 2912 if not createmarkers and newid != old.node():
2906 2913 # Strip the intermediate commit (if there was one) and the amended
2907 2914 # commit
2908 2915 if node:
2909 2916 ui.note(_('stripping intermediate changeset %s\n') % ctx)
2910 2917 ui.note(_('stripping amended changeset %s\n') % old)
2911 2918 repair.strip(ui, repo, old.node(), topic='amend-backup')
2912 2919 return newid
2913 2920
2914 2921 def commiteditor(repo, ctx, subs, editform=''):
2915 2922 if ctx.description():
2916 2923 return ctx.description()
2917 2924 return commitforceeditor(repo, ctx, subs, editform=editform,
2918 2925 unchangedmessagedetection=True)
2919 2926
2920 2927 def commitforceeditor(repo, ctx, subs, finishdesc=None, extramsg=None,
2921 2928 editform='', unchangedmessagedetection=False):
2922 2929 if not extramsg:
2923 2930 extramsg = _("Leave message empty to abort commit.")
2924 2931
2925 2932 forms = [e for e in editform.split('.') if e]
2926 2933 forms.insert(0, 'changeset')
2927 2934 templatetext = None
2928 2935 while forms:
2929 2936 ref = '.'.join(forms)
2930 2937 if repo.ui.config('committemplate', ref):
2931 2938 templatetext = committext = buildcommittemplate(
2932 2939 repo, ctx, subs, extramsg, ref)
2933 2940 break
2934 2941 forms.pop()
2935 2942 else:
2936 2943 committext = buildcommittext(repo, ctx, subs, extramsg)
2937 2944
2938 2945 # run editor in the repository root
2939 2946 olddir = pycompat.getcwd()
2940 2947 os.chdir(repo.root)
2941 2948
2942 2949 # make in-memory changes visible to external process
2943 2950 tr = repo.currenttransaction()
2944 2951 repo.dirstate.write(tr)
2945 2952 pending = tr and tr.writepending() and repo.root
2946 2953
2947 2954 editortext = repo.ui.edit(committext, ctx.user(), ctx.extra(),
2948 2955 editform=editform, pending=pending,
2949 2956 repopath=repo.path)
2950 2957 text = editortext
2951 2958
2952 2959 # strip away anything below this special string (used for editors that want
2953 2960 # to display the diff)
2954 2961 stripbelow = re.search(_linebelow, text, flags=re.MULTILINE)
2955 2962 if stripbelow:
2956 2963 text = text[:stripbelow.start()]
2957 2964
2958 2965 text = re.sub("(?m)^HG:.*(\n|$)", "", text)
2959 2966 os.chdir(olddir)
2960 2967
2961 2968 if finishdesc:
2962 2969 text = finishdesc(text)
2963 2970 if not text.strip():
2964 2971 raise error.Abort(_("empty commit message"))
2965 2972 if unchangedmessagedetection and editortext == templatetext:
2966 2973 raise error.Abort(_("commit message unchanged"))
2967 2974
2968 2975 return text
2969 2976
2970 2977 def buildcommittemplate(repo, ctx, subs, extramsg, ref):
2971 2978 ui = repo.ui
2972 2979 spec = formatter.templatespec(ref, None, None)
2973 2980 t = changeset_templater(ui, repo, spec, None, {}, False)
2974 2981 t.t.cache.update((k, templater.unquotestring(v))
2975 2982 for k, v in repo.ui.configitems('committemplate'))
2976 2983
2977 2984 if not extramsg:
2978 2985 extramsg = '' # ensure that extramsg is string
2979 2986
2980 2987 ui.pushbuffer()
2981 2988 t.show(ctx, extramsg=extramsg)
2982 2989 return ui.popbuffer()
2983 2990
2984 2991 def hgprefix(msg):
2985 2992 return "\n".join(["HG: %s" % a for a in msg.split("\n") if a])
2986 2993
2987 2994 def buildcommittext(repo, ctx, subs, extramsg):
2988 2995 edittext = []
2989 2996 modified, added, removed = ctx.modified(), ctx.added(), ctx.removed()
2990 2997 if ctx.description():
2991 2998 edittext.append(ctx.description())
2992 2999 edittext.append("")
2993 3000 edittext.append("") # Empty line between message and comments.
2994 3001 edittext.append(hgprefix(_("Enter commit message."
2995 3002 " Lines beginning with 'HG:' are removed.")))
2996 3003 edittext.append(hgprefix(extramsg))
2997 3004 edittext.append("HG: --")
2998 3005 edittext.append(hgprefix(_("user: %s") % ctx.user()))
2999 3006 if ctx.p2():
3000 3007 edittext.append(hgprefix(_("branch merge")))
3001 3008 if ctx.branch():
3002 3009 edittext.append(hgprefix(_("branch '%s'") % ctx.branch()))
3003 3010 if bookmarks.isactivewdirparent(repo):
3004 3011 edittext.append(hgprefix(_("bookmark '%s'") % repo._activebookmark))
3005 3012 edittext.extend([hgprefix(_("subrepo %s") % s) for s in subs])
3006 3013 edittext.extend([hgprefix(_("added %s") % f) for f in added])
3007 3014 edittext.extend([hgprefix(_("changed %s") % f) for f in modified])
3008 3015 edittext.extend([hgprefix(_("removed %s") % f) for f in removed])
3009 3016 if not added and not modified and not removed:
3010 3017 edittext.append(hgprefix(_("no files changed")))
3011 3018 edittext.append("")
3012 3019
3013 3020 return "\n".join(edittext)
3014 3021
3015 3022 def commitstatus(repo, node, branch, bheads=None, opts=None):
3016 3023 if opts is None:
3017 3024 opts = {}
3018 3025 ctx = repo[node]
3019 3026 parents = ctx.parents()
3020 3027
3021 3028 if (not opts.get('amend') and bheads and node not in bheads and not
3022 3029 [x for x in parents if x.node() in bheads and x.branch() == branch]):
3023 3030 repo.ui.status(_('created new head\n'))
3024 3031 # The message is not printed for initial roots. For the other
3025 3032 # changesets, it is printed in the following situations:
3026 3033 #
3027 3034 # Par column: for the 2 parents with ...
3028 3035 # N: null or no parent
3029 3036 # B: parent is on another named branch
3030 3037 # C: parent is a regular non head changeset
3031 3038 # H: parent was a branch head of the current branch
3032 3039 # Msg column: whether we print "created new head" message
3033 3040 # In the following, it is assumed that there already exists some
3034 3041 # initial branch heads of the current branch, otherwise nothing is
3035 3042 # printed anyway.
3036 3043 #
3037 3044 # Par Msg Comment
3038 3045 # N N y additional topo root
3039 3046 #
3040 3047 # B N y additional branch root
3041 3048 # C N y additional topo head
3042 3049 # H N n usual case
3043 3050 #
3044 3051 # B B y weird additional branch root
3045 3052 # C B y branch merge
3046 3053 # H B n merge with named branch
3047 3054 #
3048 3055 # C C y additional head from merge
3049 3056 # C H n merge with a head
3050 3057 #
3051 3058 # H H n head merge: head count decreases
3052 3059
3053 3060 if not opts.get('close_branch'):
3054 3061 for r in parents:
3055 3062 if r.closesbranch() and r.branch() == branch:
3056 3063 repo.ui.status(_('reopening closed branch head %d\n') % r)
3057 3064
3058 3065 if repo.ui.debugflag:
3059 3066 repo.ui.write(_('committed changeset %d:%s\n') % (int(ctx), ctx.hex()))
3060 3067 elif repo.ui.verbose:
3061 3068 repo.ui.write(_('committed changeset %d:%s\n') % (int(ctx), ctx))
3062 3069
3063 3070 def postcommitstatus(repo, pats, opts):
3064 3071 return repo.status(match=scmutil.match(repo[None], pats, opts))
3065 3072
3066 3073 def revert(ui, repo, ctx, parents, *pats, **opts):
3067 3074 parent, p2 = parents
3068 3075 node = ctx.node()
3069 3076
3070 3077 mf = ctx.manifest()
3071 3078 if node == p2:
3072 3079 parent = p2
3073 3080
3074 3081 # need all matching names in dirstate and manifest of target rev,
3075 3082 # so have to walk both. do not print errors if files exist in one
3076 3083 # but not other. in both cases, filesets should be evaluated against
3077 3084 # workingctx to get consistent result (issue4497). this means 'set:**'
3078 3085 # cannot be used to select missing files from target rev.
3079 3086
3080 3087 # `names` is a mapping for all elements in working copy and target revision
3081 3088 # The mapping is in the form:
3082 3089 # <asb path in repo> -> (<path from CWD>, <exactly specified by matcher?>)
3083 3090 names = {}
3084 3091
3085 3092 with repo.wlock():
3086 3093 ## filling of the `names` mapping
3087 3094 # walk dirstate to fill `names`
3088 3095
3089 3096 interactive = opts.get('interactive', False)
3090 3097 wctx = repo[None]
3091 3098 m = scmutil.match(wctx, pats, opts)
3092 3099
3093 3100 # we'll need this later
3094 3101 targetsubs = sorted(s for s in wctx.substate if m(s))
3095 3102
3096 3103 if not m.always():
3097 3104 matcher = matchmod.badmatch(m, lambda x, y: False)
3098 3105 for abs in wctx.walk(matcher):
3099 3106 names[abs] = m.rel(abs), m.exact(abs)
3100 3107
3101 3108 # walk target manifest to fill `names`
3102 3109
3103 3110 def badfn(path, msg):
3104 3111 if path in names:
3105 3112 return
3106 3113 if path in ctx.substate:
3107 3114 return
3108 3115 path_ = path + '/'
3109 3116 for f in names:
3110 3117 if f.startswith(path_):
3111 3118 return
3112 3119 ui.warn("%s: %s\n" % (m.rel(path), msg))
3113 3120
3114 3121 for abs in ctx.walk(matchmod.badmatch(m, badfn)):
3115 3122 if abs not in names:
3116 3123 names[abs] = m.rel(abs), m.exact(abs)
3117 3124
3118 3125 # Find status of all file in `names`.
3119 3126 m = scmutil.matchfiles(repo, names)
3120 3127
3121 3128 changes = repo.status(node1=node, match=m,
3122 3129 unknown=True, ignored=True, clean=True)
3123 3130 else:
3124 3131 changes = repo.status(node1=node, match=m)
3125 3132 for kind in changes:
3126 3133 for abs in kind:
3127 3134 names[abs] = m.rel(abs), m.exact(abs)
3128 3135
3129 3136 m = scmutil.matchfiles(repo, names)
3130 3137
3131 3138 modified = set(changes.modified)
3132 3139 added = set(changes.added)
3133 3140 removed = set(changes.removed)
3134 3141 _deleted = set(changes.deleted)
3135 3142 unknown = set(changes.unknown)
3136 3143 unknown.update(changes.ignored)
3137 3144 clean = set(changes.clean)
3138 3145 modadded = set()
3139 3146
3140 3147 # We need to account for the state of the file in the dirstate,
3141 3148 # even when we revert against something else than parent. This will
3142 3149 # slightly alter the behavior of revert (doing back up or not, delete
3143 3150 # or just forget etc).
3144 3151 if parent == node:
3145 3152 dsmodified = modified
3146 3153 dsadded = added
3147 3154 dsremoved = removed
3148 3155 # store all local modifications, useful later for rename detection
3149 3156 localchanges = dsmodified | dsadded
3150 3157 modified, added, removed = set(), set(), set()
3151 3158 else:
3152 3159 changes = repo.status(node1=parent, match=m)
3153 3160 dsmodified = set(changes.modified)
3154 3161 dsadded = set(changes.added)
3155 3162 dsremoved = set(changes.removed)
3156 3163 # store all local modifications, useful later for rename detection
3157 3164 localchanges = dsmodified | dsadded
3158 3165
3159 3166 # only take into account for removes between wc and target
3160 3167 clean |= dsremoved - removed
3161 3168 dsremoved &= removed
3162 3169 # distinct between dirstate remove and other
3163 3170 removed -= dsremoved
3164 3171
3165 3172 modadded = added & dsmodified
3166 3173 added -= modadded
3167 3174
3168 3175 # tell newly modified apart.
3169 3176 dsmodified &= modified
3170 3177 dsmodified |= modified & dsadded # dirstate added may need backup
3171 3178 modified -= dsmodified
3172 3179
3173 3180 # We need to wait for some post-processing to update this set
3174 3181 # before making the distinction. The dirstate will be used for
3175 3182 # that purpose.
3176 3183 dsadded = added
3177 3184
3178 3185 # in case of merge, files that are actually added can be reported as
3179 3186 # modified, we need to post process the result
3180 3187 if p2 != nullid:
3181 3188 mergeadd = set(dsmodified)
3182 3189 for path in dsmodified:
3183 3190 if path in mf:
3184 3191 mergeadd.remove(path)
3185 3192 dsadded |= mergeadd
3186 3193 dsmodified -= mergeadd
3187 3194
3188 3195 # if f is a rename, update `names` to also revert the source
3189 3196 cwd = repo.getcwd()
3190 3197 for f in localchanges:
3191 3198 src = repo.dirstate.copied(f)
3192 3199 # XXX should we check for rename down to target node?
3193 3200 if src and src not in names and repo.dirstate[src] == 'r':
3194 3201 dsremoved.add(src)
3195 3202 names[src] = (repo.pathto(src, cwd), True)
3196 3203
3197 3204 # determine the exact nature of the deleted changesets
3198 3205 deladded = set(_deleted)
3199 3206 for path in _deleted:
3200 3207 if path in mf:
3201 3208 deladded.remove(path)
3202 3209 deleted = _deleted - deladded
3203 3210
3204 3211 # distinguish between file to forget and the other
3205 3212 added = set()
3206 3213 for abs in dsadded:
3207 3214 if repo.dirstate[abs] != 'a':
3208 3215 added.add(abs)
3209 3216 dsadded -= added
3210 3217
3211 3218 for abs in deladded:
3212 3219 if repo.dirstate[abs] == 'a':
3213 3220 dsadded.add(abs)
3214 3221 deladded -= dsadded
3215 3222
3216 3223 # For files marked as removed, we check if an unknown file is present at
3217 3224 # the same path. If a such file exists it may need to be backed up.
3218 3225 # Making the distinction at this stage helps have simpler backup
3219 3226 # logic.
3220 3227 removunk = set()
3221 3228 for abs in removed:
3222 3229 target = repo.wjoin(abs)
3223 3230 if os.path.lexists(target):
3224 3231 removunk.add(abs)
3225 3232 removed -= removunk
3226 3233
3227 3234 dsremovunk = set()
3228 3235 for abs in dsremoved:
3229 3236 target = repo.wjoin(abs)
3230 3237 if os.path.lexists(target):
3231 3238 dsremovunk.add(abs)
3232 3239 dsremoved -= dsremovunk
3233 3240
3234 3241 # action to be actually performed by revert
3235 3242 # (<list of file>, message>) tuple
3236 3243 actions = {'revert': ([], _('reverting %s\n')),
3237 3244 'add': ([], _('adding %s\n')),
3238 3245 'remove': ([], _('removing %s\n')),
3239 3246 'drop': ([], _('removing %s\n')),
3240 3247 'forget': ([], _('forgetting %s\n')),
3241 3248 'undelete': ([], _('undeleting %s\n')),
3242 3249 'noop': (None, _('no changes needed to %s\n')),
3243 3250 'unknown': (None, _('file not managed: %s\n')),
3244 3251 }
3245 3252
3246 3253 # "constant" that convey the backup strategy.
3247 3254 # All set to `discard` if `no-backup` is set do avoid checking
3248 3255 # no_backup lower in the code.
3249 3256 # These values are ordered for comparison purposes
3250 3257 backupinteractive = 3 # do backup if interactively modified
3251 3258 backup = 2 # unconditionally do backup
3252 3259 check = 1 # check if the existing file differs from target
3253 3260 discard = 0 # never do backup
3254 3261 if opts.get('no_backup'):
3255 3262 backupinteractive = backup = check = discard
3256 3263 if interactive:
3257 3264 dsmodifiedbackup = backupinteractive
3258 3265 else:
3259 3266 dsmodifiedbackup = backup
3260 3267 tobackup = set()
3261 3268
3262 3269 backupanddel = actions['remove']
3263 3270 if not opts.get('no_backup'):
3264 3271 backupanddel = actions['drop']
3265 3272
3266 3273 disptable = (
3267 3274 # dispatch table:
3268 3275 # file state
3269 3276 # action
3270 3277 # make backup
3271 3278
3272 3279 ## Sets that results that will change file on disk
3273 3280 # Modified compared to target, no local change
3274 3281 (modified, actions['revert'], discard),
3275 3282 # Modified compared to target, but local file is deleted
3276 3283 (deleted, actions['revert'], discard),
3277 3284 # Modified compared to target, local change
3278 3285 (dsmodified, actions['revert'], dsmodifiedbackup),
3279 3286 # Added since target
3280 3287 (added, actions['remove'], discard),
3281 3288 # Added in working directory
3282 3289 (dsadded, actions['forget'], discard),
3283 3290 # Added since target, have local modification
3284 3291 (modadded, backupanddel, backup),
3285 3292 # Added since target but file is missing in working directory
3286 3293 (deladded, actions['drop'], discard),
3287 3294 # Removed since target, before working copy parent
3288 3295 (removed, actions['add'], discard),
3289 3296 # Same as `removed` but an unknown file exists at the same path
3290 3297 (removunk, actions['add'], check),
3291 3298 # Removed since targe, marked as such in working copy parent
3292 3299 (dsremoved, actions['undelete'], discard),
3293 3300 # Same as `dsremoved` but an unknown file exists at the same path
3294 3301 (dsremovunk, actions['undelete'], check),
3295 3302 ## the following sets does not result in any file changes
3296 3303 # File with no modification
3297 3304 (clean, actions['noop'], discard),
3298 3305 # Existing file, not tracked anywhere
3299 3306 (unknown, actions['unknown'], discard),
3300 3307 )
3301 3308
3302 3309 for abs, (rel, exact) in sorted(names.items()):
3303 3310 # target file to be touch on disk (relative to cwd)
3304 3311 target = repo.wjoin(abs)
3305 3312 # search the entry in the dispatch table.
3306 3313 # if the file is in any of these sets, it was touched in the working
3307 3314 # directory parent and we are sure it needs to be reverted.
3308 3315 for table, (xlist, msg), dobackup in disptable:
3309 3316 if abs not in table:
3310 3317 continue
3311 3318 if xlist is not None:
3312 3319 xlist.append(abs)
3313 3320 if dobackup:
3314 3321 # If in interactive mode, don't automatically create
3315 3322 # .orig files (issue4793)
3316 3323 if dobackup == backupinteractive:
3317 3324 tobackup.add(abs)
3318 3325 elif (backup <= dobackup or wctx[abs].cmp(ctx[abs])):
3319 3326 bakname = scmutil.origpath(ui, repo, rel)
3320 3327 ui.note(_('saving current version of %s as %s\n') %
3321 3328 (rel, bakname))
3322 3329 if not opts.get('dry_run'):
3323 3330 if interactive:
3324 3331 util.copyfile(target, bakname)
3325 3332 else:
3326 3333 util.rename(target, bakname)
3327 3334 if ui.verbose or not exact:
3328 3335 if not isinstance(msg, basestring):
3329 3336 msg = msg(abs)
3330 3337 ui.status(msg % rel)
3331 3338 elif exact:
3332 3339 ui.warn(msg % rel)
3333 3340 break
3334 3341
3335 3342 if not opts.get('dry_run'):
3336 3343 needdata = ('revert', 'add', 'undelete')
3337 3344 _revertprefetch(repo, ctx, *[actions[name][0] for name in needdata])
3338 3345 _performrevert(repo, parents, ctx, actions, interactive, tobackup)
3339 3346
3340 3347 if targetsubs:
3341 3348 # Revert the subrepos on the revert list
3342 3349 for sub in targetsubs:
3343 3350 try:
3344 3351 wctx.sub(sub).revert(ctx.substate[sub], *pats, **opts)
3345 3352 except KeyError:
3346 3353 raise error.Abort("subrepository '%s' does not exist in %s!"
3347 3354 % (sub, short(ctx.node())))
3348 3355
3349 3356 def _revertprefetch(repo, ctx, *files):
3350 3357 """Let extension changing the storage layer prefetch content"""
3351 3358 pass
3352 3359
3353 3360 def _performrevert(repo, parents, ctx, actions, interactive=False,
3354 3361 tobackup=None):
3355 3362 """function that actually perform all the actions computed for revert
3356 3363
3357 3364 This is an independent function to let extension to plug in and react to
3358 3365 the imminent revert.
3359 3366
3360 3367 Make sure you have the working directory locked when calling this function.
3361 3368 """
3362 3369 parent, p2 = parents
3363 3370 node = ctx.node()
3364 3371 excluded_files = []
3365 3372 matcher_opts = {"exclude": excluded_files}
3366 3373
3367 3374 def checkout(f):
3368 3375 fc = ctx[f]
3369 3376 repo.wwrite(f, fc.data(), fc.flags())
3370 3377
3371 3378 def doremove(f):
3372 3379 try:
3373 3380 repo.wvfs.unlinkpath(f)
3374 3381 except OSError:
3375 3382 pass
3376 3383 repo.dirstate.remove(f)
3377 3384
3378 3385 audit_path = pathutil.pathauditor(repo.root)
3379 3386 for f in actions['forget'][0]:
3380 3387 if interactive:
3381 3388 choice = repo.ui.promptchoice(
3382 3389 _("forget added file %s (Yn)?$$ &Yes $$ &No") % f)
3383 3390 if choice == 0:
3384 3391 repo.dirstate.drop(f)
3385 3392 else:
3386 3393 excluded_files.append(repo.wjoin(f))
3387 3394 else:
3388 3395 repo.dirstate.drop(f)
3389 3396 for f in actions['remove'][0]:
3390 3397 audit_path(f)
3391 3398 if interactive:
3392 3399 choice = repo.ui.promptchoice(
3393 3400 _("remove added file %s (Yn)?$$ &Yes $$ &No") % f)
3394 3401 if choice == 0:
3395 3402 doremove(f)
3396 3403 else:
3397 3404 excluded_files.append(repo.wjoin(f))
3398 3405 else:
3399 3406 doremove(f)
3400 3407 for f in actions['drop'][0]:
3401 3408 audit_path(f)
3402 3409 repo.dirstate.remove(f)
3403 3410
3404 3411 normal = None
3405 3412 if node == parent:
3406 3413 # We're reverting to our parent. If possible, we'd like status
3407 3414 # to report the file as clean. We have to use normallookup for
3408 3415 # merges to avoid losing information about merged/dirty files.
3409 3416 if p2 != nullid:
3410 3417 normal = repo.dirstate.normallookup
3411 3418 else:
3412 3419 normal = repo.dirstate.normal
3413 3420
3414 3421 newlyaddedandmodifiedfiles = set()
3415 3422 if interactive:
3416 3423 # Prompt the user for changes to revert
3417 3424 torevert = [repo.wjoin(f) for f in actions['revert'][0]]
3418 3425 m = scmutil.match(ctx, torevert, matcher_opts)
3419 3426 diffopts = patch.difffeatureopts(repo.ui, whitespace=True)
3420 3427 diffopts.nodates = True
3421 3428 diffopts.git = True
3422 3429 operation = 'discard'
3423 3430 reversehunks = True
3424 3431 if node != parent:
3425 3432 operation = 'revert'
3426 3433 reversehunks = repo.ui.configbool('experimental',
3427 3434 'revertalternateinteractivemode',
3428 3435 True)
3429 3436 if reversehunks:
3430 3437 diff = patch.diff(repo, ctx.node(), None, m, opts=diffopts)
3431 3438 else:
3432 3439 diff = patch.diff(repo, None, ctx.node(), m, opts=diffopts)
3433 3440 originalchunks = patch.parsepatch(diff)
3434 3441
3435 3442 try:
3436 3443
3437 3444 chunks, opts = recordfilter(repo.ui, originalchunks,
3438 3445 operation=operation)
3439 3446 if reversehunks:
3440 3447 chunks = patch.reversehunks(chunks)
3441 3448
3442 3449 except patch.PatchError as err:
3443 3450 raise error.Abort(_('error parsing patch: %s') % err)
3444 3451
3445 3452 newlyaddedandmodifiedfiles = newandmodified(chunks, originalchunks)
3446 3453 if tobackup is None:
3447 3454 tobackup = set()
3448 3455 # Apply changes
3449 3456 fp = stringio()
3450 3457 for c in chunks:
3451 3458 # Create a backup file only if this hunk should be backed up
3452 3459 if ishunk(c) and c.header.filename() in tobackup:
3453 3460 abs = c.header.filename()
3454 3461 target = repo.wjoin(abs)
3455 3462 bakname = scmutil.origpath(repo.ui, repo, m.rel(abs))
3456 3463 util.copyfile(target, bakname)
3457 3464 tobackup.remove(abs)
3458 3465 c.write(fp)
3459 3466 dopatch = fp.tell()
3460 3467 fp.seek(0)
3461 3468 if dopatch:
3462 3469 try:
3463 3470 patch.internalpatch(repo.ui, repo, fp, 1, eolmode=None)
3464 3471 except patch.PatchError as err:
3465 3472 raise error.Abort(str(err))
3466 3473 del fp
3467 3474 else:
3468 3475 for f in actions['revert'][0]:
3469 3476 checkout(f)
3470 3477 if normal:
3471 3478 normal(f)
3472 3479
3473 3480 for f in actions['add'][0]:
3474 3481 # Don't checkout modified files, they are already created by the diff
3475 3482 if f not in newlyaddedandmodifiedfiles:
3476 3483 checkout(f)
3477 3484 repo.dirstate.add(f)
3478 3485
3479 3486 normal = repo.dirstate.normallookup
3480 3487 if node == parent and p2 == nullid:
3481 3488 normal = repo.dirstate.normal
3482 3489 for f in actions['undelete'][0]:
3483 3490 checkout(f)
3484 3491 normal(f)
3485 3492
3486 3493 copied = copies.pathcopies(repo[parent], ctx)
3487 3494
3488 3495 for f in actions['add'][0] + actions['undelete'][0] + actions['revert'][0]:
3489 3496 if f in copied:
3490 3497 repo.dirstate.copy(copied[f], f)
3491 3498
3492 3499 class command(registrar.command):
3493 3500 def _doregister(self, func, name, *args, **kwargs):
3494 3501 func._deprecatedregistrar = True # flag for deprecwarn in extensions.py
3495 3502 return super(command, self)._doregister(func, name, *args, **kwargs)
3496 3503
3497 3504 # a list of (ui, repo, otherpeer, opts, missing) functions called by
3498 3505 # commands.outgoing. "missing" is "missing" of the result of
3499 3506 # "findcommonoutgoing()"
3500 3507 outgoinghooks = util.hooks()
3501 3508
3502 3509 # a list of (ui, repo) functions called by commands.summary
3503 3510 summaryhooks = util.hooks()
3504 3511
3505 3512 # a list of (ui, repo, opts, changes) functions called by commands.summary.
3506 3513 #
3507 3514 # functions should return tuple of booleans below, if 'changes' is None:
3508 3515 # (whether-incomings-are-needed, whether-outgoings-are-needed)
3509 3516 #
3510 3517 # otherwise, 'changes' is a tuple of tuples below:
3511 3518 # - (sourceurl, sourcebranch, sourcepeer, incoming)
3512 3519 # - (desturl, destbranch, destpeer, outgoing)
3513 3520 summaryremotehooks = util.hooks()
3514 3521
3515 3522 # A list of state files kept by multistep operations like graft.
3516 3523 # Since graft cannot be aborted, it is considered 'clearable' by update.
3517 3524 # note: bisect is intentionally excluded
3518 3525 # (state file, clearable, allowcommit, error, hint)
3519 3526 unfinishedstates = [
3520 3527 ('graftstate', True, False, _('graft in progress'),
3521 3528 _("use 'hg graft --continue' or 'hg update' to abort")),
3522 3529 ('updatestate', True, False, _('last update was interrupted'),
3523 3530 _("use 'hg update' to get a consistent checkout"))
3524 3531 ]
3525 3532
3526 3533 def checkunfinished(repo, commit=False):
3527 3534 '''Look for an unfinished multistep operation, like graft, and abort
3528 3535 if found. It's probably good to check this right before
3529 3536 bailifchanged().
3530 3537 '''
3531 3538 for f, clearable, allowcommit, msg, hint in unfinishedstates:
3532 3539 if commit and allowcommit:
3533 3540 continue
3534 3541 if repo.vfs.exists(f):
3535 3542 raise error.Abort(msg, hint=hint)
3536 3543
3537 3544 def clearunfinished(repo):
3538 3545 '''Check for unfinished operations (as above), and clear the ones
3539 3546 that are clearable.
3540 3547 '''
3541 3548 for f, clearable, allowcommit, msg, hint in unfinishedstates:
3542 3549 if not clearable and repo.vfs.exists(f):
3543 3550 raise error.Abort(msg, hint=hint)
3544 3551 for f, clearable, allowcommit, msg, hint in unfinishedstates:
3545 3552 if clearable and repo.vfs.exists(f):
3546 3553 util.unlink(repo.vfs.join(f))
3547 3554
3548 3555 afterresolvedstates = [
3549 3556 ('graftstate',
3550 3557 _('hg graft --continue')),
3551 3558 ]
3552 3559
3553 3560 def howtocontinue(repo):
3554 3561 '''Check for an unfinished operation and return the command to finish
3555 3562 it.
3556 3563
3557 3564 afterresolvedstates tuples define a .hg/{file} and the corresponding
3558 3565 command needed to finish it.
3559 3566
3560 3567 Returns a (msg, warning) tuple. 'msg' is a string and 'warning' is
3561 3568 a boolean.
3562 3569 '''
3563 3570 contmsg = _("continue: %s")
3564 3571 for f, msg in afterresolvedstates:
3565 3572 if repo.vfs.exists(f):
3566 3573 return contmsg % msg, True
3567 3574 workingctx = repo[None]
3568 3575 dirty = any(repo.status()) or any(workingctx.sub(s).dirty()
3569 3576 for s in workingctx.substate)
3570 3577 if dirty:
3571 3578 return contmsg % _("hg commit"), False
3572 3579 return None, None
3573 3580
3574 3581 def checkafterresolved(repo):
3575 3582 '''Inform the user about the next action after completing hg resolve
3576 3583
3577 3584 If there's a matching afterresolvedstates, howtocontinue will yield
3578 3585 repo.ui.warn as the reporter.
3579 3586
3580 3587 Otherwise, it will yield repo.ui.note.
3581 3588 '''
3582 3589 msg, warning = howtocontinue(repo)
3583 3590 if msg is not None:
3584 3591 if warning:
3585 3592 repo.ui.warn("%s\n" % msg)
3586 3593 else:
3587 3594 repo.ui.note("%s\n" % msg)
3588 3595
3589 3596 def wrongtooltocontinue(repo, task):
3590 3597 '''Raise an abort suggesting how to properly continue if there is an
3591 3598 active task.
3592 3599
3593 3600 Uses howtocontinue() to find the active task.
3594 3601
3595 3602 If there's no task (repo.ui.note for 'hg commit'), it does not offer
3596 3603 a hint.
3597 3604 '''
3598 3605 after = howtocontinue(repo)
3599 3606 hint = None
3600 3607 if after[1]:
3601 3608 hint = after[0]
3602 3609 raise error.Abort(_('no %s in progress') % task, hint=hint)
General Comments 0
You need to be logged in to leave comments. Login now