##// END OF EJS Templates
templating: make -T much more flexible...
Matt Mackall -
r20668:3a35ba26 default
parent child Browse files
Show More
@@ -1,735 +1,735 b''
1 1 # keyword.py - $Keyword$ expansion for Mercurial
2 2 #
3 3 # Copyright 2007-2012 Christian Ebert <blacktrash@gmx.net>
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 # $Id$
9 9 #
10 10 # Keyword expansion hack against the grain of a Distributed SCM
11 11 #
12 12 # There are many good reasons why this is not needed in a distributed
13 13 # SCM, still it may be useful in very small projects based on single
14 14 # files (like LaTeX packages), that are mostly addressed to an
15 15 # audience not running a version control system.
16 16 #
17 17 # For in-depth discussion refer to
18 18 # <http://mercurial.selenic.com/wiki/KeywordPlan>.
19 19 #
20 20 # Keyword expansion is based on Mercurial's changeset template mappings.
21 21 #
22 22 # Binary files are not touched.
23 23 #
24 24 # Files to act upon/ignore are specified in the [keyword] section.
25 25 # Customized keyword template mappings in the [keywordmaps] section.
26 26 #
27 27 # Run "hg help keyword" and "hg kwdemo" to get info on configuration.
28 28
29 29 '''expand keywords in tracked files
30 30
31 31 This extension expands RCS/CVS-like or self-customized $Keywords$ in
32 32 tracked text files selected by your configuration.
33 33
34 34 Keywords are only expanded in local repositories and not stored in the
35 35 change history. The mechanism can be regarded as a convenience for the
36 36 current user or for archive distribution.
37 37
38 38 Keywords expand to the changeset data pertaining to the latest change
39 39 relative to the working directory parent of each file.
40 40
41 41 Configuration is done in the [keyword], [keywordset] and [keywordmaps]
42 42 sections of hgrc files.
43 43
44 44 Example::
45 45
46 46 [keyword]
47 47 # expand keywords in every python file except those matching "x*"
48 48 **.py =
49 49 x* = ignore
50 50
51 51 [keywordset]
52 52 # prefer svn- over cvs-like default keywordmaps
53 53 svn = True
54 54
55 55 .. note::
56 56
57 57 The more specific you are in your filename patterns the less you
58 58 lose speed in huge repositories.
59 59
60 60 For [keywordmaps] template mapping and expansion demonstration and
61 61 control run :hg:`kwdemo`. See :hg:`help templates` for a list of
62 62 available templates and filters.
63 63
64 64 Three additional date template filters are provided:
65 65
66 66 :``utcdate``: "2006/09/18 15:13:13"
67 67 :``svnutcdate``: "2006-09-18 15:13:13Z"
68 68 :``svnisodate``: "2006-09-18 08:13:13 -700 (Mon, 18 Sep 2006)"
69 69
70 70 The default template mappings (view with :hg:`kwdemo -d`) can be
71 71 replaced with customized keywords and templates. Again, run
72 72 :hg:`kwdemo` to control the results of your configuration changes.
73 73
74 74 Before changing/disabling active keywords, you must run :hg:`kwshrink`
75 75 to avoid storing expanded keywords in the change history.
76 76
77 77 To force expansion after enabling it, or a configuration change, run
78 78 :hg:`kwexpand`.
79 79
80 80 Expansions spanning more than one line and incremental expansions,
81 81 like CVS' $Log$, are not supported. A keyword template map "Log =
82 82 {desc}" expands to the first line of the changeset description.
83 83 '''
84 84
85 85 from mercurial import commands, context, cmdutil, dispatch, filelog, extensions
86 86 from mercurial import localrepo, match, patch, templatefilters, templater, util
87 87 from mercurial import scmutil, pathutil
88 88 from mercurial.hgweb import webcommands
89 89 from mercurial.i18n import _
90 90 import os, re, shutil, tempfile
91 91
92 92 commands.optionalrepo += ' kwdemo'
93 93 commands.inferrepo += ' kwexpand kwfiles kwshrink'
94 94
95 95 cmdtable = {}
96 96 command = cmdutil.command(cmdtable)
97 97 testedwith = 'internal'
98 98
99 99 # hg commands that do not act on keywords
100 100 nokwcommands = ('add addremove annotate bundle export grep incoming init log'
101 101 ' outgoing push tip verify convert email glog')
102 102
103 103 # hg commands that trigger expansion only when writing to working dir,
104 104 # not when reading filelog, and unexpand when reading from working dir
105 105 restricted = 'merge kwexpand kwshrink record qrecord resolve transplant'
106 106
107 107 # names of extensions using dorecord
108 108 recordextensions = 'record'
109 109
110 110 colortable = {
111 111 'kwfiles.enabled': 'green bold',
112 112 'kwfiles.deleted': 'cyan bold underline',
113 113 'kwfiles.enabledunknown': 'green',
114 114 'kwfiles.ignored': 'bold',
115 115 'kwfiles.ignoredunknown': 'none'
116 116 }
117 117
118 118 # date like in cvs' $Date
119 119 def utcdate(text):
120 120 ''':utcdate: Date. Returns a UTC-date in this format: "2009/08/18 11:00:13".
121 121 '''
122 122 return util.datestr((util.parsedate(text)[0], 0), '%Y/%m/%d %H:%M:%S')
123 123 # date like in svn's $Date
124 124 def svnisodate(text):
125 125 ''':svnisodate: Date. Returns a date in this format: "2009-08-18 13:00:13
126 126 +0200 (Tue, 18 Aug 2009)".
127 127 '''
128 128 return util.datestr(text, '%Y-%m-%d %H:%M:%S %1%2 (%a, %d %b %Y)')
129 129 # date like in svn's $Id
130 130 def svnutcdate(text):
131 131 ''':svnutcdate: Date. Returns a UTC-date in this format: "2009-08-18
132 132 11:00:13Z".
133 133 '''
134 134 return util.datestr((util.parsedate(text)[0], 0), '%Y-%m-%d %H:%M:%SZ')
135 135
136 136 templatefilters.filters.update({'utcdate': utcdate,
137 137 'svnisodate': svnisodate,
138 138 'svnutcdate': svnutcdate})
139 139
140 140 # make keyword tools accessible
141 141 kwtools = {'templater': None, 'hgcmd': ''}
142 142
143 143 def _defaultkwmaps(ui):
144 144 '''Returns default keywordmaps according to keywordset configuration.'''
145 145 templates = {
146 146 'Revision': '{node|short}',
147 147 'Author': '{author|user}',
148 148 }
149 149 kwsets = ({
150 150 'Date': '{date|utcdate}',
151 151 'RCSfile': '{file|basename},v',
152 152 'RCSFile': '{file|basename},v', # kept for backwards compatibility
153 153 # with hg-keyword
154 154 'Source': '{root}/{file},v',
155 155 'Id': '{file|basename},v {node|short} {date|utcdate} {author|user}',
156 156 'Header': '{root}/{file},v {node|short} {date|utcdate} {author|user}',
157 157 }, {
158 158 'Date': '{date|svnisodate}',
159 159 'Id': '{file|basename},v {node|short} {date|svnutcdate} {author|user}',
160 160 'LastChangedRevision': '{node|short}',
161 161 'LastChangedBy': '{author|user}',
162 162 'LastChangedDate': '{date|svnisodate}',
163 163 })
164 164 templates.update(kwsets[ui.configbool('keywordset', 'svn')])
165 165 return templates
166 166
167 167 def _shrinktext(text, subfunc):
168 168 '''Helper for keyword expansion removal in text.
169 169 Depending on subfunc also returns number of substitutions.'''
170 170 return subfunc(r'$\1$', text)
171 171
172 172 def _preselect(wstatus, changed):
173 173 '''Retrieves modified and added files from a working directory state
174 174 and returns the subset of each contained in given changed files
175 175 retrieved from a change context.'''
176 176 modified, added = wstatus[:2]
177 177 modified = [f for f in modified if f in changed]
178 178 added = [f for f in added if f in changed]
179 179 return modified, added
180 180
181 181
182 182 class kwtemplater(object):
183 183 '''
184 184 Sets up keyword templates, corresponding keyword regex, and
185 185 provides keyword substitution functions.
186 186 '''
187 187
188 188 def __init__(self, ui, repo, inc, exc):
189 189 self.ui = ui
190 190 self.repo = repo
191 191 self.match = match.match(repo.root, '', [], inc, exc)
192 192 self.restrict = kwtools['hgcmd'] in restricted.split()
193 193 self.postcommit = False
194 194
195 195 kwmaps = self.ui.configitems('keywordmaps')
196 196 if kwmaps: # override default templates
197 197 self.templates = dict((k, templater.parsestring(v, False))
198 198 for k, v in kwmaps)
199 199 else:
200 200 self.templates = _defaultkwmaps(self.ui)
201 201
202 202 @util.propertycache
203 203 def escape(self):
204 204 '''Returns bar-separated and escaped keywords.'''
205 205 return '|'.join(map(re.escape, self.templates.keys()))
206 206
207 207 @util.propertycache
208 208 def rekw(self):
209 209 '''Returns regex for unexpanded keywords.'''
210 210 return re.compile(r'\$(%s)\$' % self.escape)
211 211
212 212 @util.propertycache
213 213 def rekwexp(self):
214 214 '''Returns regex for expanded keywords.'''
215 215 return re.compile(r'\$(%s): [^$\n\r]*? \$' % self.escape)
216 216
217 217 def substitute(self, data, path, ctx, subfunc):
218 218 '''Replaces keywords in data with expanded template.'''
219 219 def kwsub(mobj):
220 220 kw = mobj.group(1)
221 ct = cmdutil.changeset_templater(self.ui, self.repo, False, None
221 ct = cmdutil.changeset_templater(self.ui, self.repo, False, None,
222 222 self.templates[kw], '', False)
223 223 self.ui.pushbuffer()
224 224 ct.show(ctx, root=self.repo.root, file=path)
225 225 ekw = templatefilters.firstline(self.ui.popbuffer())
226 226 return '$%s: %s $' % (kw, ekw)
227 227 return subfunc(kwsub, data)
228 228
229 229 def linkctx(self, path, fileid):
230 230 '''Similar to filelog.linkrev, but returns a changectx.'''
231 231 return self.repo.filectx(path, fileid=fileid).changectx()
232 232
233 233 def expand(self, path, node, data):
234 234 '''Returns data with keywords expanded.'''
235 235 if not self.restrict and self.match(path) and not util.binary(data):
236 236 ctx = self.linkctx(path, node)
237 237 return self.substitute(data, path, ctx, self.rekw.sub)
238 238 return data
239 239
240 240 def iskwfile(self, cand, ctx):
241 241 '''Returns subset of candidates which are configured for keyword
242 242 expansion but are not symbolic links.'''
243 243 return [f for f in cand if self.match(f) and 'l' not in ctx.flags(f)]
244 244
245 245 def overwrite(self, ctx, candidates, lookup, expand, rekw=False):
246 246 '''Overwrites selected files expanding/shrinking keywords.'''
247 247 if self.restrict or lookup or self.postcommit: # exclude kw_copy
248 248 candidates = self.iskwfile(candidates, ctx)
249 249 if not candidates:
250 250 return
251 251 kwcmd = self.restrict and lookup # kwexpand/kwshrink
252 252 if self.restrict or expand and lookup:
253 253 mf = ctx.manifest()
254 254 if self.restrict or rekw:
255 255 re_kw = self.rekw
256 256 else:
257 257 re_kw = self.rekwexp
258 258 if expand:
259 259 msg = _('overwriting %s expanding keywords\n')
260 260 else:
261 261 msg = _('overwriting %s shrinking keywords\n')
262 262 for f in candidates:
263 263 if self.restrict:
264 264 data = self.repo.file(f).read(mf[f])
265 265 else:
266 266 data = self.repo.wread(f)
267 267 if util.binary(data):
268 268 continue
269 269 if expand:
270 270 if lookup:
271 271 ctx = self.linkctx(f, mf[f])
272 272 data, found = self.substitute(data, f, ctx, re_kw.subn)
273 273 elif self.restrict:
274 274 found = re_kw.search(data)
275 275 else:
276 276 data, found = _shrinktext(data, re_kw.subn)
277 277 if found:
278 278 self.ui.note(msg % f)
279 279 fp = self.repo.wopener(f, "wb", atomictemp=True)
280 280 fp.write(data)
281 281 fp.close()
282 282 if kwcmd:
283 283 self.repo.dirstate.normal(f)
284 284 elif self.postcommit:
285 285 self.repo.dirstate.normallookup(f)
286 286
287 287 def shrink(self, fname, text):
288 288 '''Returns text with all keyword substitutions removed.'''
289 289 if self.match(fname) and not util.binary(text):
290 290 return _shrinktext(text, self.rekwexp.sub)
291 291 return text
292 292
293 293 def shrinklines(self, fname, lines):
294 294 '''Returns lines with keyword substitutions removed.'''
295 295 if self.match(fname):
296 296 text = ''.join(lines)
297 297 if not util.binary(text):
298 298 return _shrinktext(text, self.rekwexp.sub).splitlines(True)
299 299 return lines
300 300
301 301 def wread(self, fname, data):
302 302 '''If in restricted mode returns data read from wdir with
303 303 keyword substitutions removed.'''
304 304 if self.restrict:
305 305 return self.shrink(fname, data)
306 306 return data
307 307
308 308 class kwfilelog(filelog.filelog):
309 309 '''
310 310 Subclass of filelog to hook into its read, add, cmp methods.
311 311 Keywords are "stored" unexpanded, and processed on reading.
312 312 '''
313 313 def __init__(self, opener, kwt, path):
314 314 super(kwfilelog, self).__init__(opener, path)
315 315 self.kwt = kwt
316 316 self.path = path
317 317
318 318 def read(self, node):
319 319 '''Expands keywords when reading filelog.'''
320 320 data = super(kwfilelog, self).read(node)
321 321 if self.renamed(node):
322 322 return data
323 323 return self.kwt.expand(self.path, node, data)
324 324
325 325 def add(self, text, meta, tr, link, p1=None, p2=None):
326 326 '''Removes keyword substitutions when adding to filelog.'''
327 327 text = self.kwt.shrink(self.path, text)
328 328 return super(kwfilelog, self).add(text, meta, tr, link, p1, p2)
329 329
330 330 def cmp(self, node, text):
331 331 '''Removes keyword substitutions for comparison.'''
332 332 text = self.kwt.shrink(self.path, text)
333 333 return super(kwfilelog, self).cmp(node, text)
334 334
335 335 def _status(ui, repo, wctx, kwt, *pats, **opts):
336 336 '''Bails out if [keyword] configuration is not active.
337 337 Returns status of working directory.'''
338 338 if kwt:
339 339 return repo.status(match=scmutil.match(wctx, pats, opts), clean=True,
340 340 unknown=opts.get('unknown') or opts.get('all'))
341 341 if ui.configitems('keyword'):
342 342 raise util.Abort(_('[keyword] patterns cannot match'))
343 343 raise util.Abort(_('no [keyword] patterns configured'))
344 344
345 345 def _kwfwrite(ui, repo, expand, *pats, **opts):
346 346 '''Selects files and passes them to kwtemplater.overwrite.'''
347 347 wctx = repo[None]
348 348 if len(wctx.parents()) > 1:
349 349 raise util.Abort(_('outstanding uncommitted merge'))
350 350 kwt = kwtools['templater']
351 351 wlock = repo.wlock()
352 352 try:
353 353 status = _status(ui, repo, wctx, kwt, *pats, **opts)
354 354 modified, added, removed, deleted, unknown, ignored, clean = status
355 355 if modified or added or removed or deleted:
356 356 raise util.Abort(_('outstanding uncommitted changes'))
357 357 kwt.overwrite(wctx, clean, True, expand)
358 358 finally:
359 359 wlock.release()
360 360
361 361 @command('kwdemo',
362 362 [('d', 'default', None, _('show default keyword template maps')),
363 363 ('f', 'rcfile', '',
364 364 _('read maps from rcfile'), _('FILE'))],
365 365 _('hg kwdemo [-d] [-f RCFILE] [TEMPLATEMAP]...'))
366 366 def demo(ui, repo, *args, **opts):
367 367 '''print [keywordmaps] configuration and an expansion example
368 368
369 369 Show current, custom, or default keyword template maps and their
370 370 expansions.
371 371
372 372 Extend the current configuration by specifying maps as arguments
373 373 and using -f/--rcfile to source an external hgrc file.
374 374
375 375 Use -d/--default to disable current configuration.
376 376
377 377 See :hg:`help templates` for information on templates and filters.
378 378 '''
379 379 def demoitems(section, items):
380 380 ui.write('[%s]\n' % section)
381 381 for k, v in sorted(items):
382 382 ui.write('%s = %s\n' % (k, v))
383 383
384 384 fn = 'demo.txt'
385 385 tmpdir = tempfile.mkdtemp('', 'kwdemo.')
386 386 ui.note(_('creating temporary repository at %s\n') % tmpdir)
387 387 repo = localrepo.localrepository(repo.baseui, tmpdir, True)
388 388 ui.setconfig('keyword', fn, '')
389 389 svn = ui.configbool('keywordset', 'svn')
390 390 # explicitly set keywordset for demo output
391 391 ui.setconfig('keywordset', 'svn', svn)
392 392
393 393 uikwmaps = ui.configitems('keywordmaps')
394 394 if args or opts.get('rcfile'):
395 395 ui.status(_('\n\tconfiguration using custom keyword template maps\n'))
396 396 if uikwmaps:
397 397 ui.status(_('\textending current template maps\n'))
398 398 if opts.get('default') or not uikwmaps:
399 399 if svn:
400 400 ui.status(_('\toverriding default svn keywordset\n'))
401 401 else:
402 402 ui.status(_('\toverriding default cvs keywordset\n'))
403 403 if opts.get('rcfile'):
404 404 ui.readconfig(opts.get('rcfile'))
405 405 if args:
406 406 # simulate hgrc parsing
407 407 rcmaps = ['[keywordmaps]\n'] + [a + '\n' for a in args]
408 408 fp = repo.opener('hgrc', 'w')
409 409 fp.writelines(rcmaps)
410 410 fp.close()
411 411 ui.readconfig(repo.join('hgrc'))
412 412 kwmaps = dict(ui.configitems('keywordmaps'))
413 413 elif opts.get('default'):
414 414 if svn:
415 415 ui.status(_('\n\tconfiguration using default svn keywordset\n'))
416 416 else:
417 417 ui.status(_('\n\tconfiguration using default cvs keywordset\n'))
418 418 kwmaps = _defaultkwmaps(ui)
419 419 if uikwmaps:
420 420 ui.status(_('\tdisabling current template maps\n'))
421 421 for k, v in kwmaps.iteritems():
422 422 ui.setconfig('keywordmaps', k, v)
423 423 else:
424 424 ui.status(_('\n\tconfiguration using current keyword template maps\n'))
425 425 if uikwmaps:
426 426 kwmaps = dict(uikwmaps)
427 427 else:
428 428 kwmaps = _defaultkwmaps(ui)
429 429
430 430 uisetup(ui)
431 431 reposetup(ui, repo)
432 432 ui.write('[extensions]\nkeyword =\n')
433 433 demoitems('keyword', ui.configitems('keyword'))
434 434 demoitems('keywordset', ui.configitems('keywordset'))
435 435 demoitems('keywordmaps', kwmaps.iteritems())
436 436 keywords = '$' + '$\n$'.join(sorted(kwmaps.keys())) + '$\n'
437 437 repo.wopener.write(fn, keywords)
438 438 repo[None].add([fn])
439 439 ui.note(_('\nkeywords written to %s:\n') % fn)
440 440 ui.note(keywords)
441 441 wlock = repo.wlock()
442 442 try:
443 443 repo.dirstate.setbranch('demobranch')
444 444 finally:
445 445 wlock.release()
446 446 for name, cmd in ui.configitems('hooks'):
447 447 if name.split('.', 1)[0].find('commit') > -1:
448 448 repo.ui.setconfig('hooks', name, '')
449 449 msg = _('hg keyword configuration and expansion example')
450 450 ui.note(("hg ci -m '%s'\n" % msg))
451 451 repo.commit(text=msg)
452 452 ui.status(_('\n\tkeywords expanded\n'))
453 453 ui.write(repo.wread(fn))
454 454 shutil.rmtree(tmpdir, ignore_errors=True)
455 455
456 456 @command('kwexpand', commands.walkopts, _('hg kwexpand [OPTION]... [FILE]...'))
457 457 def expand(ui, repo, *pats, **opts):
458 458 '''expand keywords in the working directory
459 459
460 460 Run after (re)enabling keyword expansion.
461 461
462 462 kwexpand refuses to run if given files contain local changes.
463 463 '''
464 464 # 3rd argument sets expansion to True
465 465 _kwfwrite(ui, repo, True, *pats, **opts)
466 466
467 467 @command('kwfiles',
468 468 [('A', 'all', None, _('show keyword status flags of all files')),
469 469 ('i', 'ignore', None, _('show files excluded from expansion')),
470 470 ('u', 'unknown', None, _('only show unknown (not tracked) files')),
471 471 ] + commands.walkopts,
472 472 _('hg kwfiles [OPTION]... [FILE]...'))
473 473 def files(ui, repo, *pats, **opts):
474 474 '''show files configured for keyword expansion
475 475
476 476 List which files in the working directory are matched by the
477 477 [keyword] configuration patterns.
478 478
479 479 Useful to prevent inadvertent keyword expansion and to speed up
480 480 execution by including only files that are actual candidates for
481 481 expansion.
482 482
483 483 See :hg:`help keyword` on how to construct patterns both for
484 484 inclusion and exclusion of files.
485 485
486 486 With -A/--all and -v/--verbose the codes used to show the status
487 487 of files are::
488 488
489 489 K = keyword expansion candidate
490 490 k = keyword expansion candidate (not tracked)
491 491 I = ignored
492 492 i = ignored (not tracked)
493 493 '''
494 494 kwt = kwtools['templater']
495 495 wctx = repo[None]
496 496 status = _status(ui, repo, wctx, kwt, *pats, **opts)
497 497 cwd = pats and repo.getcwd() or ''
498 498 modified, added, removed, deleted, unknown, ignored, clean = status
499 499 files = []
500 500 if not opts.get('unknown') or opts.get('all'):
501 501 files = sorted(modified + added + clean)
502 502 kwfiles = kwt.iskwfile(files, wctx)
503 503 kwdeleted = kwt.iskwfile(deleted, wctx)
504 504 kwunknown = kwt.iskwfile(unknown, wctx)
505 505 if not opts.get('ignore') or opts.get('all'):
506 506 showfiles = kwfiles, kwdeleted, kwunknown
507 507 else:
508 508 showfiles = [], [], []
509 509 if opts.get('all') or opts.get('ignore'):
510 510 showfiles += ([f for f in files if f not in kwfiles],
511 511 [f for f in unknown if f not in kwunknown])
512 512 kwlabels = 'enabled deleted enabledunknown ignored ignoredunknown'.split()
513 513 kwstates = zip(kwlabels, 'K!kIi', showfiles)
514 514 fm = ui.formatter('kwfiles', opts)
515 515 fmt = '%.0s%s\n'
516 516 if opts.get('all') or ui.verbose:
517 517 fmt = '%s %s\n'
518 518 for kwstate, char, filenames in kwstates:
519 519 label = 'kwfiles.' + kwstate
520 520 for f in filenames:
521 521 fm.startitem()
522 522 fm.write('kwstatus path', fmt, char,
523 523 repo.pathto(f, cwd), label=label)
524 524 fm.end()
525 525
526 526 @command('kwshrink', commands.walkopts, _('hg kwshrink [OPTION]... [FILE]...'))
527 527 def shrink(ui, repo, *pats, **opts):
528 528 '''revert expanded keywords in the working directory
529 529
530 530 Must be run before changing/disabling active keywords.
531 531
532 532 kwshrink refuses to run if given files contain local changes.
533 533 '''
534 534 # 3rd argument sets expansion to False
535 535 _kwfwrite(ui, repo, False, *pats, **opts)
536 536
537 537
538 538 def uisetup(ui):
539 539 ''' Monkeypatches dispatch._parse to retrieve user command.'''
540 540
541 541 def kwdispatch_parse(orig, ui, args):
542 542 '''Monkeypatch dispatch._parse to obtain running hg command.'''
543 543 cmd, func, args, options, cmdoptions = orig(ui, args)
544 544 kwtools['hgcmd'] = cmd
545 545 return cmd, func, args, options, cmdoptions
546 546
547 547 extensions.wrapfunction(dispatch, '_parse', kwdispatch_parse)
548 548
549 549 def reposetup(ui, repo):
550 550 '''Sets up repo as kwrepo for keyword substitution.
551 551 Overrides file method to return kwfilelog instead of filelog
552 552 if file matches user configuration.
553 553 Wraps commit to overwrite configured files with updated
554 554 keyword substitutions.
555 555 Monkeypatches patch and webcommands.'''
556 556
557 557 try:
558 558 if (not repo.local() or kwtools['hgcmd'] in nokwcommands.split()
559 559 or '.hg' in util.splitpath(repo.root)
560 560 or repo._url.startswith('bundle:')):
561 561 return
562 562 except AttributeError:
563 563 pass
564 564
565 565 inc, exc = [], ['.hg*']
566 566 for pat, opt in ui.configitems('keyword'):
567 567 if opt != 'ignore':
568 568 inc.append(pat)
569 569 else:
570 570 exc.append(pat)
571 571 if not inc:
572 572 return
573 573
574 574 kwtools['templater'] = kwt = kwtemplater(ui, repo, inc, exc)
575 575
576 576 class kwrepo(repo.__class__):
577 577 def file(self, f):
578 578 if f[0] == '/':
579 579 f = f[1:]
580 580 return kwfilelog(self.sopener, kwt, f)
581 581
582 582 def wread(self, filename):
583 583 data = super(kwrepo, self).wread(filename)
584 584 return kwt.wread(filename, data)
585 585
586 586 def commit(self, *args, **opts):
587 587 # use custom commitctx for user commands
588 588 # other extensions can still wrap repo.commitctx directly
589 589 self.commitctx = self.kwcommitctx
590 590 try:
591 591 return super(kwrepo, self).commit(*args, **opts)
592 592 finally:
593 593 del self.commitctx
594 594
595 595 def kwcommitctx(self, ctx, error=False):
596 596 n = super(kwrepo, self).commitctx(ctx, error)
597 597 # no lock needed, only called from repo.commit() which already locks
598 598 if not kwt.postcommit:
599 599 restrict = kwt.restrict
600 600 kwt.restrict = True
601 601 kwt.overwrite(self[n], sorted(ctx.added() + ctx.modified()),
602 602 False, True)
603 603 kwt.restrict = restrict
604 604 return n
605 605
606 606 def rollback(self, dryrun=False, force=False):
607 607 wlock = self.wlock()
608 608 try:
609 609 if not dryrun:
610 610 changed = self['.'].files()
611 611 ret = super(kwrepo, self).rollback(dryrun, force)
612 612 if not dryrun:
613 613 ctx = self['.']
614 614 modified, added = _preselect(self[None].status(), changed)
615 615 kwt.overwrite(ctx, modified, True, True)
616 616 kwt.overwrite(ctx, added, True, False)
617 617 return ret
618 618 finally:
619 619 wlock.release()
620 620
621 621 # monkeypatches
622 622 def kwpatchfile_init(orig, self, ui, gp, backend, store, eolmode=None):
623 623 '''Monkeypatch/wrap patch.patchfile.__init__ to avoid
624 624 rejects or conflicts due to expanded keywords in working dir.'''
625 625 orig(self, ui, gp, backend, store, eolmode)
626 626 # shrink keywords read from working dir
627 627 self.lines = kwt.shrinklines(self.fname, self.lines)
628 628
629 629 def kw_diff(orig, repo, node1=None, node2=None, match=None, changes=None,
630 630 opts=None, prefix=''):
631 631 '''Monkeypatch patch.diff to avoid expansion.'''
632 632 kwt.restrict = True
633 633 return orig(repo, node1, node2, match, changes, opts, prefix)
634 634
635 635 def kwweb_skip(orig, web, req, tmpl):
636 636 '''Wraps webcommands.x turning off keyword expansion.'''
637 637 kwt.match = util.never
638 638 return orig(web, req, tmpl)
639 639
640 640 def kw_amend(orig, ui, repo, commitfunc, old, extra, pats, opts):
641 641 '''Wraps cmdutil.amend expanding keywords after amend.'''
642 642 wlock = repo.wlock()
643 643 try:
644 644 kwt.postcommit = True
645 645 newid = orig(ui, repo, commitfunc, old, extra, pats, opts)
646 646 if newid != old.node():
647 647 ctx = repo[newid]
648 648 kwt.restrict = True
649 649 kwt.overwrite(ctx, ctx.files(), False, True)
650 650 kwt.restrict = False
651 651 return newid
652 652 finally:
653 653 wlock.release()
654 654
655 655 def kw_copy(orig, ui, repo, pats, opts, rename=False):
656 656 '''Wraps cmdutil.copy so that copy/rename destinations do not
657 657 contain expanded keywords.
658 658 Note that the source of a regular file destination may also be a
659 659 symlink:
660 660 hg cp sym x -> x is symlink
661 661 cp sym x; hg cp -A sym x -> x is file (maybe expanded keywords)
662 662 For the latter we have to follow the symlink to find out whether its
663 663 target is configured for expansion and we therefore must unexpand the
664 664 keywords in the destination.'''
665 665 wlock = repo.wlock()
666 666 try:
667 667 orig(ui, repo, pats, opts, rename)
668 668 if opts.get('dry_run'):
669 669 return
670 670 wctx = repo[None]
671 671 cwd = repo.getcwd()
672 672
673 673 def haskwsource(dest):
674 674 '''Returns true if dest is a regular file and configured for
675 675 expansion or a symlink which points to a file configured for
676 676 expansion. '''
677 677 source = repo.dirstate.copied(dest)
678 678 if 'l' in wctx.flags(source):
679 679 source = pathutil.canonpath(repo.root, cwd,
680 680 os.path.realpath(source))
681 681 return kwt.match(source)
682 682
683 683 candidates = [f for f in repo.dirstate.copies() if
684 684 'l' not in wctx.flags(f) and haskwsource(f)]
685 685 kwt.overwrite(wctx, candidates, False, False)
686 686 finally:
687 687 wlock.release()
688 688
689 689 def kw_dorecord(orig, ui, repo, commitfunc, *pats, **opts):
690 690 '''Wraps record.dorecord expanding keywords after recording.'''
691 691 wlock = repo.wlock()
692 692 try:
693 693 # record returns 0 even when nothing has changed
694 694 # therefore compare nodes before and after
695 695 kwt.postcommit = True
696 696 ctx = repo['.']
697 697 wstatus = repo[None].status()
698 698 ret = orig(ui, repo, commitfunc, *pats, **opts)
699 699 recctx = repo['.']
700 700 if ctx != recctx:
701 701 modified, added = _preselect(wstatus, recctx.files())
702 702 kwt.restrict = False
703 703 kwt.overwrite(recctx, modified, False, True)
704 704 kwt.overwrite(recctx, added, False, True, True)
705 705 kwt.restrict = True
706 706 return ret
707 707 finally:
708 708 wlock.release()
709 709
710 710 def kwfilectx_cmp(orig, self, fctx):
711 711 # keyword affects data size, comparing wdir and filelog size does
712 712 # not make sense
713 713 if (fctx._filerev is None and
714 714 (self._repo._encodefilterpats or
715 715 kwt.match(fctx.path()) and 'l' not in fctx.flags() or
716 716 self.size() - 4 == fctx.size()) or
717 717 self.size() == fctx.size()):
718 718 return self._filelog.cmp(self._filenode, fctx.data())
719 719 return True
720 720
721 721 extensions.wrapfunction(context.filectx, 'cmp', kwfilectx_cmp)
722 722 extensions.wrapfunction(patch.patchfile, '__init__', kwpatchfile_init)
723 723 extensions.wrapfunction(patch, 'diff', kw_diff)
724 724 extensions.wrapfunction(cmdutil, 'amend', kw_amend)
725 725 extensions.wrapfunction(cmdutil, 'copy', kw_copy)
726 726 for c in 'annotate changeset rev filediff diff'.split():
727 727 extensions.wrapfunction(webcommands, c, kwweb_skip)
728 728 for name in recordextensions.split():
729 729 try:
730 730 record = extensions.find(name)
731 731 extensions.wrapfunction(record, 'dorecord', kw_dorecord)
732 732 except KeyError:
733 733 pass
734 734
735 735 repo.__class__ = kwrepo
@@ -1,2315 +1,2348 b''
1 1 # cmdutil.py - help for command processing in mercurial
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from node import hex, nullid, nullrev, short
9 9 from i18n import _
10 10 import os, sys, errno, re, tempfile
11 11 import util, scmutil, templater, patch, error, templatekw, revlog, copies
12 12 import match as matchmod
13 13 import context, repair, graphmod, revset, phases, obsolete, pathutil
14 14 import changelog
15 15 import bookmarks
16 16 import lock as lockmod
17 17
18 18 def parsealiases(cmd):
19 19 return cmd.lstrip("^").split("|")
20 20
21 21 def findpossible(cmd, table, strict=False):
22 22 """
23 23 Return cmd -> (aliases, command table entry)
24 24 for each matching command.
25 25 Return debug commands (or their aliases) only if no normal command matches.
26 26 """
27 27 choice = {}
28 28 debugchoice = {}
29 29
30 30 if cmd in table:
31 31 # short-circuit exact matches, "log" alias beats "^log|history"
32 32 keys = [cmd]
33 33 else:
34 34 keys = table.keys()
35 35
36 36 for e in keys:
37 37 aliases = parsealiases(e)
38 38 found = None
39 39 if cmd in aliases:
40 40 found = cmd
41 41 elif not strict:
42 42 for a in aliases:
43 43 if a.startswith(cmd):
44 44 found = a
45 45 break
46 46 if found is not None:
47 47 if aliases[0].startswith("debug") or found.startswith("debug"):
48 48 debugchoice[found] = (aliases, table[e])
49 49 else:
50 50 choice[found] = (aliases, table[e])
51 51
52 52 if not choice and debugchoice:
53 53 choice = debugchoice
54 54
55 55 return choice
56 56
57 57 def findcmd(cmd, table, strict=True):
58 58 """Return (aliases, command table entry) for command string."""
59 59 choice = findpossible(cmd, table, strict)
60 60
61 61 if cmd in choice:
62 62 return choice[cmd]
63 63
64 64 if len(choice) > 1:
65 65 clist = choice.keys()
66 66 clist.sort()
67 67 raise error.AmbiguousCommand(cmd, clist)
68 68
69 69 if choice:
70 70 return choice.values()[0]
71 71
72 72 raise error.UnknownCommand(cmd)
73 73
74 74 def findrepo(p):
75 75 while not os.path.isdir(os.path.join(p, ".hg")):
76 76 oldp, p = p, os.path.dirname(p)
77 77 if p == oldp:
78 78 return None
79 79
80 80 return p
81 81
82 82 def bailifchanged(repo):
83 83 if repo.dirstate.p2() != nullid:
84 84 raise util.Abort(_('outstanding uncommitted merge'))
85 85 modified, added, removed, deleted = repo.status()[:4]
86 86 if modified or added or removed or deleted:
87 87 raise util.Abort(_('uncommitted changes'))
88 88 ctx = repo[None]
89 89 for s in sorted(ctx.substate):
90 90 if ctx.sub(s).dirty():
91 91 raise util.Abort(_("uncommitted changes in subrepo %s") % s)
92 92
93 93 def logmessage(ui, opts):
94 94 """ get the log message according to -m and -l option """
95 95 message = opts.get('message')
96 96 logfile = opts.get('logfile')
97 97
98 98 if message and logfile:
99 99 raise util.Abort(_('options --message and --logfile are mutually '
100 100 'exclusive'))
101 101 if not message and logfile:
102 102 try:
103 103 if logfile == '-':
104 104 message = ui.fin.read()
105 105 else:
106 106 message = '\n'.join(util.readfile(logfile).splitlines())
107 107 except IOError, inst:
108 108 raise util.Abort(_("can't read commit message '%s': %s") %
109 109 (logfile, inst.strerror))
110 110 return message
111 111
112 112 def loglimit(opts):
113 113 """get the log limit according to option -l/--limit"""
114 114 limit = opts.get('limit')
115 115 if limit:
116 116 try:
117 117 limit = int(limit)
118 118 except ValueError:
119 119 raise util.Abort(_('limit must be a positive integer'))
120 120 if limit <= 0:
121 121 raise util.Abort(_('limit must be positive'))
122 122 else:
123 123 limit = None
124 124 return limit
125 125
126 126 def makefilename(repo, pat, node, desc=None,
127 127 total=None, seqno=None, revwidth=None, pathname=None):
128 128 node_expander = {
129 129 'H': lambda: hex(node),
130 130 'R': lambda: str(repo.changelog.rev(node)),
131 131 'h': lambda: short(node),
132 132 'm': lambda: re.sub('[^\w]', '_', str(desc))
133 133 }
134 134 expander = {
135 135 '%': lambda: '%',
136 136 'b': lambda: os.path.basename(repo.root),
137 137 }
138 138
139 139 try:
140 140 if node:
141 141 expander.update(node_expander)
142 142 if node:
143 143 expander['r'] = (lambda:
144 144 str(repo.changelog.rev(node)).zfill(revwidth or 0))
145 145 if total is not None:
146 146 expander['N'] = lambda: str(total)
147 147 if seqno is not None:
148 148 expander['n'] = lambda: str(seqno)
149 149 if total is not None and seqno is not None:
150 150 expander['n'] = lambda: str(seqno).zfill(len(str(total)))
151 151 if pathname is not None:
152 152 expander['s'] = lambda: os.path.basename(pathname)
153 153 expander['d'] = lambda: os.path.dirname(pathname) or '.'
154 154 expander['p'] = lambda: pathname
155 155
156 156 newname = []
157 157 patlen = len(pat)
158 158 i = 0
159 159 while i < patlen:
160 160 c = pat[i]
161 161 if c == '%':
162 162 i += 1
163 163 c = pat[i]
164 164 c = expander[c]()
165 165 newname.append(c)
166 166 i += 1
167 167 return ''.join(newname)
168 168 except KeyError, inst:
169 169 raise util.Abort(_("invalid format spec '%%%s' in output filename") %
170 170 inst.args[0])
171 171
172 172 def makefileobj(repo, pat, node=None, desc=None, total=None,
173 173 seqno=None, revwidth=None, mode='wb', modemap=None,
174 174 pathname=None):
175 175
176 176 writable = mode not in ('r', 'rb')
177 177
178 178 if not pat or pat == '-':
179 179 fp = writable and repo.ui.fout or repo.ui.fin
180 180 if util.safehasattr(fp, 'fileno'):
181 181 return os.fdopen(os.dup(fp.fileno()), mode)
182 182 else:
183 183 # if this fp can't be duped properly, return
184 184 # a dummy object that can be closed
185 185 class wrappedfileobj(object):
186 186 noop = lambda x: None
187 187 def __init__(self, f):
188 188 self.f = f
189 189 def __getattr__(self, attr):
190 190 if attr == 'close':
191 191 return self.noop
192 192 else:
193 193 return getattr(self.f, attr)
194 194
195 195 return wrappedfileobj(fp)
196 196 if util.safehasattr(pat, 'write') and writable:
197 197 return pat
198 198 if util.safehasattr(pat, 'read') and 'r' in mode:
199 199 return pat
200 200 fn = makefilename(repo, pat, node, desc, total, seqno, revwidth, pathname)
201 201 if modemap is not None:
202 202 mode = modemap.get(fn, mode)
203 203 if mode == 'wb':
204 204 modemap[fn] = 'ab'
205 205 return open(fn, mode)
206 206
207 207 def openrevlog(repo, cmd, file_, opts):
208 208 """opens the changelog, manifest, a filelog or a given revlog"""
209 209 cl = opts['changelog']
210 210 mf = opts['manifest']
211 211 msg = None
212 212 if cl and mf:
213 213 msg = _('cannot specify --changelog and --manifest at the same time')
214 214 elif cl or mf:
215 215 if file_:
216 216 msg = _('cannot specify filename with --changelog or --manifest')
217 217 elif not repo:
218 218 msg = _('cannot specify --changelog or --manifest '
219 219 'without a repository')
220 220 if msg:
221 221 raise util.Abort(msg)
222 222
223 223 r = None
224 224 if repo:
225 225 if cl:
226 226 r = repo.changelog
227 227 elif mf:
228 228 r = repo.manifest
229 229 elif file_:
230 230 filelog = repo.file(file_)
231 231 if len(filelog):
232 232 r = filelog
233 233 if not r:
234 234 if not file_:
235 235 raise error.CommandError(cmd, _('invalid arguments'))
236 236 if not os.path.isfile(file_):
237 237 raise util.Abort(_("revlog '%s' not found") % file_)
238 238 r = revlog.revlog(scmutil.opener(os.getcwd(), audit=False),
239 239 file_[:-2] + ".i")
240 240 return r
241 241
242 242 def copy(ui, repo, pats, opts, rename=False):
243 243 # called with the repo lock held
244 244 #
245 245 # hgsep => pathname that uses "/" to separate directories
246 246 # ossep => pathname that uses os.sep to separate directories
247 247 cwd = repo.getcwd()
248 248 targets = {}
249 249 after = opts.get("after")
250 250 dryrun = opts.get("dry_run")
251 251 wctx = repo[None]
252 252
253 253 def walkpat(pat):
254 254 srcs = []
255 255 badstates = after and '?' or '?r'
256 256 m = scmutil.match(repo[None], [pat], opts, globbed=True)
257 257 for abs in repo.walk(m):
258 258 state = repo.dirstate[abs]
259 259 rel = m.rel(abs)
260 260 exact = m.exact(abs)
261 261 if state in badstates:
262 262 if exact and state == '?':
263 263 ui.warn(_('%s: not copying - file is not managed\n') % rel)
264 264 if exact and state == 'r':
265 265 ui.warn(_('%s: not copying - file has been marked for'
266 266 ' remove\n') % rel)
267 267 continue
268 268 # abs: hgsep
269 269 # rel: ossep
270 270 srcs.append((abs, rel, exact))
271 271 return srcs
272 272
273 273 # abssrc: hgsep
274 274 # relsrc: ossep
275 275 # otarget: ossep
276 276 def copyfile(abssrc, relsrc, otarget, exact):
277 277 abstarget = pathutil.canonpath(repo.root, cwd, otarget)
278 278 if '/' in abstarget:
279 279 # We cannot normalize abstarget itself, this would prevent
280 280 # case only renames, like a => A.
281 281 abspath, absname = abstarget.rsplit('/', 1)
282 282 abstarget = repo.dirstate.normalize(abspath) + '/' + absname
283 283 reltarget = repo.pathto(abstarget, cwd)
284 284 target = repo.wjoin(abstarget)
285 285 src = repo.wjoin(abssrc)
286 286 state = repo.dirstate[abstarget]
287 287
288 288 scmutil.checkportable(ui, abstarget)
289 289
290 290 # check for collisions
291 291 prevsrc = targets.get(abstarget)
292 292 if prevsrc is not None:
293 293 ui.warn(_('%s: not overwriting - %s collides with %s\n') %
294 294 (reltarget, repo.pathto(abssrc, cwd),
295 295 repo.pathto(prevsrc, cwd)))
296 296 return
297 297
298 298 # check for overwrites
299 299 exists = os.path.lexists(target)
300 300 samefile = False
301 301 if exists and abssrc != abstarget:
302 302 if (repo.dirstate.normalize(abssrc) ==
303 303 repo.dirstate.normalize(abstarget)):
304 304 if not rename:
305 305 ui.warn(_("%s: can't copy - same file\n") % reltarget)
306 306 return
307 307 exists = False
308 308 samefile = True
309 309
310 310 if not after and exists or after and state in 'mn':
311 311 if not opts['force']:
312 312 ui.warn(_('%s: not overwriting - file exists\n') %
313 313 reltarget)
314 314 return
315 315
316 316 if after:
317 317 if not exists:
318 318 if rename:
319 319 ui.warn(_('%s: not recording move - %s does not exist\n') %
320 320 (relsrc, reltarget))
321 321 else:
322 322 ui.warn(_('%s: not recording copy - %s does not exist\n') %
323 323 (relsrc, reltarget))
324 324 return
325 325 elif not dryrun:
326 326 try:
327 327 if exists:
328 328 os.unlink(target)
329 329 targetdir = os.path.dirname(target) or '.'
330 330 if not os.path.isdir(targetdir):
331 331 os.makedirs(targetdir)
332 332 if samefile:
333 333 tmp = target + "~hgrename"
334 334 os.rename(src, tmp)
335 335 os.rename(tmp, target)
336 336 else:
337 337 util.copyfile(src, target)
338 338 srcexists = True
339 339 except IOError, inst:
340 340 if inst.errno == errno.ENOENT:
341 341 ui.warn(_('%s: deleted in working copy\n') % relsrc)
342 342 srcexists = False
343 343 else:
344 344 ui.warn(_('%s: cannot copy - %s\n') %
345 345 (relsrc, inst.strerror))
346 346 return True # report a failure
347 347
348 348 if ui.verbose or not exact:
349 349 if rename:
350 350 ui.status(_('moving %s to %s\n') % (relsrc, reltarget))
351 351 else:
352 352 ui.status(_('copying %s to %s\n') % (relsrc, reltarget))
353 353
354 354 targets[abstarget] = abssrc
355 355
356 356 # fix up dirstate
357 357 scmutil.dirstatecopy(ui, repo, wctx, abssrc, abstarget,
358 358 dryrun=dryrun, cwd=cwd)
359 359 if rename and not dryrun:
360 360 if not after and srcexists and not samefile:
361 361 util.unlinkpath(repo.wjoin(abssrc))
362 362 wctx.forget([abssrc])
363 363
364 364 # pat: ossep
365 365 # dest ossep
366 366 # srcs: list of (hgsep, hgsep, ossep, bool)
367 367 # return: function that takes hgsep and returns ossep
368 368 def targetpathfn(pat, dest, srcs):
369 369 if os.path.isdir(pat):
370 370 abspfx = pathutil.canonpath(repo.root, cwd, pat)
371 371 abspfx = util.localpath(abspfx)
372 372 if destdirexists:
373 373 striplen = len(os.path.split(abspfx)[0])
374 374 else:
375 375 striplen = len(abspfx)
376 376 if striplen:
377 377 striplen += len(os.sep)
378 378 res = lambda p: os.path.join(dest, util.localpath(p)[striplen:])
379 379 elif destdirexists:
380 380 res = lambda p: os.path.join(dest,
381 381 os.path.basename(util.localpath(p)))
382 382 else:
383 383 res = lambda p: dest
384 384 return res
385 385
386 386 # pat: ossep
387 387 # dest ossep
388 388 # srcs: list of (hgsep, hgsep, ossep, bool)
389 389 # return: function that takes hgsep and returns ossep
390 390 def targetpathafterfn(pat, dest, srcs):
391 391 if matchmod.patkind(pat):
392 392 # a mercurial pattern
393 393 res = lambda p: os.path.join(dest,
394 394 os.path.basename(util.localpath(p)))
395 395 else:
396 396 abspfx = pathutil.canonpath(repo.root, cwd, pat)
397 397 if len(abspfx) < len(srcs[0][0]):
398 398 # A directory. Either the target path contains the last
399 399 # component of the source path or it does not.
400 400 def evalpath(striplen):
401 401 score = 0
402 402 for s in srcs:
403 403 t = os.path.join(dest, util.localpath(s[0])[striplen:])
404 404 if os.path.lexists(t):
405 405 score += 1
406 406 return score
407 407
408 408 abspfx = util.localpath(abspfx)
409 409 striplen = len(abspfx)
410 410 if striplen:
411 411 striplen += len(os.sep)
412 412 if os.path.isdir(os.path.join(dest, os.path.split(abspfx)[1])):
413 413 score = evalpath(striplen)
414 414 striplen1 = len(os.path.split(abspfx)[0])
415 415 if striplen1:
416 416 striplen1 += len(os.sep)
417 417 if evalpath(striplen1) > score:
418 418 striplen = striplen1
419 419 res = lambda p: os.path.join(dest,
420 420 util.localpath(p)[striplen:])
421 421 else:
422 422 # a file
423 423 if destdirexists:
424 424 res = lambda p: os.path.join(dest,
425 425 os.path.basename(util.localpath(p)))
426 426 else:
427 427 res = lambda p: dest
428 428 return res
429 429
430 430
431 431 pats = scmutil.expandpats(pats)
432 432 if not pats:
433 433 raise util.Abort(_('no source or destination specified'))
434 434 if len(pats) == 1:
435 435 raise util.Abort(_('no destination specified'))
436 436 dest = pats.pop()
437 437 destdirexists = os.path.isdir(dest) and not os.path.islink(dest)
438 438 if not destdirexists:
439 439 if len(pats) > 1 or matchmod.patkind(pats[0]):
440 440 raise util.Abort(_('with multiple sources, destination must be an '
441 441 'existing directory'))
442 442 if util.endswithsep(dest):
443 443 raise util.Abort(_('destination %s is not a directory') % dest)
444 444
445 445 tfn = targetpathfn
446 446 if after:
447 447 tfn = targetpathafterfn
448 448 copylist = []
449 449 for pat in pats:
450 450 srcs = walkpat(pat)
451 451 if not srcs:
452 452 continue
453 453 copylist.append((tfn(pat, dest, srcs), srcs))
454 454 if not copylist:
455 455 raise util.Abort(_('no files to copy'))
456 456
457 457 errors = 0
458 458 for targetpath, srcs in copylist:
459 459 for abssrc, relsrc, exact in srcs:
460 460 if copyfile(abssrc, relsrc, targetpath(abssrc), exact):
461 461 errors += 1
462 462
463 463 if errors:
464 464 ui.warn(_('(consider using --after)\n'))
465 465
466 466 return errors != 0
467 467
468 468 def service(opts, parentfn=None, initfn=None, runfn=None, logfile=None,
469 469 runargs=None, appendpid=False):
470 470 '''Run a command as a service.'''
471 471
472 472 def writepid(pid):
473 473 if opts['pid_file']:
474 474 mode = appendpid and 'a' or 'w'
475 475 fp = open(opts['pid_file'], mode)
476 476 fp.write(str(pid) + '\n')
477 477 fp.close()
478 478
479 479 if opts['daemon'] and not opts['daemon_pipefds']:
480 480 # Signal child process startup with file removal
481 481 lockfd, lockpath = tempfile.mkstemp(prefix='hg-service-')
482 482 os.close(lockfd)
483 483 try:
484 484 if not runargs:
485 485 runargs = util.hgcmd() + sys.argv[1:]
486 486 runargs.append('--daemon-pipefds=%s' % lockpath)
487 487 # Don't pass --cwd to the child process, because we've already
488 488 # changed directory.
489 489 for i in xrange(1, len(runargs)):
490 490 if runargs[i].startswith('--cwd='):
491 491 del runargs[i]
492 492 break
493 493 elif runargs[i].startswith('--cwd'):
494 494 del runargs[i:i + 2]
495 495 break
496 496 def condfn():
497 497 return not os.path.exists(lockpath)
498 498 pid = util.rundetached(runargs, condfn)
499 499 if pid < 0:
500 500 raise util.Abort(_('child process failed to start'))
501 501 writepid(pid)
502 502 finally:
503 503 try:
504 504 os.unlink(lockpath)
505 505 except OSError, e:
506 506 if e.errno != errno.ENOENT:
507 507 raise
508 508 if parentfn:
509 509 return parentfn(pid)
510 510 else:
511 511 return
512 512
513 513 if initfn:
514 514 initfn()
515 515
516 516 if not opts['daemon']:
517 517 writepid(os.getpid())
518 518
519 519 if opts['daemon_pipefds']:
520 520 lockpath = opts['daemon_pipefds']
521 521 try:
522 522 os.setsid()
523 523 except AttributeError:
524 524 pass
525 525 os.unlink(lockpath)
526 526 util.hidewindow()
527 527 sys.stdout.flush()
528 528 sys.stderr.flush()
529 529
530 530 nullfd = os.open(os.devnull, os.O_RDWR)
531 531 logfilefd = nullfd
532 532 if logfile:
533 533 logfilefd = os.open(logfile, os.O_RDWR | os.O_CREAT | os.O_APPEND)
534 534 os.dup2(nullfd, 0)
535 535 os.dup2(logfilefd, 1)
536 536 os.dup2(logfilefd, 2)
537 537 if nullfd not in (0, 1, 2):
538 538 os.close(nullfd)
539 539 if logfile and logfilefd not in (0, 1, 2):
540 540 os.close(logfilefd)
541 541
542 542 if runfn:
543 543 return runfn()
544 544
545 545 def tryimportone(ui, repo, hunk, parents, opts, msgs, updatefunc):
546 546 """Utility function used by commands.import to import a single patch
547 547
548 548 This function is explicitly defined here to help the evolve extension to
549 549 wrap this part of the import logic.
550 550
551 551 The API is currently a bit ugly because it a simple code translation from
552 552 the import command. Feel free to make it better.
553 553
554 554 :hunk: a patch (as a binary string)
555 555 :parents: nodes that will be parent of the created commit
556 556 :opts: the full dict of option passed to the import command
557 557 :msgs: list to save commit message to.
558 558 (used in case we need to save it when failing)
559 559 :updatefunc: a function that update a repo to a given node
560 560 updatefunc(<repo>, <node>)
561 561 """
562 562 tmpname, message, user, date, branch, nodeid, p1, p2 = \
563 563 patch.extract(ui, hunk)
564 564
565 565 editor = commiteditor
566 566 if opts.get('edit'):
567 567 editor = commitforceeditor
568 568 update = not opts.get('bypass')
569 569 strip = opts["strip"]
570 570 sim = float(opts.get('similarity') or 0)
571 571 if not tmpname:
572 572 return (None, None)
573 573 msg = _('applied to working directory')
574 574
575 575 try:
576 576 cmdline_message = logmessage(ui, opts)
577 577 if cmdline_message:
578 578 # pickup the cmdline msg
579 579 message = cmdline_message
580 580 elif message:
581 581 # pickup the patch msg
582 582 message = message.strip()
583 583 else:
584 584 # launch the editor
585 585 message = None
586 586 ui.debug('message:\n%s\n' % message)
587 587
588 588 if len(parents) == 1:
589 589 parents.append(repo[nullid])
590 590 if opts.get('exact'):
591 591 if not nodeid or not p1:
592 592 raise util.Abort(_('not a Mercurial patch'))
593 593 p1 = repo[p1]
594 594 p2 = repo[p2 or nullid]
595 595 elif p2:
596 596 try:
597 597 p1 = repo[p1]
598 598 p2 = repo[p2]
599 599 # Without any options, consider p2 only if the
600 600 # patch is being applied on top of the recorded
601 601 # first parent.
602 602 if p1 != parents[0]:
603 603 p1 = parents[0]
604 604 p2 = repo[nullid]
605 605 except error.RepoError:
606 606 p1, p2 = parents
607 607 else:
608 608 p1, p2 = parents
609 609
610 610 n = None
611 611 if update:
612 612 if p1 != parents[0]:
613 613 updatefunc(repo, p1.node())
614 614 if p2 != parents[1]:
615 615 repo.setparents(p1.node(), p2.node())
616 616
617 617 if opts.get('exact') or opts.get('import_branch'):
618 618 repo.dirstate.setbranch(branch or 'default')
619 619
620 620 files = set()
621 621 patch.patch(ui, repo, tmpname, strip=strip, files=files,
622 622 eolmode=None, similarity=sim / 100.0)
623 623 files = list(files)
624 624 if opts.get('no_commit'):
625 625 if message:
626 626 msgs.append(message)
627 627 else:
628 628 if opts.get('exact') or p2:
629 629 # If you got here, you either use --force and know what
630 630 # you are doing or used --exact or a merge patch while
631 631 # being updated to its first parent.
632 632 m = None
633 633 else:
634 634 m = scmutil.matchfiles(repo, files or [])
635 635 n = repo.commit(message, opts.get('user') or user,
636 636 opts.get('date') or date, match=m,
637 637 editor=editor)
638 638 else:
639 639 if opts.get('exact') or opts.get('import_branch'):
640 640 branch = branch or 'default'
641 641 else:
642 642 branch = p1.branch()
643 643 store = patch.filestore()
644 644 try:
645 645 files = set()
646 646 try:
647 647 patch.patchrepo(ui, repo, p1, store, tmpname, strip,
648 648 files, eolmode=None)
649 649 except patch.PatchError, e:
650 650 raise util.Abort(str(e))
651 651 memctx = context.makememctx(repo, (p1.node(), p2.node()),
652 652 message,
653 653 opts.get('user') or user,
654 654 opts.get('date') or date,
655 655 branch, files, store,
656 656 editor=commiteditor)
657 657 repo.savecommitmessage(memctx.description())
658 658 n = memctx.commit()
659 659 finally:
660 660 store.close()
661 661 if opts.get('exact') and hex(n) != nodeid:
662 662 raise util.Abort(_('patch is damaged or loses information'))
663 663 if n:
664 664 # i18n: refers to a short changeset id
665 665 msg = _('created %s') % short(n)
666 666 return (msg, n)
667 667 finally:
668 668 os.unlink(tmpname)
669 669
670 670 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
671 671 opts=None):
672 672 '''export changesets as hg patches.'''
673 673
674 674 total = len(revs)
675 675 revwidth = max([len(str(rev)) for rev in revs])
676 676 filemode = {}
677 677
678 678 def single(rev, seqno, fp):
679 679 ctx = repo[rev]
680 680 node = ctx.node()
681 681 parents = [p.node() for p in ctx.parents() if p]
682 682 branch = ctx.branch()
683 683 if switch_parent:
684 684 parents.reverse()
685 685 prev = (parents and parents[0]) or nullid
686 686
687 687 shouldclose = False
688 688 if not fp and len(template) > 0:
689 689 desc_lines = ctx.description().rstrip().split('\n')
690 690 desc = desc_lines[0] #Commit always has a first line.
691 691 fp = makefileobj(repo, template, node, desc=desc, total=total,
692 692 seqno=seqno, revwidth=revwidth, mode='wb',
693 693 modemap=filemode)
694 694 if fp != template:
695 695 shouldclose = True
696 696 if fp and fp != sys.stdout and util.safehasattr(fp, 'name'):
697 697 repo.ui.note("%s\n" % fp.name)
698 698
699 699 if not fp:
700 700 write = repo.ui.write
701 701 else:
702 702 def write(s, **kw):
703 703 fp.write(s)
704 704
705 705
706 706 write("# HG changeset patch\n")
707 707 write("# User %s\n" % ctx.user())
708 708 write("# Date %d %d\n" % ctx.date())
709 709 write("# %s\n" % util.datestr(ctx.date()))
710 710 if branch and branch != 'default':
711 711 write("# Branch %s\n" % branch)
712 712 write("# Node ID %s\n" % hex(node))
713 713 write("# Parent %s\n" % hex(prev))
714 714 if len(parents) > 1:
715 715 write("# Parent %s\n" % hex(parents[1]))
716 716 write(ctx.description().rstrip())
717 717 write("\n\n")
718 718
719 719 for chunk, label in patch.diffui(repo, prev, node, opts=opts):
720 720 write(chunk, label=label)
721 721
722 722 if shouldclose:
723 723 fp.close()
724 724
725 725 for seqno, rev in enumerate(revs):
726 726 single(rev, seqno + 1, fp)
727 727
728 728 def diffordiffstat(ui, repo, diffopts, node1, node2, match,
729 729 changes=None, stat=False, fp=None, prefix='',
730 730 listsubrepos=False):
731 731 '''show diff or diffstat.'''
732 732 if fp is None:
733 733 write = ui.write
734 734 else:
735 735 def write(s, **kw):
736 736 fp.write(s)
737 737
738 738 if stat:
739 739 diffopts = diffopts.copy(context=0)
740 740 width = 80
741 741 if not ui.plain():
742 742 width = ui.termwidth()
743 743 chunks = patch.diff(repo, node1, node2, match, changes, diffopts,
744 744 prefix=prefix)
745 745 for chunk, label in patch.diffstatui(util.iterlines(chunks),
746 746 width=width,
747 747 git=diffopts.git):
748 748 write(chunk, label=label)
749 749 else:
750 750 for chunk, label in patch.diffui(repo, node1, node2, match,
751 751 changes, diffopts, prefix=prefix):
752 752 write(chunk, label=label)
753 753
754 754 if listsubrepos:
755 755 ctx1 = repo[node1]
756 756 ctx2 = repo[node2]
757 757 for subpath, sub in scmutil.itersubrepos(ctx1, ctx2):
758 758 tempnode2 = node2
759 759 try:
760 760 if node2 is not None:
761 761 tempnode2 = ctx2.substate[subpath][1]
762 762 except KeyError:
763 763 # A subrepo that existed in node1 was deleted between node1 and
764 764 # node2 (inclusive). Thus, ctx2's substate won't contain that
765 765 # subpath. The best we can do is to ignore it.
766 766 tempnode2 = None
767 767 submatch = matchmod.narrowmatcher(subpath, match)
768 768 sub.diff(ui, diffopts, tempnode2, submatch, changes=changes,
769 769 stat=stat, fp=fp, prefix=prefix)
770 770
771 771 class changeset_printer(object):
772 772 '''show changeset information when templating not requested.'''
773 773
774 774 def __init__(self, ui, repo, patch, diffopts, buffered):
775 775 self.ui = ui
776 776 self.repo = repo
777 777 self.buffered = buffered
778 778 self.patch = patch
779 779 self.diffopts = diffopts
780 780 self.header = {}
781 781 self.hunk = {}
782 782 self.lastheader = None
783 783 self.footer = None
784 784
785 785 def flush(self, rev):
786 786 if rev in self.header:
787 787 h = self.header[rev]
788 788 if h != self.lastheader:
789 789 self.lastheader = h
790 790 self.ui.write(h)
791 791 del self.header[rev]
792 792 if rev in self.hunk:
793 793 self.ui.write(self.hunk[rev])
794 794 del self.hunk[rev]
795 795 return 1
796 796 return 0
797 797
798 798 def close(self):
799 799 if self.footer:
800 800 self.ui.write(self.footer)
801 801
802 802 def show(self, ctx, copies=None, matchfn=None, **props):
803 803 if self.buffered:
804 804 self.ui.pushbuffer()
805 805 self._show(ctx, copies, matchfn, props)
806 806 self.hunk[ctx.rev()] = self.ui.popbuffer(labeled=True)
807 807 else:
808 808 self._show(ctx, copies, matchfn, props)
809 809
810 810 def _show(self, ctx, copies, matchfn, props):
811 811 '''show a single changeset or file revision'''
812 812 changenode = ctx.node()
813 813 rev = ctx.rev()
814 814
815 815 if self.ui.quiet:
816 816 self.ui.write("%d:%s\n" % (rev, short(changenode)),
817 817 label='log.node')
818 818 return
819 819
820 820 log = self.repo.changelog
821 821 date = util.datestr(ctx.date())
822 822
823 823 hexfunc = self.ui.debugflag and hex or short
824 824
825 825 parents = [(p, hexfunc(log.node(p)))
826 826 for p in self._meaningful_parentrevs(log, rev)]
827 827
828 828 # i18n: column positioning for "hg log"
829 829 self.ui.write(_("changeset: %d:%s\n") % (rev, hexfunc(changenode)),
830 830 label='log.changeset changeset.%s' % ctx.phasestr())
831 831
832 832 branch = ctx.branch()
833 833 # don't show the default branch name
834 834 if branch != 'default':
835 835 # i18n: column positioning for "hg log"
836 836 self.ui.write(_("branch: %s\n") % branch,
837 837 label='log.branch')
838 838 for bookmark in self.repo.nodebookmarks(changenode):
839 839 # i18n: column positioning for "hg log"
840 840 self.ui.write(_("bookmark: %s\n") % bookmark,
841 841 label='log.bookmark')
842 842 for tag in self.repo.nodetags(changenode):
843 843 # i18n: column positioning for "hg log"
844 844 self.ui.write(_("tag: %s\n") % tag,
845 845 label='log.tag')
846 846 if self.ui.debugflag and ctx.phase():
847 847 # i18n: column positioning for "hg log"
848 848 self.ui.write(_("phase: %s\n") % _(ctx.phasestr()),
849 849 label='log.phase')
850 850 for parent in parents:
851 851 # i18n: column positioning for "hg log"
852 852 self.ui.write(_("parent: %d:%s\n") % parent,
853 853 label='log.parent changeset.%s' % ctx.phasestr())
854 854
855 855 if self.ui.debugflag:
856 856 mnode = ctx.manifestnode()
857 857 # i18n: column positioning for "hg log"
858 858 self.ui.write(_("manifest: %d:%s\n") %
859 859 (self.repo.manifest.rev(mnode), hex(mnode)),
860 860 label='ui.debug log.manifest')
861 861 # i18n: column positioning for "hg log"
862 862 self.ui.write(_("user: %s\n") % ctx.user(),
863 863 label='log.user')
864 864 # i18n: column positioning for "hg log"
865 865 self.ui.write(_("date: %s\n") % date,
866 866 label='log.date')
867 867
868 868 if self.ui.debugflag:
869 869 files = self.repo.status(log.parents(changenode)[0], changenode)[:3]
870 870 for key, value in zip([# i18n: column positioning for "hg log"
871 871 _("files:"),
872 872 # i18n: column positioning for "hg log"
873 873 _("files+:"),
874 874 # i18n: column positioning for "hg log"
875 875 _("files-:")], files):
876 876 if value:
877 877 self.ui.write("%-12s %s\n" % (key, " ".join(value)),
878 878 label='ui.debug log.files')
879 879 elif ctx.files() and self.ui.verbose:
880 880 # i18n: column positioning for "hg log"
881 881 self.ui.write(_("files: %s\n") % " ".join(ctx.files()),
882 882 label='ui.note log.files')
883 883 if copies and self.ui.verbose:
884 884 copies = ['%s (%s)' % c for c in copies]
885 885 # i18n: column positioning for "hg log"
886 886 self.ui.write(_("copies: %s\n") % ' '.join(copies),
887 887 label='ui.note log.copies')
888 888
889 889 extra = ctx.extra()
890 890 if extra and self.ui.debugflag:
891 891 for key, value in sorted(extra.items()):
892 892 # i18n: column positioning for "hg log"
893 893 self.ui.write(_("extra: %s=%s\n")
894 894 % (key, value.encode('string_escape')),
895 895 label='ui.debug log.extra')
896 896
897 897 description = ctx.description().strip()
898 898 if description:
899 899 if self.ui.verbose:
900 900 self.ui.write(_("description:\n"),
901 901 label='ui.note log.description')
902 902 self.ui.write(description,
903 903 label='ui.note log.description')
904 904 self.ui.write("\n\n")
905 905 else:
906 906 # i18n: column positioning for "hg log"
907 907 self.ui.write(_("summary: %s\n") %
908 908 description.splitlines()[0],
909 909 label='log.summary')
910 910 self.ui.write("\n")
911 911
912 912 self.showpatch(changenode, matchfn)
913 913
914 914 def showpatch(self, node, matchfn):
915 915 if not matchfn:
916 916 matchfn = self.patch
917 917 if matchfn:
918 918 stat = self.diffopts.get('stat')
919 919 diff = self.diffopts.get('patch')
920 920 diffopts = patch.diffopts(self.ui, self.diffopts)
921 921 prev = self.repo.changelog.parents(node)[0]
922 922 if stat:
923 923 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
924 924 match=matchfn, stat=True)
925 925 if diff:
926 926 if stat:
927 927 self.ui.write("\n")
928 928 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
929 929 match=matchfn, stat=False)
930 930 self.ui.write("\n")
931 931
932 932 def _meaningful_parentrevs(self, log, rev):
933 933 """Return list of meaningful (or all if debug) parentrevs for rev.
934 934
935 935 For merges (two non-nullrev revisions) both parents are meaningful.
936 936 Otherwise the first parent revision is considered meaningful if it
937 937 is not the preceding revision.
938 938 """
939 939 parents = log.parentrevs(rev)
940 940 if not self.ui.debugflag and parents[1] == nullrev:
941 941 if parents[0] >= rev - 1:
942 942 parents = []
943 943 else:
944 944 parents = [parents[0]]
945 945 return parents
946 946
947 947
948 948 class changeset_templater(changeset_printer):
949 949 '''format changeset information.'''
950 950
951 951 def __init__(self, ui, repo, patch, diffopts, tmpl, mapfile, buffered):
952 952 changeset_printer.__init__(self, ui, repo, patch, diffopts, buffered)
953 953 formatnode = ui.debugflag and (lambda x: x) or (lambda x: x[:12])
954 954 defaulttempl = {
955 955 'parent': '{rev}:{node|formatnode} ',
956 956 'manifest': '{rev}:{node|formatnode}',
957 957 'file_copy': '{name} ({source})',
958 958 'extra': '{key}={value|stringescape}'
959 959 }
960 960 # filecopy is preserved for compatibility reasons
961 961 defaulttempl['filecopy'] = defaulttempl['file_copy']
962 962 self.t = templater.templater(mapfile, {'formatnode': formatnode},
963 963 cache=defaulttempl)
964 964 if tmpl:
965 965 self.t.cache['changeset'] = tmpl
966 966
967 967 self.cache = {}
968 968
969 969 def _meaningful_parentrevs(self, ctx):
970 970 """Return list of meaningful (or all if debug) parentrevs for rev.
971 971 """
972 972 parents = ctx.parents()
973 973 if len(parents) > 1:
974 974 return parents
975 975 if self.ui.debugflag:
976 976 return [parents[0], self.repo['null']]
977 977 if parents[0].rev() >= ctx.rev() - 1:
978 978 return []
979 979 return parents
980 980
981 981 def _show(self, ctx, copies, matchfn, props):
982 982 '''show a single changeset or file revision'''
983 983
984 984 showlist = templatekw.showlist
985 985
986 986 # showparents() behaviour depends on ui trace level which
987 987 # causes unexpected behaviours at templating level and makes
988 988 # it harder to extract it in a standalone function. Its
989 989 # behaviour cannot be changed so leave it here for now.
990 990 def showparents(**args):
991 991 ctx = args['ctx']
992 992 parents = [[('rev', p.rev()), ('node', p.hex())]
993 993 for p in self._meaningful_parentrevs(ctx)]
994 994 return showlist('parent', parents, **args)
995 995
996 996 props = props.copy()
997 997 props.update(templatekw.keywords)
998 998 props['parents'] = showparents
999 999 props['templ'] = self.t
1000 1000 props['ctx'] = ctx
1001 1001 props['repo'] = self.repo
1002 1002 props['revcache'] = {'copies': copies}
1003 1003 props['cache'] = self.cache
1004 1004
1005 1005 # find correct templates for current mode
1006 1006
1007 1007 tmplmodes = [
1008 1008 (True, None),
1009 1009 (self.ui.verbose, 'verbose'),
1010 1010 (self.ui.quiet, 'quiet'),
1011 1011 (self.ui.debugflag, 'debug'),
1012 1012 ]
1013 1013
1014 1014 types = {'header': '', 'footer':'', 'changeset': 'changeset'}
1015 1015 for mode, postfix in tmplmodes:
1016 1016 for type in types:
1017 1017 cur = postfix and ('%s_%s' % (type, postfix)) or type
1018 1018 if mode and cur in self.t:
1019 1019 types[type] = cur
1020 1020
1021 1021 try:
1022 1022
1023 1023 # write header
1024 1024 if types['header']:
1025 1025 h = templater.stringify(self.t(types['header'], **props))
1026 1026 if self.buffered:
1027 1027 self.header[ctx.rev()] = h
1028 1028 else:
1029 1029 if self.lastheader != h:
1030 1030 self.lastheader = h
1031 1031 self.ui.write(h)
1032 1032
1033 1033 # write changeset metadata, then patch if requested
1034 1034 key = types['changeset']
1035 1035 self.ui.write(templater.stringify(self.t(key, **props)))
1036 1036 self.showpatch(ctx.node(), matchfn)
1037 1037
1038 1038 if types['footer']:
1039 1039 if not self.footer:
1040 1040 self.footer = templater.stringify(self.t(types['footer'],
1041 1041 **props))
1042 1042
1043 1043 except KeyError, inst:
1044 1044 msg = _("%s: no key named '%s'")
1045 1045 raise util.Abort(msg % (self.t.mapfile, inst.args[0]))
1046 1046 except SyntaxError, inst:
1047 1047 raise util.Abort('%s: %s' % (self.t.mapfile, inst.args[0]))
1048 1048
1049 1049 def gettemplate(ui, tmpl, style):
1050 1050 """
1051 1051 Find the template matching the given template spec or style.
1052 1052 """
1053 1053
1054 1054 # ui settings
1055 1055 if not tmpl and not style:
1056 1056 tmpl = ui.config('ui', 'logtemplate')
1057 1057 if tmpl:
1058 1058 try:
1059 1059 tmpl = templater.parsestring(tmpl)
1060 1060 except SyntaxError:
1061 1061 tmpl = templater.parsestring(tmpl, quoted=False)
1062 return tmpl, None
1062 1063 else:
1063 1064 style = util.expandpath(ui.config('ui', 'style', ''))
1064 1065
1065 1066 if style:
1066 1067 mapfile = style
1067 1068 if not os.path.split(mapfile)[0]:
1068 1069 mapname = (templater.templatepath('map-cmdline.' + mapfile)
1069 1070 or templater.templatepath(mapfile))
1070 1071 if mapname:
1071 1072 mapfile = mapname
1072 1073 return None, mapfile
1073 1074
1075 if not tmpl:
1076 return None, None
1077
1078 # looks like a literal template?
1079 if '{' in tmpl:
1080 return tmpl, None
1081
1082 # perhaps a stock style?
1083 if not os.path.split(tmpl)[0]:
1084 mapname = (templater.templatepath('map-cmdline.' + tmpl)
1085 or templater.templatepath(tmpl))
1086 if mapname and os.path.isfile(mapname):
1087 return None, mapname
1088
1089 # perhaps it's a reference to [templates]
1090 t = ui.config('templates', tmpl)
1091 if t:
1092 try:
1093 tmpl = templater.parsestring(t)
1094 except SyntaxError:
1095 tmpl = templater.parsestring(t, quoted=False)
1096 return tmpl, None
1097
1098 # perhaps it's a path to a map or a template
1099 if ('/' in tmpl or '\\' in tmpl) and os.path.isfile(tmpl):
1100 # is it a mapfile for a style?
1101 if os.path.basename(tmpl).startswith("map-"):
1102 return None, os.path.realpath(tmpl)
1103 tmpl = open(tmpl).read()
1104 return tmpl, None
1105
1106 # constant string?
1074 1107 return tmpl, None
1075 1108
1076 1109 def show_changeset(ui, repo, opts, buffered=False):
1077 1110 """show one changeset using template or regular display.
1078 1111
1079 1112 Display format will be the first non-empty hit of:
1080 1113 1. option 'template'
1081 1114 2. option 'style'
1082 1115 3. [ui] setting 'logtemplate'
1083 1116 4. [ui] setting 'style'
1084 1117 If all of these values are either the unset or the empty string,
1085 1118 regular display via changeset_printer() is done.
1086 1119 """
1087 1120 # options
1088 1121 patch = None
1089 1122 if opts.get('patch') or opts.get('stat'):
1090 1123 patch = scmutil.matchall(repo)
1091 1124
1092 1125 tmpl, mapfile = gettemplate(ui, opts.get('template'), opts.get('style'))
1093 1126
1094 1127 if not tmpl and not mapfile:
1095 1128 return changeset_printer(ui, repo, patch, opts, buffered)
1096 1129
1097 1130 try:
1098 1131 t = changeset_templater(ui, repo, patch, opts, tmpl, mapfile, buffered)
1099 1132 except SyntaxError, inst:
1100 1133 raise util.Abort(inst.args[0])
1101 1134 return t
1102 1135
1103 1136 def showmarker(ui, marker):
1104 1137 """utility function to display obsolescence marker in a readable way
1105 1138
1106 1139 To be used by debug function."""
1107 1140 ui.write(hex(marker.precnode()))
1108 1141 for repl in marker.succnodes():
1109 1142 ui.write(' ')
1110 1143 ui.write(hex(repl))
1111 1144 ui.write(' %X ' % marker._data[2])
1112 1145 ui.write('{%s}' % (', '.join('%r: %r' % t for t in
1113 1146 sorted(marker.metadata().items()))))
1114 1147 ui.write('\n')
1115 1148
1116 1149 def finddate(ui, repo, date):
1117 1150 """Find the tipmost changeset that matches the given date spec"""
1118 1151
1119 1152 df = util.matchdate(date)
1120 1153 m = scmutil.matchall(repo)
1121 1154 results = {}
1122 1155
1123 1156 def prep(ctx, fns):
1124 1157 d = ctx.date()
1125 1158 if df(d[0]):
1126 1159 results[ctx.rev()] = d
1127 1160
1128 1161 for ctx in walkchangerevs(repo, m, {'rev': None}, prep):
1129 1162 rev = ctx.rev()
1130 1163 if rev in results:
1131 1164 ui.status(_("found revision %s from %s\n") %
1132 1165 (rev, util.datestr(results[rev])))
1133 1166 return str(rev)
1134 1167
1135 1168 raise util.Abort(_("revision matching date not found"))
1136 1169
1137 1170 def increasingwindows(windowsize=8, sizelimit=512):
1138 1171 while True:
1139 1172 yield windowsize
1140 1173 if windowsize < sizelimit:
1141 1174 windowsize *= 2
1142 1175
1143 1176 class FileWalkError(Exception):
1144 1177 pass
1145 1178
1146 1179 def walkfilerevs(repo, match, follow, revs, fncache):
1147 1180 '''Walks the file history for the matched files.
1148 1181
1149 1182 Returns the changeset revs that are involved in the file history.
1150 1183
1151 1184 Throws FileWalkError if the file history can't be walked using
1152 1185 filelogs alone.
1153 1186 '''
1154 1187 wanted = set()
1155 1188 copies = []
1156 1189 minrev, maxrev = min(revs), max(revs)
1157 1190 def filerevgen(filelog, last):
1158 1191 """
1159 1192 Only files, no patterns. Check the history of each file.
1160 1193
1161 1194 Examines filelog entries within minrev, maxrev linkrev range
1162 1195 Returns an iterator yielding (linkrev, parentlinkrevs, copied)
1163 1196 tuples in backwards order
1164 1197 """
1165 1198 cl_count = len(repo)
1166 1199 revs = []
1167 1200 for j in xrange(0, last + 1):
1168 1201 linkrev = filelog.linkrev(j)
1169 1202 if linkrev < minrev:
1170 1203 continue
1171 1204 # only yield rev for which we have the changelog, it can
1172 1205 # happen while doing "hg log" during a pull or commit
1173 1206 if linkrev >= cl_count:
1174 1207 break
1175 1208
1176 1209 parentlinkrevs = []
1177 1210 for p in filelog.parentrevs(j):
1178 1211 if p != nullrev:
1179 1212 parentlinkrevs.append(filelog.linkrev(p))
1180 1213 n = filelog.node(j)
1181 1214 revs.append((linkrev, parentlinkrevs,
1182 1215 follow and filelog.renamed(n)))
1183 1216
1184 1217 return reversed(revs)
1185 1218 def iterfiles():
1186 1219 pctx = repo['.']
1187 1220 for filename in match.files():
1188 1221 if follow:
1189 1222 if filename not in pctx:
1190 1223 raise util.Abort(_('cannot follow file not in parent '
1191 1224 'revision: "%s"') % filename)
1192 1225 yield filename, pctx[filename].filenode()
1193 1226 else:
1194 1227 yield filename, None
1195 1228 for filename_node in copies:
1196 1229 yield filename_node
1197 1230
1198 1231 for file_, node in iterfiles():
1199 1232 filelog = repo.file(file_)
1200 1233 if not len(filelog):
1201 1234 if node is None:
1202 1235 # A zero count may be a directory or deleted file, so
1203 1236 # try to find matching entries on the slow path.
1204 1237 if follow:
1205 1238 raise util.Abort(
1206 1239 _('cannot follow nonexistent file: "%s"') % file_)
1207 1240 raise FileWalkError("Cannot walk via filelog")
1208 1241 else:
1209 1242 continue
1210 1243
1211 1244 if node is None:
1212 1245 last = len(filelog) - 1
1213 1246 else:
1214 1247 last = filelog.rev(node)
1215 1248
1216 1249
1217 1250 # keep track of all ancestors of the file
1218 1251 ancestors = set([filelog.linkrev(last)])
1219 1252
1220 1253 # iterate from latest to oldest revision
1221 1254 for rev, flparentlinkrevs, copied in filerevgen(filelog, last):
1222 1255 if not follow:
1223 1256 if rev > maxrev:
1224 1257 continue
1225 1258 else:
1226 1259 # Note that last might not be the first interesting
1227 1260 # rev to us:
1228 1261 # if the file has been changed after maxrev, we'll
1229 1262 # have linkrev(last) > maxrev, and we still need
1230 1263 # to explore the file graph
1231 1264 if rev not in ancestors:
1232 1265 continue
1233 1266 # XXX insert 1327 fix here
1234 1267 if flparentlinkrevs:
1235 1268 ancestors.update(flparentlinkrevs)
1236 1269
1237 1270 fncache.setdefault(rev, []).append(file_)
1238 1271 wanted.add(rev)
1239 1272 if copied:
1240 1273 copies.append(copied)
1241 1274
1242 1275 return wanted
1243 1276
1244 1277 def walkchangerevs(repo, match, opts, prepare):
1245 1278 '''Iterate over files and the revs in which they changed.
1246 1279
1247 1280 Callers most commonly need to iterate backwards over the history
1248 1281 in which they are interested. Doing so has awful (quadratic-looking)
1249 1282 performance, so we use iterators in a "windowed" way.
1250 1283
1251 1284 We walk a window of revisions in the desired order. Within the
1252 1285 window, we first walk forwards to gather data, then in the desired
1253 1286 order (usually backwards) to display it.
1254 1287
1255 1288 This function returns an iterator yielding contexts. Before
1256 1289 yielding each context, the iterator will first call the prepare
1257 1290 function on each context in the window in forward order.'''
1258 1291
1259 1292 follow = opts.get('follow') or opts.get('follow_first')
1260 1293
1261 1294 if opts.get('rev'):
1262 1295 revs = scmutil.revrange(repo, opts.get('rev'))
1263 1296 elif follow:
1264 1297 revs = repo.revs('reverse(:.)')
1265 1298 else:
1266 1299 revs = revset.baseset(repo)
1267 1300 revs.reverse()
1268 1301 if not revs:
1269 1302 return []
1270 1303 wanted = set()
1271 1304 slowpath = match.anypats() or (match.files() and opts.get('removed'))
1272 1305 fncache = {}
1273 1306 change = repo.changectx
1274 1307
1275 1308 # First step is to fill wanted, the set of revisions that we want to yield.
1276 1309 # When it does not induce extra cost, we also fill fncache for revisions in
1277 1310 # wanted: a cache of filenames that were changed (ctx.files()) and that
1278 1311 # match the file filtering conditions.
1279 1312
1280 1313 if not slowpath and not match.files():
1281 1314 # No files, no patterns. Display all revs.
1282 1315 wanted = revs
1283 1316
1284 1317 if not slowpath and match.files():
1285 1318 # We only have to read through the filelog to find wanted revisions
1286 1319
1287 1320 try:
1288 1321 wanted = walkfilerevs(repo, match, follow, revs, fncache)
1289 1322 except FileWalkError:
1290 1323 slowpath = True
1291 1324
1292 1325 # We decided to fall back to the slowpath because at least one
1293 1326 # of the paths was not a file. Check to see if at least one of them
1294 1327 # existed in history, otherwise simply return
1295 1328 for path in match.files():
1296 1329 if path == '.' or path in repo.store:
1297 1330 break
1298 1331 else:
1299 1332 return []
1300 1333
1301 1334 if slowpath:
1302 1335 # We have to read the changelog to match filenames against
1303 1336 # changed files
1304 1337
1305 1338 if follow:
1306 1339 raise util.Abort(_('can only follow copies/renames for explicit '
1307 1340 'filenames'))
1308 1341
1309 1342 # The slow path checks files modified in every changeset.
1310 1343 # This is really slow on large repos, so compute the set lazily.
1311 1344 class lazywantedset(object):
1312 1345 def __init__(self):
1313 1346 self.set = set()
1314 1347 self.revs = set(revs)
1315 1348
1316 1349 # No need to worry about locality here because it will be accessed
1317 1350 # in the same order as the increasing window below.
1318 1351 def __contains__(self, value):
1319 1352 if value in self.set:
1320 1353 return True
1321 1354 elif not value in self.revs:
1322 1355 return False
1323 1356 else:
1324 1357 self.revs.discard(value)
1325 1358 ctx = change(value)
1326 1359 matches = filter(match, ctx.files())
1327 1360 if matches:
1328 1361 fncache[value] = matches
1329 1362 self.set.add(value)
1330 1363 return True
1331 1364 return False
1332 1365
1333 1366 def discard(self, value):
1334 1367 self.revs.discard(value)
1335 1368 self.set.discard(value)
1336 1369
1337 1370 wanted = lazywantedset()
1338 1371
1339 1372 class followfilter(object):
1340 1373 def __init__(self, onlyfirst=False):
1341 1374 self.startrev = nullrev
1342 1375 self.roots = set()
1343 1376 self.onlyfirst = onlyfirst
1344 1377
1345 1378 def match(self, rev):
1346 1379 def realparents(rev):
1347 1380 if self.onlyfirst:
1348 1381 return repo.changelog.parentrevs(rev)[0:1]
1349 1382 else:
1350 1383 return filter(lambda x: x != nullrev,
1351 1384 repo.changelog.parentrevs(rev))
1352 1385
1353 1386 if self.startrev == nullrev:
1354 1387 self.startrev = rev
1355 1388 return True
1356 1389
1357 1390 if rev > self.startrev:
1358 1391 # forward: all descendants
1359 1392 if not self.roots:
1360 1393 self.roots.add(self.startrev)
1361 1394 for parent in realparents(rev):
1362 1395 if parent in self.roots:
1363 1396 self.roots.add(rev)
1364 1397 return True
1365 1398 else:
1366 1399 # backwards: all parents
1367 1400 if not self.roots:
1368 1401 self.roots.update(realparents(self.startrev))
1369 1402 if rev in self.roots:
1370 1403 self.roots.remove(rev)
1371 1404 self.roots.update(realparents(rev))
1372 1405 return True
1373 1406
1374 1407 return False
1375 1408
1376 1409 # it might be worthwhile to do this in the iterator if the rev range
1377 1410 # is descending and the prune args are all within that range
1378 1411 for rev in opts.get('prune', ()):
1379 1412 rev = repo[rev].rev()
1380 1413 ff = followfilter()
1381 1414 stop = min(revs[0], revs[-1])
1382 1415 for x in xrange(rev, stop - 1, -1):
1383 1416 if ff.match(x):
1384 1417 wanted = wanted - [x]
1385 1418
1386 1419 # Now that wanted is correctly initialized, we can iterate over the
1387 1420 # revision range, yielding only revisions in wanted.
1388 1421 def iterate():
1389 1422 if follow and not match.files():
1390 1423 ff = followfilter(onlyfirst=opts.get('follow_first'))
1391 1424 def want(rev):
1392 1425 return ff.match(rev) and rev in wanted
1393 1426 else:
1394 1427 def want(rev):
1395 1428 return rev in wanted
1396 1429
1397 1430 it = iter(revs)
1398 1431 stopiteration = False
1399 1432 for windowsize in increasingwindows():
1400 1433 nrevs = []
1401 1434 for i in xrange(windowsize):
1402 1435 try:
1403 1436 rev = it.next()
1404 1437 if want(rev):
1405 1438 nrevs.append(rev)
1406 1439 except (StopIteration):
1407 1440 stopiteration = True
1408 1441 break
1409 1442 for rev in sorted(nrevs):
1410 1443 fns = fncache.get(rev)
1411 1444 ctx = change(rev)
1412 1445 if not fns:
1413 1446 def fns_generator():
1414 1447 for f in ctx.files():
1415 1448 if match(f):
1416 1449 yield f
1417 1450 fns = fns_generator()
1418 1451 prepare(ctx, fns)
1419 1452 for rev in nrevs:
1420 1453 yield change(rev)
1421 1454
1422 1455 if stopiteration:
1423 1456 break
1424 1457
1425 1458 return iterate()
1426 1459
1427 1460 def _makegraphfilematcher(repo, pats, followfirst):
1428 1461 # When displaying a revision with --patch --follow FILE, we have
1429 1462 # to know which file of the revision must be diffed. With
1430 1463 # --follow, we want the names of the ancestors of FILE in the
1431 1464 # revision, stored in "fcache". "fcache" is populated by
1432 1465 # reproducing the graph traversal already done by --follow revset
1433 1466 # and relating linkrevs to file names (which is not "correct" but
1434 1467 # good enough).
1435 1468 fcache = {}
1436 1469 fcacheready = [False]
1437 1470 pctx = repo['.']
1438 1471 wctx = repo[None]
1439 1472
1440 1473 def populate():
1441 1474 for fn in pats:
1442 1475 for i in ((pctx[fn],), pctx[fn].ancestors(followfirst=followfirst)):
1443 1476 for c in i:
1444 1477 fcache.setdefault(c.linkrev(), set()).add(c.path())
1445 1478
1446 1479 def filematcher(rev):
1447 1480 if not fcacheready[0]:
1448 1481 # Lazy initialization
1449 1482 fcacheready[0] = True
1450 1483 populate()
1451 1484 return scmutil.match(wctx, fcache.get(rev, []), default='path')
1452 1485
1453 1486 return filematcher
1454 1487
1455 1488 def _makegraphlogrevset(repo, pats, opts, revs):
1456 1489 """Return (expr, filematcher) where expr is a revset string built
1457 1490 from log options and file patterns or None. If --stat or --patch
1458 1491 are not passed filematcher is None. Otherwise it is a callable
1459 1492 taking a revision number and returning a match objects filtering
1460 1493 the files to be detailed when displaying the revision.
1461 1494 """
1462 1495 opt2revset = {
1463 1496 'no_merges': ('not merge()', None),
1464 1497 'only_merges': ('merge()', None),
1465 1498 '_ancestors': ('ancestors(%(val)s)', None),
1466 1499 '_fancestors': ('_firstancestors(%(val)s)', None),
1467 1500 '_descendants': ('descendants(%(val)s)', None),
1468 1501 '_fdescendants': ('_firstdescendants(%(val)s)', None),
1469 1502 '_matchfiles': ('_matchfiles(%(val)s)', None),
1470 1503 'date': ('date(%(val)r)', None),
1471 1504 'branch': ('branch(%(val)r)', ' or '),
1472 1505 '_patslog': ('filelog(%(val)r)', ' or '),
1473 1506 '_patsfollow': ('follow(%(val)r)', ' or '),
1474 1507 '_patsfollowfirst': ('_followfirst(%(val)r)', ' or '),
1475 1508 'keyword': ('keyword(%(val)r)', ' or '),
1476 1509 'prune': ('not (%(val)r or ancestors(%(val)r))', ' and '),
1477 1510 'user': ('user(%(val)r)', ' or '),
1478 1511 }
1479 1512
1480 1513 opts = dict(opts)
1481 1514 # follow or not follow?
1482 1515 follow = opts.get('follow') or opts.get('follow_first')
1483 1516 followfirst = opts.get('follow_first') and 1 or 0
1484 1517 # --follow with FILE behaviour depends on revs...
1485 1518 startrev = revs[0]
1486 1519 followdescendants = (len(revs) > 1 and revs[0] < revs[1]) and 1 or 0
1487 1520
1488 1521 # branch and only_branch are really aliases and must be handled at
1489 1522 # the same time
1490 1523 opts['branch'] = opts.get('branch', []) + opts.get('only_branch', [])
1491 1524 opts['branch'] = [repo.lookupbranch(b) for b in opts['branch']]
1492 1525 # pats/include/exclude are passed to match.match() directly in
1493 1526 # _matchfiles() revset but walkchangerevs() builds its matcher with
1494 1527 # scmutil.match(). The difference is input pats are globbed on
1495 1528 # platforms without shell expansion (windows).
1496 1529 pctx = repo[None]
1497 1530 match, pats = scmutil.matchandpats(pctx, pats, opts)
1498 1531 slowpath = match.anypats() or (match.files() and opts.get('removed'))
1499 1532 if not slowpath:
1500 1533 for f in match.files():
1501 1534 if follow and f not in pctx:
1502 1535 raise util.Abort(_('cannot follow file not in parent '
1503 1536 'revision: "%s"') % f)
1504 1537 filelog = repo.file(f)
1505 1538 if not filelog:
1506 1539 # A zero count may be a directory or deleted file, so
1507 1540 # try to find matching entries on the slow path.
1508 1541 if follow:
1509 1542 raise util.Abort(
1510 1543 _('cannot follow nonexistent file: "%s"') % f)
1511 1544 slowpath = True
1512 1545
1513 1546 # We decided to fall back to the slowpath because at least one
1514 1547 # of the paths was not a file. Check to see if at least one of them
1515 1548 # existed in history - in that case, we'll continue down the
1516 1549 # slowpath; otherwise, we can turn off the slowpath
1517 1550 if slowpath:
1518 1551 for path in match.files():
1519 1552 if path == '.' or path in repo.store:
1520 1553 break
1521 1554 else:
1522 1555 slowpath = False
1523 1556
1524 1557 if slowpath:
1525 1558 # See walkchangerevs() slow path.
1526 1559 #
1527 1560 if follow:
1528 1561 raise util.Abort(_('can only follow copies/renames for explicit '
1529 1562 'filenames'))
1530 1563 # pats/include/exclude cannot be represented as separate
1531 1564 # revset expressions as their filtering logic applies at file
1532 1565 # level. For instance "-I a -X a" matches a revision touching
1533 1566 # "a" and "b" while "file(a) and not file(b)" does
1534 1567 # not. Besides, filesets are evaluated against the working
1535 1568 # directory.
1536 1569 matchargs = ['r:', 'd:relpath']
1537 1570 for p in pats:
1538 1571 matchargs.append('p:' + p)
1539 1572 for p in opts.get('include', []):
1540 1573 matchargs.append('i:' + p)
1541 1574 for p in opts.get('exclude', []):
1542 1575 matchargs.append('x:' + p)
1543 1576 matchargs = ','.join(('%r' % p) for p in matchargs)
1544 1577 opts['_matchfiles'] = matchargs
1545 1578 else:
1546 1579 if follow:
1547 1580 fpats = ('_patsfollow', '_patsfollowfirst')
1548 1581 fnopats = (('_ancestors', '_fancestors'),
1549 1582 ('_descendants', '_fdescendants'))
1550 1583 if pats:
1551 1584 # follow() revset interprets its file argument as a
1552 1585 # manifest entry, so use match.files(), not pats.
1553 1586 opts[fpats[followfirst]] = list(match.files())
1554 1587 else:
1555 1588 opts[fnopats[followdescendants][followfirst]] = str(startrev)
1556 1589 else:
1557 1590 opts['_patslog'] = list(pats)
1558 1591
1559 1592 filematcher = None
1560 1593 if opts.get('patch') or opts.get('stat'):
1561 1594 if follow:
1562 1595 filematcher = _makegraphfilematcher(repo, pats, followfirst)
1563 1596 else:
1564 1597 filematcher = lambda rev: match
1565 1598
1566 1599 expr = []
1567 1600 for op, val in opts.iteritems():
1568 1601 if not val:
1569 1602 continue
1570 1603 if op not in opt2revset:
1571 1604 continue
1572 1605 revop, andor = opt2revset[op]
1573 1606 if '%(val)' not in revop:
1574 1607 expr.append(revop)
1575 1608 else:
1576 1609 if not isinstance(val, list):
1577 1610 e = revop % {'val': val}
1578 1611 else:
1579 1612 e = '(' + andor.join((revop % {'val': v}) for v in val) + ')'
1580 1613 expr.append(e)
1581 1614
1582 1615 if expr:
1583 1616 expr = '(' + ' and '.join(expr) + ')'
1584 1617 else:
1585 1618 expr = None
1586 1619 return expr, filematcher
1587 1620
1588 1621 def getgraphlogrevs(repo, pats, opts):
1589 1622 """Return (revs, expr, filematcher) where revs is an iterable of
1590 1623 revision numbers, expr is a revset string built from log options
1591 1624 and file patterns or None, and used to filter 'revs'. If --stat or
1592 1625 --patch are not passed filematcher is None. Otherwise it is a
1593 1626 callable taking a revision number and returning a match objects
1594 1627 filtering the files to be detailed when displaying the revision.
1595 1628 """
1596 1629 if not len(repo):
1597 1630 return [], None, None
1598 1631 limit = loglimit(opts)
1599 1632 # Default --rev value depends on --follow but --follow behaviour
1600 1633 # depends on revisions resolved from --rev...
1601 1634 follow = opts.get('follow') or opts.get('follow_first')
1602 1635 possiblyunsorted = False # whether revs might need sorting
1603 1636 if opts.get('rev'):
1604 1637 revs = scmutil.revrange(repo, opts['rev'])
1605 1638 # Don't sort here because _makegraphlogrevset might depend on the
1606 1639 # order of revs
1607 1640 possiblyunsorted = True
1608 1641 else:
1609 1642 if follow and len(repo) > 0:
1610 1643 revs = repo.revs('reverse(:.)')
1611 1644 else:
1612 1645 revs = revset.baseset(repo.changelog)
1613 1646 revs.reverse()
1614 1647 if not revs:
1615 1648 return [], None, None
1616 1649 revs = revset.baseset(revs)
1617 1650 expr, filematcher = _makegraphlogrevset(repo, pats, opts, revs)
1618 1651 if possiblyunsorted:
1619 1652 revs.sort(reverse=True)
1620 1653 if expr:
1621 1654 # Revset matchers often operate faster on revisions in changelog
1622 1655 # order, because most filters deal with the changelog.
1623 1656 revs.reverse()
1624 1657 matcher = revset.match(repo.ui, expr)
1625 1658 # Revset matches can reorder revisions. "A or B" typically returns
1626 1659 # returns the revision matching A then the revision matching B. Sort
1627 1660 # again to fix that.
1628 1661 revs = matcher(repo, revs)
1629 1662 revs.sort(reverse=True)
1630 1663 if limit is not None:
1631 1664 revs = revs[:limit]
1632 1665
1633 1666 return revs, expr, filematcher
1634 1667
1635 1668 def displaygraph(ui, dag, displayer, showparents, edgefn, getrenamed=None,
1636 1669 filematcher=None):
1637 1670 seen, state = [], graphmod.asciistate()
1638 1671 for rev, type, ctx, parents in dag:
1639 1672 char = 'o'
1640 1673 if ctx.node() in showparents:
1641 1674 char = '@'
1642 1675 elif ctx.obsolete():
1643 1676 char = 'x'
1644 1677 copies = None
1645 1678 if getrenamed and ctx.rev():
1646 1679 copies = []
1647 1680 for fn in ctx.files():
1648 1681 rename = getrenamed(fn, ctx.rev())
1649 1682 if rename:
1650 1683 copies.append((fn, rename[0]))
1651 1684 revmatchfn = None
1652 1685 if filematcher is not None:
1653 1686 revmatchfn = filematcher(ctx.rev())
1654 1687 displayer.show(ctx, copies=copies, matchfn=revmatchfn)
1655 1688 lines = displayer.hunk.pop(rev).split('\n')
1656 1689 if not lines[-1]:
1657 1690 del lines[-1]
1658 1691 displayer.flush(rev)
1659 1692 edges = edgefn(type, char, lines, seen, rev, parents)
1660 1693 for type, char, lines, coldata in edges:
1661 1694 graphmod.ascii(ui, state, type, char, lines, coldata)
1662 1695 displayer.close()
1663 1696
1664 1697 def graphlog(ui, repo, *pats, **opts):
1665 1698 # Parameters are identical to log command ones
1666 1699 revs, expr, filematcher = getgraphlogrevs(repo, pats, opts)
1667 1700 revdag = graphmod.dagwalker(repo, revs)
1668 1701
1669 1702 getrenamed = None
1670 1703 if opts.get('copies'):
1671 1704 endrev = None
1672 1705 if opts.get('rev'):
1673 1706 endrev = max(scmutil.revrange(repo, opts.get('rev'))) + 1
1674 1707 getrenamed = templatekw.getrenamedfn(repo, endrev=endrev)
1675 1708 displayer = show_changeset(ui, repo, opts, buffered=True)
1676 1709 showparents = [ctx.node() for ctx in repo[None].parents()]
1677 1710 displaygraph(ui, revdag, displayer, showparents,
1678 1711 graphmod.asciiedges, getrenamed, filematcher)
1679 1712
1680 1713 def checkunsupportedgraphflags(pats, opts):
1681 1714 for op in ["newest_first"]:
1682 1715 if op in opts and opts[op]:
1683 1716 raise util.Abort(_("-G/--graph option is incompatible with --%s")
1684 1717 % op.replace("_", "-"))
1685 1718
1686 1719 def graphrevs(repo, nodes, opts):
1687 1720 limit = loglimit(opts)
1688 1721 nodes.reverse()
1689 1722 if limit is not None:
1690 1723 nodes = nodes[:limit]
1691 1724 return graphmod.nodes(repo, nodes)
1692 1725
1693 1726 def add(ui, repo, match, dryrun, listsubrepos, prefix, explicitonly):
1694 1727 join = lambda f: os.path.join(prefix, f)
1695 1728 bad = []
1696 1729 oldbad = match.bad
1697 1730 match.bad = lambda x, y: bad.append(x) or oldbad(x, y)
1698 1731 names = []
1699 1732 wctx = repo[None]
1700 1733 cca = None
1701 1734 abort, warn = scmutil.checkportabilityalert(ui)
1702 1735 if abort or warn:
1703 1736 cca = scmutil.casecollisionauditor(ui, abort, repo.dirstate)
1704 1737 for f in repo.walk(match):
1705 1738 exact = match.exact(f)
1706 1739 if exact or not explicitonly and f not in repo.dirstate:
1707 1740 if cca:
1708 1741 cca(f)
1709 1742 names.append(f)
1710 1743 if ui.verbose or not exact:
1711 1744 ui.status(_('adding %s\n') % match.rel(join(f)))
1712 1745
1713 1746 for subpath in sorted(wctx.substate):
1714 1747 sub = wctx.sub(subpath)
1715 1748 try:
1716 1749 submatch = matchmod.narrowmatcher(subpath, match)
1717 1750 if listsubrepos:
1718 1751 bad.extend(sub.add(ui, submatch, dryrun, listsubrepos, prefix,
1719 1752 False))
1720 1753 else:
1721 1754 bad.extend(sub.add(ui, submatch, dryrun, listsubrepos, prefix,
1722 1755 True))
1723 1756 except error.LookupError:
1724 1757 ui.status(_("skipping missing subrepository: %s\n")
1725 1758 % join(subpath))
1726 1759
1727 1760 if not dryrun:
1728 1761 rejected = wctx.add(names, prefix)
1729 1762 bad.extend(f for f in rejected if f in match.files())
1730 1763 return bad
1731 1764
1732 1765 def forget(ui, repo, match, prefix, explicitonly):
1733 1766 join = lambda f: os.path.join(prefix, f)
1734 1767 bad = []
1735 1768 oldbad = match.bad
1736 1769 match.bad = lambda x, y: bad.append(x) or oldbad(x, y)
1737 1770 wctx = repo[None]
1738 1771 forgot = []
1739 1772 s = repo.status(match=match, clean=True)
1740 1773 forget = sorted(s[0] + s[1] + s[3] + s[6])
1741 1774 if explicitonly:
1742 1775 forget = [f for f in forget if match.exact(f)]
1743 1776
1744 1777 for subpath in sorted(wctx.substate):
1745 1778 sub = wctx.sub(subpath)
1746 1779 try:
1747 1780 submatch = matchmod.narrowmatcher(subpath, match)
1748 1781 subbad, subforgot = sub.forget(ui, submatch, prefix)
1749 1782 bad.extend([subpath + '/' + f for f in subbad])
1750 1783 forgot.extend([subpath + '/' + f for f in subforgot])
1751 1784 except error.LookupError:
1752 1785 ui.status(_("skipping missing subrepository: %s\n")
1753 1786 % join(subpath))
1754 1787
1755 1788 if not explicitonly:
1756 1789 for f in match.files():
1757 1790 if f not in repo.dirstate and not os.path.isdir(match.rel(join(f))):
1758 1791 if f not in forgot:
1759 1792 if os.path.exists(match.rel(join(f))):
1760 1793 ui.warn(_('not removing %s: '
1761 1794 'file is already untracked\n')
1762 1795 % match.rel(join(f)))
1763 1796 bad.append(f)
1764 1797
1765 1798 for f in forget:
1766 1799 if ui.verbose or not match.exact(f):
1767 1800 ui.status(_('removing %s\n') % match.rel(join(f)))
1768 1801
1769 1802 rejected = wctx.forget(forget, prefix)
1770 1803 bad.extend(f for f in rejected if f in match.files())
1771 1804 forgot.extend(forget)
1772 1805 return bad, forgot
1773 1806
1774 1807 def duplicatecopies(repo, rev, fromrev):
1775 1808 '''reproduce copies from fromrev to rev in the dirstate'''
1776 1809 for dst, src in copies.pathcopies(repo[fromrev], repo[rev]).iteritems():
1777 1810 # copies.pathcopies returns backward renames, so dst might not
1778 1811 # actually be in the dirstate
1779 1812 if repo.dirstate[dst] in "nma":
1780 1813 repo.dirstate.copy(src, dst)
1781 1814
1782 1815 def commit(ui, repo, commitfunc, pats, opts):
1783 1816 '''commit the specified files or all outstanding changes'''
1784 1817 date = opts.get('date')
1785 1818 if date:
1786 1819 opts['date'] = util.parsedate(date)
1787 1820 message = logmessage(ui, opts)
1788 1821
1789 1822 # extract addremove carefully -- this function can be called from a command
1790 1823 # that doesn't support addremove
1791 1824 if opts.get('addremove'):
1792 1825 scmutil.addremove(repo, pats, opts)
1793 1826
1794 1827 return commitfunc(ui, repo, message,
1795 1828 scmutil.match(repo[None], pats, opts), opts)
1796 1829
1797 1830 def amend(ui, repo, commitfunc, old, extra, pats, opts):
1798 1831 ui.note(_('amending changeset %s\n') % old)
1799 1832 base = old.p1()
1800 1833
1801 1834 wlock = lock = newid = None
1802 1835 try:
1803 1836 wlock = repo.wlock()
1804 1837 lock = repo.lock()
1805 1838 tr = repo.transaction('amend')
1806 1839 try:
1807 1840 # See if we got a message from -m or -l, if not, open the editor
1808 1841 # with the message of the changeset to amend
1809 1842 message = logmessage(ui, opts)
1810 1843 # ensure logfile does not conflict with later enforcement of the
1811 1844 # message. potential logfile content has been processed by
1812 1845 # `logmessage` anyway.
1813 1846 opts.pop('logfile')
1814 1847 # First, do a regular commit to record all changes in the working
1815 1848 # directory (if there are any)
1816 1849 ui.callhooks = False
1817 1850 currentbookmark = repo._bookmarkcurrent
1818 1851 try:
1819 1852 repo._bookmarkcurrent = None
1820 1853 opts['message'] = 'temporary amend commit for %s' % old
1821 1854 node = commit(ui, repo, commitfunc, pats, opts)
1822 1855 finally:
1823 1856 repo._bookmarkcurrent = currentbookmark
1824 1857 ui.callhooks = True
1825 1858 ctx = repo[node]
1826 1859
1827 1860 # Participating changesets:
1828 1861 #
1829 1862 # node/ctx o - new (intermediate) commit that contains changes
1830 1863 # | from working dir to go into amending commit
1831 1864 # | (or a workingctx if there were no changes)
1832 1865 # |
1833 1866 # old o - changeset to amend
1834 1867 # |
1835 1868 # base o - parent of amending changeset
1836 1869
1837 1870 # Update extra dict from amended commit (e.g. to preserve graft
1838 1871 # source)
1839 1872 extra.update(old.extra())
1840 1873
1841 1874 # Also update it from the intermediate commit or from the wctx
1842 1875 extra.update(ctx.extra())
1843 1876
1844 1877 if len(old.parents()) > 1:
1845 1878 # ctx.files() isn't reliable for merges, so fall back to the
1846 1879 # slower repo.status() method
1847 1880 files = set([fn for st in repo.status(base, old)[:3]
1848 1881 for fn in st])
1849 1882 else:
1850 1883 files = set(old.files())
1851 1884
1852 1885 # Second, we use either the commit we just did, or if there were no
1853 1886 # changes the parent of the working directory as the version of the
1854 1887 # files in the final amend commit
1855 1888 if node:
1856 1889 ui.note(_('copying changeset %s to %s\n') % (ctx, base))
1857 1890
1858 1891 user = ctx.user()
1859 1892 date = ctx.date()
1860 1893 # Recompute copies (avoid recording a -> b -> a)
1861 1894 copied = copies.pathcopies(base, ctx)
1862 1895
1863 1896 # Prune files which were reverted by the updates: if old
1864 1897 # introduced file X and our intermediate commit, node,
1865 1898 # renamed that file, then those two files are the same and
1866 1899 # we can discard X from our list of files. Likewise if X
1867 1900 # was deleted, it's no longer relevant
1868 1901 files.update(ctx.files())
1869 1902
1870 1903 def samefile(f):
1871 1904 if f in ctx.manifest():
1872 1905 a = ctx.filectx(f)
1873 1906 if f in base.manifest():
1874 1907 b = base.filectx(f)
1875 1908 return (not a.cmp(b)
1876 1909 and a.flags() == b.flags())
1877 1910 else:
1878 1911 return False
1879 1912 else:
1880 1913 return f not in base.manifest()
1881 1914 files = [f for f in files if not samefile(f)]
1882 1915
1883 1916 def filectxfn(repo, ctx_, path):
1884 1917 try:
1885 1918 fctx = ctx[path]
1886 1919 flags = fctx.flags()
1887 1920 mctx = context.memfilectx(fctx.path(), fctx.data(),
1888 1921 islink='l' in flags,
1889 1922 isexec='x' in flags,
1890 1923 copied=copied.get(path))
1891 1924 return mctx
1892 1925 except KeyError:
1893 1926 raise IOError
1894 1927 else:
1895 1928 ui.note(_('copying changeset %s to %s\n') % (old, base))
1896 1929
1897 1930 # Use version of files as in the old cset
1898 1931 def filectxfn(repo, ctx_, path):
1899 1932 try:
1900 1933 return old.filectx(path)
1901 1934 except KeyError:
1902 1935 raise IOError
1903 1936
1904 1937 user = opts.get('user') or old.user()
1905 1938 date = opts.get('date') or old.date()
1906 1939 editmsg = False
1907 1940 if not message:
1908 1941 editmsg = True
1909 1942 message = old.description()
1910 1943
1911 1944 pureextra = extra.copy()
1912 1945 extra['amend_source'] = old.hex()
1913 1946
1914 1947 new = context.memctx(repo,
1915 1948 parents=[base.node(), old.p2().node()],
1916 1949 text=message,
1917 1950 files=files,
1918 1951 filectxfn=filectxfn,
1919 1952 user=user,
1920 1953 date=date,
1921 1954 extra=extra)
1922 1955 if editmsg:
1923 1956 new._text = commitforceeditor(repo, new, [])
1924 1957
1925 1958 newdesc = changelog.stripdesc(new.description())
1926 1959 if ((not node)
1927 1960 and newdesc == old.description()
1928 1961 and user == old.user()
1929 1962 and date == old.date()
1930 1963 and pureextra == old.extra()):
1931 1964 # nothing changed. continuing here would create a new node
1932 1965 # anyway because of the amend_source noise.
1933 1966 #
1934 1967 # This not what we expect from amend.
1935 1968 return old.node()
1936 1969
1937 1970 ph = repo.ui.config('phases', 'new-commit', phases.draft)
1938 1971 try:
1939 1972 repo.ui.setconfig('phases', 'new-commit', old.phase())
1940 1973 newid = repo.commitctx(new)
1941 1974 finally:
1942 1975 repo.ui.setconfig('phases', 'new-commit', ph)
1943 1976 if newid != old.node():
1944 1977 # Reroute the working copy parent to the new changeset
1945 1978 repo.setparents(newid, nullid)
1946 1979
1947 1980 # Move bookmarks from old parent to amend commit
1948 1981 bms = repo.nodebookmarks(old.node())
1949 1982 if bms:
1950 1983 marks = repo._bookmarks
1951 1984 for bm in bms:
1952 1985 marks[bm] = newid
1953 1986 marks.write()
1954 1987 #commit the whole amend process
1955 1988 if obsolete._enabled and newid != old.node():
1956 1989 # mark the new changeset as successor of the rewritten one
1957 1990 new = repo[newid]
1958 1991 obs = [(old, (new,))]
1959 1992 if node:
1960 1993 obs.append((ctx, ()))
1961 1994
1962 1995 obsolete.createmarkers(repo, obs)
1963 1996 tr.close()
1964 1997 finally:
1965 1998 tr.release()
1966 1999 if (not obsolete._enabled) and newid != old.node():
1967 2000 # Strip the intermediate commit (if there was one) and the amended
1968 2001 # commit
1969 2002 if node:
1970 2003 ui.note(_('stripping intermediate changeset %s\n') % ctx)
1971 2004 ui.note(_('stripping amended changeset %s\n') % old)
1972 2005 repair.strip(ui, repo, old.node(), topic='amend-backup')
1973 2006 finally:
1974 2007 if newid is None:
1975 2008 repo.dirstate.invalidate()
1976 2009 lockmod.release(lock, wlock)
1977 2010 return newid
1978 2011
1979 2012 def commiteditor(repo, ctx, subs):
1980 2013 if ctx.description():
1981 2014 return ctx.description()
1982 2015 return commitforceeditor(repo, ctx, subs)
1983 2016
1984 2017 def commitforceeditor(repo, ctx, subs):
1985 2018 edittext = []
1986 2019 modified, added, removed = ctx.modified(), ctx.added(), ctx.removed()
1987 2020 if ctx.description():
1988 2021 edittext.append(ctx.description())
1989 2022 edittext.append("")
1990 2023 edittext.append("") # Empty line between message and comments.
1991 2024 edittext.append(_("HG: Enter commit message."
1992 2025 " Lines beginning with 'HG:' are removed."))
1993 2026 edittext.append(_("HG: Leave message empty to abort commit."))
1994 2027 edittext.append("HG: --")
1995 2028 edittext.append(_("HG: user: %s") % ctx.user())
1996 2029 if ctx.p2():
1997 2030 edittext.append(_("HG: branch merge"))
1998 2031 if ctx.branch():
1999 2032 edittext.append(_("HG: branch '%s'") % ctx.branch())
2000 2033 if bookmarks.iscurrent(repo):
2001 2034 edittext.append(_("HG: bookmark '%s'") % repo._bookmarkcurrent)
2002 2035 edittext.extend([_("HG: subrepo %s") % s for s in subs])
2003 2036 edittext.extend([_("HG: added %s") % f for f in added])
2004 2037 edittext.extend([_("HG: changed %s") % f for f in modified])
2005 2038 edittext.extend([_("HG: removed %s") % f for f in removed])
2006 2039 if not added and not modified and not removed:
2007 2040 edittext.append(_("HG: no files changed"))
2008 2041 edittext.append("")
2009 2042 # run editor in the repository root
2010 2043 olddir = os.getcwd()
2011 2044 os.chdir(repo.root)
2012 2045 text = repo.ui.edit("\n".join(edittext), ctx.user(), ctx.extra())
2013 2046 text = re.sub("(?m)^HG:.*(\n|$)", "", text)
2014 2047 os.chdir(olddir)
2015 2048
2016 2049 if not text.strip():
2017 2050 raise util.Abort(_("empty commit message"))
2018 2051
2019 2052 return text
2020 2053
2021 2054 def commitstatus(repo, node, branch, bheads=None, opts={}):
2022 2055 ctx = repo[node]
2023 2056 parents = ctx.parents()
2024 2057
2025 2058 if (not opts.get('amend') and bheads and node not in bheads and not
2026 2059 [x for x in parents if x.node() in bheads and x.branch() == branch]):
2027 2060 repo.ui.status(_('created new head\n'))
2028 2061 # The message is not printed for initial roots. For the other
2029 2062 # changesets, it is printed in the following situations:
2030 2063 #
2031 2064 # Par column: for the 2 parents with ...
2032 2065 # N: null or no parent
2033 2066 # B: parent is on another named branch
2034 2067 # C: parent is a regular non head changeset
2035 2068 # H: parent was a branch head of the current branch
2036 2069 # Msg column: whether we print "created new head" message
2037 2070 # In the following, it is assumed that there already exists some
2038 2071 # initial branch heads of the current branch, otherwise nothing is
2039 2072 # printed anyway.
2040 2073 #
2041 2074 # Par Msg Comment
2042 2075 # N N y additional topo root
2043 2076 #
2044 2077 # B N y additional branch root
2045 2078 # C N y additional topo head
2046 2079 # H N n usual case
2047 2080 #
2048 2081 # B B y weird additional branch root
2049 2082 # C B y branch merge
2050 2083 # H B n merge with named branch
2051 2084 #
2052 2085 # C C y additional head from merge
2053 2086 # C H n merge with a head
2054 2087 #
2055 2088 # H H n head merge: head count decreases
2056 2089
2057 2090 if not opts.get('close_branch'):
2058 2091 for r in parents:
2059 2092 if r.closesbranch() and r.branch() == branch:
2060 2093 repo.ui.status(_('reopening closed branch head %d\n') % r)
2061 2094
2062 2095 if repo.ui.debugflag:
2063 2096 repo.ui.write(_('committed changeset %d:%s\n') % (int(ctx), ctx.hex()))
2064 2097 elif repo.ui.verbose:
2065 2098 repo.ui.write(_('committed changeset %d:%s\n') % (int(ctx), ctx))
2066 2099
2067 2100 def revert(ui, repo, ctx, parents, *pats, **opts):
2068 2101 parent, p2 = parents
2069 2102 node = ctx.node()
2070 2103
2071 2104 mf = ctx.manifest()
2072 2105 if node == parent:
2073 2106 pmf = mf
2074 2107 else:
2075 2108 pmf = None
2076 2109
2077 2110 # need all matching names in dirstate and manifest of target rev,
2078 2111 # so have to walk both. do not print errors if files exist in one
2079 2112 # but not other.
2080 2113
2081 2114 names = {}
2082 2115
2083 2116 wlock = repo.wlock()
2084 2117 try:
2085 2118 # walk dirstate.
2086 2119
2087 2120 m = scmutil.match(repo[None], pats, opts)
2088 2121 m.bad = lambda x, y: False
2089 2122 for abs in repo.walk(m):
2090 2123 names[abs] = m.rel(abs), m.exact(abs)
2091 2124
2092 2125 # walk target manifest.
2093 2126
2094 2127 def badfn(path, msg):
2095 2128 if path in names:
2096 2129 return
2097 2130 if path in ctx.substate:
2098 2131 return
2099 2132 path_ = path + '/'
2100 2133 for f in names:
2101 2134 if f.startswith(path_):
2102 2135 return
2103 2136 ui.warn("%s: %s\n" % (m.rel(path), msg))
2104 2137
2105 2138 m = scmutil.match(ctx, pats, opts)
2106 2139 m.bad = badfn
2107 2140 for abs in ctx.walk(m):
2108 2141 if abs not in names:
2109 2142 names[abs] = m.rel(abs), m.exact(abs)
2110 2143
2111 2144 # get the list of subrepos that must be reverted
2112 2145 targetsubs = sorted(s for s in ctx.substate if m(s))
2113 2146 m = scmutil.matchfiles(repo, names)
2114 2147 changes = repo.status(match=m)[:4]
2115 2148 modified, added, removed, deleted = map(set, changes)
2116 2149
2117 2150 # if f is a rename, also revert the source
2118 2151 cwd = repo.getcwd()
2119 2152 for f in added:
2120 2153 src = repo.dirstate.copied(f)
2121 2154 if src and src not in names and repo.dirstate[src] == 'r':
2122 2155 removed.add(src)
2123 2156 names[src] = (repo.pathto(src, cwd), True)
2124 2157
2125 2158 def removeforget(abs):
2126 2159 if repo.dirstate[abs] == 'a':
2127 2160 return _('forgetting %s\n')
2128 2161 return _('removing %s\n')
2129 2162
2130 2163 revert = ([], _('reverting %s\n'))
2131 2164 add = ([], _('adding %s\n'))
2132 2165 remove = ([], removeforget)
2133 2166 undelete = ([], _('undeleting %s\n'))
2134 2167
2135 2168 disptable = (
2136 2169 # dispatch table:
2137 2170 # file state
2138 2171 # action if in target manifest
2139 2172 # action if not in target manifest
2140 2173 # make backup if in target manifest
2141 2174 # make backup if not in target manifest
2142 2175 (modified, revert, remove, True, True),
2143 2176 (added, revert, remove, True, False),
2144 2177 (removed, undelete, None, True, False),
2145 2178 (deleted, revert, remove, False, False),
2146 2179 )
2147 2180
2148 2181 for abs, (rel, exact) in sorted(names.items()):
2149 2182 mfentry = mf.get(abs)
2150 2183 target = repo.wjoin(abs)
2151 2184 def handle(xlist, dobackup):
2152 2185 xlist[0].append(abs)
2153 2186 if (dobackup and not opts.get('no_backup') and
2154 2187 os.path.lexists(target) and
2155 2188 abs in ctx and repo[None][abs].cmp(ctx[abs])):
2156 2189 bakname = "%s.orig" % rel
2157 2190 ui.note(_('saving current version of %s as %s\n') %
2158 2191 (rel, bakname))
2159 2192 if not opts.get('dry_run'):
2160 2193 util.rename(target, bakname)
2161 2194 if ui.verbose or not exact:
2162 2195 msg = xlist[1]
2163 2196 if not isinstance(msg, basestring):
2164 2197 msg = msg(abs)
2165 2198 ui.status(msg % rel)
2166 2199 for table, hitlist, misslist, backuphit, backupmiss in disptable:
2167 2200 if abs not in table:
2168 2201 continue
2169 2202 # file has changed in dirstate
2170 2203 if mfentry:
2171 2204 handle(hitlist, backuphit)
2172 2205 elif misslist is not None:
2173 2206 handle(misslist, backupmiss)
2174 2207 break
2175 2208 else:
2176 2209 if abs not in repo.dirstate:
2177 2210 if mfentry:
2178 2211 handle(add, True)
2179 2212 elif exact:
2180 2213 ui.warn(_('file not managed: %s\n') % rel)
2181 2214 continue
2182 2215 # file has not changed in dirstate
2183 2216 if node == parent:
2184 2217 if exact:
2185 2218 ui.warn(_('no changes needed to %s\n') % rel)
2186 2219 continue
2187 2220 if pmf is None:
2188 2221 # only need parent manifest in this unlikely case,
2189 2222 # so do not read by default
2190 2223 pmf = repo[parent].manifest()
2191 2224 if abs in pmf and mfentry:
2192 2225 # if version of file is same in parent and target
2193 2226 # manifests, do nothing
2194 2227 if (pmf[abs] != mfentry or
2195 2228 pmf.flags(abs) != mf.flags(abs)):
2196 2229 handle(revert, False)
2197 2230 else:
2198 2231 handle(remove, False)
2199 2232 if not opts.get('dry_run'):
2200 2233 _performrevert(repo, parents, ctx, revert, add, remove, undelete)
2201 2234
2202 2235 if targetsubs:
2203 2236 # Revert the subrepos on the revert list
2204 2237 for sub in targetsubs:
2205 2238 ctx.sub(sub).revert(ui, ctx.substate[sub], *pats, **opts)
2206 2239 finally:
2207 2240 wlock.release()
2208 2241
2209 2242 def _performrevert(repo, parents, ctx, revert, add, remove, undelete):
2210 2243 """function that actually perform all the action computed for revert
2211 2244
2212 2245 This is an independent function to let extension to plug in and react to
2213 2246 the imminent revert.
2214 2247
2215 2248 Make sure you have the working directory locked when caling this function.
2216 2249 """
2217 2250 parent, p2 = parents
2218 2251 node = ctx.node()
2219 2252 def checkout(f):
2220 2253 fc = ctx[f]
2221 2254 repo.wwrite(f, fc.data(), fc.flags())
2222 2255
2223 2256 audit_path = pathutil.pathauditor(repo.root)
2224 2257 for f in remove[0]:
2225 2258 if repo.dirstate[f] == 'a':
2226 2259 repo.dirstate.drop(f)
2227 2260 continue
2228 2261 audit_path(f)
2229 2262 try:
2230 2263 util.unlinkpath(repo.wjoin(f))
2231 2264 except OSError:
2232 2265 pass
2233 2266 repo.dirstate.remove(f)
2234 2267
2235 2268 normal = None
2236 2269 if node == parent:
2237 2270 # We're reverting to our parent. If possible, we'd like status
2238 2271 # to report the file as clean. We have to use normallookup for
2239 2272 # merges to avoid losing information about merged/dirty files.
2240 2273 if p2 != nullid:
2241 2274 normal = repo.dirstate.normallookup
2242 2275 else:
2243 2276 normal = repo.dirstate.normal
2244 2277 for f in revert[0]:
2245 2278 checkout(f)
2246 2279 if normal:
2247 2280 normal(f)
2248 2281
2249 2282 for f in add[0]:
2250 2283 checkout(f)
2251 2284 repo.dirstate.add(f)
2252 2285
2253 2286 normal = repo.dirstate.normallookup
2254 2287 if node == parent and p2 == nullid:
2255 2288 normal = repo.dirstate.normal
2256 2289 for f in undelete[0]:
2257 2290 checkout(f)
2258 2291 normal(f)
2259 2292
2260 2293 copied = copies.pathcopies(repo[parent], ctx)
2261 2294
2262 2295 for f in add[0] + undelete[0] + revert[0]:
2263 2296 if f in copied:
2264 2297 repo.dirstate.copy(copied[f], f)
2265 2298
2266 2299 def command(table):
2267 2300 '''returns a function object bound to table which can be used as
2268 2301 a decorator for populating table as a command table'''
2269 2302
2270 2303 def cmd(name, options=(), synopsis=None):
2271 2304 def decorator(func):
2272 2305 if synopsis:
2273 2306 table[name] = func, list(options), synopsis
2274 2307 else:
2275 2308 table[name] = func, list(options)
2276 2309 return func
2277 2310 return decorator
2278 2311
2279 2312 return cmd
2280 2313
2281 2314 # a list of (ui, repo) functions called by commands.summary
2282 2315 summaryhooks = util.hooks()
2283 2316
2284 2317 # A list of state files kept by multistep operations like graft.
2285 2318 # Since graft cannot be aborted, it is considered 'clearable' by update.
2286 2319 # note: bisect is intentionally excluded
2287 2320 # (state file, clearable, allowcommit, error, hint)
2288 2321 unfinishedstates = [
2289 2322 ('graftstate', True, False, _('graft in progress'),
2290 2323 _("use 'hg graft --continue' or 'hg update' to abort")),
2291 2324 ('updatestate', True, False, _('last update was interrupted'),
2292 2325 _("use 'hg update' to get a consistent checkout"))
2293 2326 ]
2294 2327
2295 2328 def checkunfinished(repo, commit=False):
2296 2329 '''Look for an unfinished multistep operation, like graft, and abort
2297 2330 if found. It's probably good to check this right before
2298 2331 bailifchanged().
2299 2332 '''
2300 2333 for f, clearable, allowcommit, msg, hint in unfinishedstates:
2301 2334 if commit and allowcommit:
2302 2335 continue
2303 2336 if repo.vfs.exists(f):
2304 2337 raise util.Abort(msg, hint=hint)
2305 2338
2306 2339 def clearunfinished(repo):
2307 2340 '''Check for unfinished operations (as above), and clear the ones
2308 2341 that are clearable.
2309 2342 '''
2310 2343 for f, clearable, allowcommit, msg, hint in unfinishedstates:
2311 2344 if not clearable and repo.vfs.exists(f):
2312 2345 raise util.Abort(msg, hint=hint)
2313 2346 for f, clearable, allowcommit, msg, hint in unfinishedstates:
2314 2347 if clearable and repo.vfs.exists(f):
2315 2348 util.unlink(repo.join(f))
@@ -1,1685 +1,1708 b''
1 1 $ hg init a
2 2 $ cd a
3 3 $ echo a > a
4 4 $ hg add a
5 5 $ echo line 1 > b
6 6 $ echo line 2 >> b
7 7 $ hg commit -l b -d '1000000 0' -u 'User Name <user@hostname>'
8 8
9 9 $ hg add b
10 10 $ echo other 1 > c
11 11 $ echo other 2 >> c
12 12 $ echo >> c
13 13 $ echo other 3 >> c
14 14 $ hg commit -l c -d '1100000 0' -u 'A. N. Other <other@place>'
15 15
16 16 $ hg add c
17 17 $ hg commit -m 'no person' -d '1200000 0' -u 'other@place'
18 18 $ echo c >> c
19 19 $ hg commit -m 'no user, no domain' -d '1300000 0' -u 'person'
20 20
21 21 $ echo foo > .hg/branch
22 22 $ hg commit -m 'new branch' -d '1400000 0' -u 'person'
23 23
24 24 $ hg co -q 3
25 25 $ echo other 4 >> d
26 26 $ hg add d
27 27 $ hg commit -m 'new head' -d '1500000 0' -u 'person'
28 28
29 29 $ hg merge -q foo
30 30 $ hg commit -m 'merge' -d '1500001 0' -u 'person'
31 31
32 32 Second branch starting at nullrev:
33 33
34 34 $ hg update null
35 35 0 files updated, 0 files merged, 4 files removed, 0 files unresolved
36 36 $ echo second > second
37 37 $ hg add second
38 38 $ hg commit -m second -d '1000000 0' -u 'User Name <user@hostname>'
39 39 created new head
40 40
41 41 $ echo third > third
42 42 $ hg add third
43 43 $ hg mv second fourth
44 44 $ hg commit -m third -d "2020-01-01 10:01"
45 45
46 46 $ hg log --template '{join(file_copies, ",\n")}\n' -r .
47 47 fourth (second)
48 48 $ hg log -T '{file_copies % "{source} -> {name}\n"}' -r .
49 49 second -> fourth
50 50
51 51 Quoting for ui.logtemplate
52 52
53 53 $ hg tip --config "ui.logtemplate={rev}\n"
54 54 8
55 55 $ hg tip --config "ui.logtemplate='{rev}\n'"
56 56 8
57 57 $ hg tip --config 'ui.logtemplate="{rev}\n"'
58 58 8
59 59
60 60 Make sure user/global hgrc does not affect tests
61 61
62 62 $ echo '[ui]' > .hg/hgrc
63 63 $ echo 'logtemplate =' >> .hg/hgrc
64 64 $ echo 'style =' >> .hg/hgrc
65 65
66 Add some simple styles to settings
67
68 $ echo '[templates]' >> .hg/hgrc
69 $ printf 'simple = "{rev}\\n"\n' >> .hg/hgrc
70 $ printf 'simple2 = {rev}\\n\n' >> .hg/hgrc
71
72 $ hg log -l1 -Tsimple
73 8
74 $ hg log -l1 -Tsimple2
75 8
76
77 Test templates and style maps in files:
78
79 $ echo "{rev}" > tmpl
80 $ hg log -l1 -T./tmpl
81 8
82 $ hg log -l1 -Tblah/blah
83 blah/blah (no-eol)
84
85 $ printf 'changeset = "{rev}\\n"\n' > map-simple
86 $ hg log -l1 -T./map-simple
87 8
88
66 89 Default style is like normal output:
67 90
68 91 $ hg log > log.out
69 92 $ hg log --style default > style.out
70 93 $ cmp log.out style.out || diff -u log.out style.out
71 94
72 95 $ hg log -v > log.out
73 96 $ hg log -v --style default > style.out
74 97 $ cmp log.out style.out || diff -u log.out style.out
75 98
76 99 $ hg log --debug > log.out
77 100 $ hg log --debug --style default > style.out
78 101 $ cmp log.out style.out || diff -u log.out style.out
79 102
80 103 Revision with no copies (used to print a traceback):
81 104
82 105 $ hg tip -v --template '\n'
83 106
84 107
85 108 Compact style works:
86 109
87 $ hg log --style compact
110 $ hg log -Tcompact
88 111 8[tip] 95c24699272e 2020-01-01 10:01 +0000 test
89 112 third
90 113
91 114 7:-1 29114dbae42b 1970-01-12 13:46 +0000 user
92 115 second
93 116
94 117 6:5,4 d41e714fe50d 1970-01-18 08:40 +0000 person
95 118 merge
96 119
97 120 5:3 13207e5a10d9 1970-01-18 08:40 +0000 person
98 121 new head
99 122
100 123 4 bbe44766e73d 1970-01-17 04:53 +0000 person
101 124 new branch
102 125
103 126 3 10e46f2dcbf4 1970-01-16 01:06 +0000 person
104 127 no user, no domain
105 128
106 129 2 97054abb4ab8 1970-01-14 21:20 +0000 other
107 130 no person
108 131
109 132 1 b608e9d1a3f0 1970-01-13 17:33 +0000 other
110 133 other 1
111 134
112 135 0 1e4e1b8f71e0 1970-01-12 13:46 +0000 user
113 136 line 1
114 137
115 138
116 139 $ hg log -v --style compact
117 140 8[tip] 95c24699272e 2020-01-01 10:01 +0000 test
118 141 third
119 142
120 143 7:-1 29114dbae42b 1970-01-12 13:46 +0000 User Name <user@hostname>
121 144 second
122 145
123 146 6:5,4 d41e714fe50d 1970-01-18 08:40 +0000 person
124 147 merge
125 148
126 149 5:3 13207e5a10d9 1970-01-18 08:40 +0000 person
127 150 new head
128 151
129 152 4 bbe44766e73d 1970-01-17 04:53 +0000 person
130 153 new branch
131 154
132 155 3 10e46f2dcbf4 1970-01-16 01:06 +0000 person
133 156 no user, no domain
134 157
135 158 2 97054abb4ab8 1970-01-14 21:20 +0000 other@place
136 159 no person
137 160
138 161 1 b608e9d1a3f0 1970-01-13 17:33 +0000 A. N. Other <other@place>
139 162 other 1
140 163 other 2
141 164
142 165 other 3
143 166
144 167 0 1e4e1b8f71e0 1970-01-12 13:46 +0000 User Name <user@hostname>
145 168 line 1
146 169 line 2
147 170
148 171
149 172 $ hg log --debug --style compact
150 173 8[tip]:7,-1 95c24699272e 2020-01-01 10:01 +0000 test
151 174 third
152 175
153 176 7:-1,-1 29114dbae42b 1970-01-12 13:46 +0000 User Name <user@hostname>
154 177 second
155 178
156 179 6:5,4 d41e714fe50d 1970-01-18 08:40 +0000 person
157 180 merge
158 181
159 182 5:3,-1 13207e5a10d9 1970-01-18 08:40 +0000 person
160 183 new head
161 184
162 185 4:3,-1 bbe44766e73d 1970-01-17 04:53 +0000 person
163 186 new branch
164 187
165 188 3:2,-1 10e46f2dcbf4 1970-01-16 01:06 +0000 person
166 189 no user, no domain
167 190
168 191 2:1,-1 97054abb4ab8 1970-01-14 21:20 +0000 other@place
169 192 no person
170 193
171 194 1:0,-1 b608e9d1a3f0 1970-01-13 17:33 +0000 A. N. Other <other@place>
172 195 other 1
173 196 other 2
174 197
175 198 other 3
176 199
177 200 0:-1,-1 1e4e1b8f71e0 1970-01-12 13:46 +0000 User Name <user@hostname>
178 201 line 1
179 202 line 2
180 203
181 204
182 205 Test xml styles:
183 206
184 207 $ hg log --style xml
185 208 <?xml version="1.0"?>
186 209 <log>
187 210 <logentry revision="8" node="95c24699272ef57d062b8bccc32c878bf841784a">
188 211 <tag>tip</tag>
189 212 <author email="test">test</author>
190 213 <date>2020-01-01T10:01:00+00:00</date>
191 214 <msg xml:space="preserve">third</msg>
192 215 </logentry>
193 216 <logentry revision="7" node="29114dbae42b9f078cf2714dbe3a86bba8ec7453">
194 217 <parent revision="-1" node="0000000000000000000000000000000000000000" />
195 218 <author email="user@hostname">User Name</author>
196 219 <date>1970-01-12T13:46:40+00:00</date>
197 220 <msg xml:space="preserve">second</msg>
198 221 </logentry>
199 222 <logentry revision="6" node="d41e714fe50d9e4a5f11b4d595d543481b5f980b">
200 223 <parent revision="5" node="13207e5a10d9fd28ec424934298e176197f2c67f" />
201 224 <parent revision="4" node="bbe44766e73d5f11ed2177f1838de10c53ef3e74" />
202 225 <author email="person">person</author>
203 226 <date>1970-01-18T08:40:01+00:00</date>
204 227 <msg xml:space="preserve">merge</msg>
205 228 </logentry>
206 229 <logentry revision="5" node="13207e5a10d9fd28ec424934298e176197f2c67f">
207 230 <parent revision="3" node="10e46f2dcbf4823578cf180f33ecf0b957964c47" />
208 231 <author email="person">person</author>
209 232 <date>1970-01-18T08:40:00+00:00</date>
210 233 <msg xml:space="preserve">new head</msg>
211 234 </logentry>
212 235 <logentry revision="4" node="bbe44766e73d5f11ed2177f1838de10c53ef3e74">
213 236 <branch>foo</branch>
214 237 <author email="person">person</author>
215 238 <date>1970-01-17T04:53:20+00:00</date>
216 239 <msg xml:space="preserve">new branch</msg>
217 240 </logentry>
218 241 <logentry revision="3" node="10e46f2dcbf4823578cf180f33ecf0b957964c47">
219 242 <author email="person">person</author>
220 243 <date>1970-01-16T01:06:40+00:00</date>
221 244 <msg xml:space="preserve">no user, no domain</msg>
222 245 </logentry>
223 246 <logentry revision="2" node="97054abb4ab824450e9164180baf491ae0078465">
224 247 <author email="other@place">other</author>
225 248 <date>1970-01-14T21:20:00+00:00</date>
226 249 <msg xml:space="preserve">no person</msg>
227 250 </logentry>
228 251 <logentry revision="1" node="b608e9d1a3f0273ccf70fb85fd6866b3482bf965">
229 252 <author email="other@place">A. N. Other</author>
230 253 <date>1970-01-13T17:33:20+00:00</date>
231 254 <msg xml:space="preserve">other 1
232 255 other 2
233 256
234 257 other 3</msg>
235 258 </logentry>
236 259 <logentry revision="0" node="1e4e1b8f71e05681d422154f5421e385fec3454f">
237 260 <author email="user@hostname">User Name</author>
238 261 <date>1970-01-12T13:46:40+00:00</date>
239 262 <msg xml:space="preserve">line 1
240 263 line 2</msg>
241 264 </logentry>
242 265 </log>
243 266
244 267 $ hg log -v --style xml
245 268 <?xml version="1.0"?>
246 269 <log>
247 270 <logentry revision="8" node="95c24699272ef57d062b8bccc32c878bf841784a">
248 271 <tag>tip</tag>
249 272 <author email="test">test</author>
250 273 <date>2020-01-01T10:01:00+00:00</date>
251 274 <msg xml:space="preserve">third</msg>
252 275 <paths>
253 276 <path action="A">fourth</path>
254 277 <path action="A">third</path>
255 278 <path action="R">second</path>
256 279 </paths>
257 280 <copies>
258 281 <copy source="second">fourth</copy>
259 282 </copies>
260 283 </logentry>
261 284 <logentry revision="7" node="29114dbae42b9f078cf2714dbe3a86bba8ec7453">
262 285 <parent revision="-1" node="0000000000000000000000000000000000000000" />
263 286 <author email="user@hostname">User Name</author>
264 287 <date>1970-01-12T13:46:40+00:00</date>
265 288 <msg xml:space="preserve">second</msg>
266 289 <paths>
267 290 <path action="A">second</path>
268 291 </paths>
269 292 </logentry>
270 293 <logentry revision="6" node="d41e714fe50d9e4a5f11b4d595d543481b5f980b">
271 294 <parent revision="5" node="13207e5a10d9fd28ec424934298e176197f2c67f" />
272 295 <parent revision="4" node="bbe44766e73d5f11ed2177f1838de10c53ef3e74" />
273 296 <author email="person">person</author>
274 297 <date>1970-01-18T08:40:01+00:00</date>
275 298 <msg xml:space="preserve">merge</msg>
276 299 <paths>
277 300 </paths>
278 301 </logentry>
279 302 <logentry revision="5" node="13207e5a10d9fd28ec424934298e176197f2c67f">
280 303 <parent revision="3" node="10e46f2dcbf4823578cf180f33ecf0b957964c47" />
281 304 <author email="person">person</author>
282 305 <date>1970-01-18T08:40:00+00:00</date>
283 306 <msg xml:space="preserve">new head</msg>
284 307 <paths>
285 308 <path action="A">d</path>
286 309 </paths>
287 310 </logentry>
288 311 <logentry revision="4" node="bbe44766e73d5f11ed2177f1838de10c53ef3e74">
289 312 <branch>foo</branch>
290 313 <author email="person">person</author>
291 314 <date>1970-01-17T04:53:20+00:00</date>
292 315 <msg xml:space="preserve">new branch</msg>
293 316 <paths>
294 317 </paths>
295 318 </logentry>
296 319 <logentry revision="3" node="10e46f2dcbf4823578cf180f33ecf0b957964c47">
297 320 <author email="person">person</author>
298 321 <date>1970-01-16T01:06:40+00:00</date>
299 322 <msg xml:space="preserve">no user, no domain</msg>
300 323 <paths>
301 324 <path action="M">c</path>
302 325 </paths>
303 326 </logentry>
304 327 <logentry revision="2" node="97054abb4ab824450e9164180baf491ae0078465">
305 328 <author email="other@place">other</author>
306 329 <date>1970-01-14T21:20:00+00:00</date>
307 330 <msg xml:space="preserve">no person</msg>
308 331 <paths>
309 332 <path action="A">c</path>
310 333 </paths>
311 334 </logentry>
312 335 <logentry revision="1" node="b608e9d1a3f0273ccf70fb85fd6866b3482bf965">
313 336 <author email="other@place">A. N. Other</author>
314 337 <date>1970-01-13T17:33:20+00:00</date>
315 338 <msg xml:space="preserve">other 1
316 339 other 2
317 340
318 341 other 3</msg>
319 342 <paths>
320 343 <path action="A">b</path>
321 344 </paths>
322 345 </logentry>
323 346 <logentry revision="0" node="1e4e1b8f71e05681d422154f5421e385fec3454f">
324 347 <author email="user@hostname">User Name</author>
325 348 <date>1970-01-12T13:46:40+00:00</date>
326 349 <msg xml:space="preserve">line 1
327 350 line 2</msg>
328 351 <paths>
329 352 <path action="A">a</path>
330 353 </paths>
331 354 </logentry>
332 355 </log>
333 356
334 357 $ hg log --debug --style xml
335 358 <?xml version="1.0"?>
336 359 <log>
337 360 <logentry revision="8" node="95c24699272ef57d062b8bccc32c878bf841784a">
338 361 <tag>tip</tag>
339 362 <parent revision="7" node="29114dbae42b9f078cf2714dbe3a86bba8ec7453" />
340 363 <parent revision="-1" node="0000000000000000000000000000000000000000" />
341 364 <author email="test">test</author>
342 365 <date>2020-01-01T10:01:00+00:00</date>
343 366 <msg xml:space="preserve">third</msg>
344 367 <paths>
345 368 <path action="A">fourth</path>
346 369 <path action="A">third</path>
347 370 <path action="R">second</path>
348 371 </paths>
349 372 <copies>
350 373 <copy source="second">fourth</copy>
351 374 </copies>
352 375 <extra key="branch">default</extra>
353 376 </logentry>
354 377 <logentry revision="7" node="29114dbae42b9f078cf2714dbe3a86bba8ec7453">
355 378 <parent revision="-1" node="0000000000000000000000000000000000000000" />
356 379 <parent revision="-1" node="0000000000000000000000000000000000000000" />
357 380 <author email="user@hostname">User Name</author>
358 381 <date>1970-01-12T13:46:40+00:00</date>
359 382 <msg xml:space="preserve">second</msg>
360 383 <paths>
361 384 <path action="A">second</path>
362 385 </paths>
363 386 <extra key="branch">default</extra>
364 387 </logentry>
365 388 <logentry revision="6" node="d41e714fe50d9e4a5f11b4d595d543481b5f980b">
366 389 <parent revision="5" node="13207e5a10d9fd28ec424934298e176197f2c67f" />
367 390 <parent revision="4" node="bbe44766e73d5f11ed2177f1838de10c53ef3e74" />
368 391 <author email="person">person</author>
369 392 <date>1970-01-18T08:40:01+00:00</date>
370 393 <msg xml:space="preserve">merge</msg>
371 394 <paths>
372 395 </paths>
373 396 <extra key="branch">default</extra>
374 397 </logentry>
375 398 <logentry revision="5" node="13207e5a10d9fd28ec424934298e176197f2c67f">
376 399 <parent revision="3" node="10e46f2dcbf4823578cf180f33ecf0b957964c47" />
377 400 <parent revision="-1" node="0000000000000000000000000000000000000000" />
378 401 <author email="person">person</author>
379 402 <date>1970-01-18T08:40:00+00:00</date>
380 403 <msg xml:space="preserve">new head</msg>
381 404 <paths>
382 405 <path action="A">d</path>
383 406 </paths>
384 407 <extra key="branch">default</extra>
385 408 </logentry>
386 409 <logentry revision="4" node="bbe44766e73d5f11ed2177f1838de10c53ef3e74">
387 410 <branch>foo</branch>
388 411 <parent revision="3" node="10e46f2dcbf4823578cf180f33ecf0b957964c47" />
389 412 <parent revision="-1" node="0000000000000000000000000000000000000000" />
390 413 <author email="person">person</author>
391 414 <date>1970-01-17T04:53:20+00:00</date>
392 415 <msg xml:space="preserve">new branch</msg>
393 416 <paths>
394 417 </paths>
395 418 <extra key="branch">foo</extra>
396 419 </logentry>
397 420 <logentry revision="3" node="10e46f2dcbf4823578cf180f33ecf0b957964c47">
398 421 <parent revision="2" node="97054abb4ab824450e9164180baf491ae0078465" />
399 422 <parent revision="-1" node="0000000000000000000000000000000000000000" />
400 423 <author email="person">person</author>
401 424 <date>1970-01-16T01:06:40+00:00</date>
402 425 <msg xml:space="preserve">no user, no domain</msg>
403 426 <paths>
404 427 <path action="M">c</path>
405 428 </paths>
406 429 <extra key="branch">default</extra>
407 430 </logentry>
408 431 <logentry revision="2" node="97054abb4ab824450e9164180baf491ae0078465">
409 432 <parent revision="1" node="b608e9d1a3f0273ccf70fb85fd6866b3482bf965" />
410 433 <parent revision="-1" node="0000000000000000000000000000000000000000" />
411 434 <author email="other@place">other</author>
412 435 <date>1970-01-14T21:20:00+00:00</date>
413 436 <msg xml:space="preserve">no person</msg>
414 437 <paths>
415 438 <path action="A">c</path>
416 439 </paths>
417 440 <extra key="branch">default</extra>
418 441 </logentry>
419 442 <logentry revision="1" node="b608e9d1a3f0273ccf70fb85fd6866b3482bf965">
420 443 <parent revision="0" node="1e4e1b8f71e05681d422154f5421e385fec3454f" />
421 444 <parent revision="-1" node="0000000000000000000000000000000000000000" />
422 445 <author email="other@place">A. N. Other</author>
423 446 <date>1970-01-13T17:33:20+00:00</date>
424 447 <msg xml:space="preserve">other 1
425 448 other 2
426 449
427 450 other 3</msg>
428 451 <paths>
429 452 <path action="A">b</path>
430 453 </paths>
431 454 <extra key="branch">default</extra>
432 455 </logentry>
433 456 <logentry revision="0" node="1e4e1b8f71e05681d422154f5421e385fec3454f">
434 457 <parent revision="-1" node="0000000000000000000000000000000000000000" />
435 458 <parent revision="-1" node="0000000000000000000000000000000000000000" />
436 459 <author email="user@hostname">User Name</author>
437 460 <date>1970-01-12T13:46:40+00:00</date>
438 461 <msg xml:space="preserve">line 1
439 462 line 2</msg>
440 463 <paths>
441 464 <path action="A">a</path>
442 465 </paths>
443 466 <extra key="branch">default</extra>
444 467 </logentry>
445 468 </log>
446 469
447 470
448 471 Error if style not readable:
449 472
450 473 #if unix-permissions no-root
451 474 $ touch q
452 475 $ chmod 0 q
453 476 $ hg log --style ./q
454 477 abort: Permission denied: ./q
455 478 [255]
456 479 #endif
457 480
458 481 Error if no style:
459 482
460 483 $ hg log --style notexist
461 484 abort: style 'notexist' not found
462 485 (available styles: bisect, changelog, compact, default, phases, xml)
463 486 [255]
464 487
465 488 Error if style missing key:
466 489
467 490 $ echo 'q = q' > t
468 491 $ hg log --style ./t
469 492 abort: "changeset" not in template map
470 493 [255]
471 494
472 495 Error if style missing value:
473 496
474 497 $ echo 'changeset =' > t
475 498 $ hg log --style t
476 499 abort: t:1: missing value
477 500 [255]
478 501
479 502 Error if include fails:
480 503
481 504 $ echo 'changeset = q' >> t
482 505 #if unix-permissions no-root
483 506 $ hg log --style ./t
484 507 abort: template file ./q: Permission denied
485 508 [255]
486 509 $ rm q
487 510 #endif
488 511
489 512 Include works:
490 513
491 514 $ echo '{rev}' > q
492 515 $ hg log --style ./t
493 516 8
494 517 7
495 518 6
496 519 5
497 520 4
498 521 3
499 522 2
500 523 1
501 524 0
502 525
503 526 Missing non-standard names give no error (backward compatibility):
504 527
505 528 $ echo "changeset = '{c}'" > t
506 529 $ hg log --style ./t
507 530
508 531 Defining non-standard name works:
509 532
510 533 $ cat <<EOF > t
511 534 > changeset = '{c}'
512 535 > c = q
513 536 > EOF
514 537 $ hg log --style ./t
515 538 8
516 539 7
517 540 6
518 541 5
519 542 4
520 543 3
521 544 2
522 545 1
523 546 0
524 547
525 548 ui.style works:
526 549
527 550 $ echo '[ui]' > .hg/hgrc
528 551 $ echo 'style = t' >> .hg/hgrc
529 552 $ hg log
530 553 8
531 554 7
532 555 6
533 556 5
534 557 4
535 558 3
536 559 2
537 560 1
538 561 0
539 562
540 563
541 564 Issue338:
542 565
543 566 $ hg log --style=changelog > changelog
544 567
545 568 $ cat changelog
546 569 2020-01-01 test <test>
547 570
548 571 * fourth, second, third:
549 572 third
550 573 [95c24699272e] [tip]
551 574
552 575 1970-01-12 User Name <user@hostname>
553 576
554 577 * second:
555 578 second
556 579 [29114dbae42b]
557 580
558 581 1970-01-18 person <person>
559 582
560 583 * merge
561 584 [d41e714fe50d]
562 585
563 586 * d:
564 587 new head
565 588 [13207e5a10d9]
566 589
567 590 1970-01-17 person <person>
568 591
569 592 * new branch
570 593 [bbe44766e73d] <foo>
571 594
572 595 1970-01-16 person <person>
573 596
574 597 * c:
575 598 no user, no domain
576 599 [10e46f2dcbf4]
577 600
578 601 1970-01-14 other <other@place>
579 602
580 603 * c:
581 604 no person
582 605 [97054abb4ab8]
583 606
584 607 1970-01-13 A. N. Other <other@place>
585 608
586 609 * b:
587 610 other 1 other 2
588 611
589 612 other 3
590 613 [b608e9d1a3f0]
591 614
592 615 1970-01-12 User Name <user@hostname>
593 616
594 617 * a:
595 618 line 1 line 2
596 619 [1e4e1b8f71e0]
597 620
598 621
599 622 Issue2130: xml output for 'hg heads' is malformed
600 623
601 624 $ hg heads --style changelog
602 625 2020-01-01 test <test>
603 626
604 627 * fourth, second, third:
605 628 third
606 629 [95c24699272e] [tip]
607 630
608 631 1970-01-18 person <person>
609 632
610 633 * merge
611 634 [d41e714fe50d]
612 635
613 636 1970-01-17 person <person>
614 637
615 638 * new branch
616 639 [bbe44766e73d] <foo>
617 640
618 641
619 642 Keys work:
620 643
621 644 $ for key in author branch branches date desc file_adds file_dels file_mods \
622 645 > file_copies file_copies_switch files \
623 646 > manifest node parents rev tags diffstat extras \
624 647 > p1rev p2rev p1node p2node; do
625 648 > for mode in '' --verbose --debug; do
626 649 > hg log $mode --template "$key$mode: {$key}\n"
627 650 > done
628 651 > done
629 652 author: test
630 653 author: User Name <user@hostname>
631 654 author: person
632 655 author: person
633 656 author: person
634 657 author: person
635 658 author: other@place
636 659 author: A. N. Other <other@place>
637 660 author: User Name <user@hostname>
638 661 author--verbose: test
639 662 author--verbose: User Name <user@hostname>
640 663 author--verbose: person
641 664 author--verbose: person
642 665 author--verbose: person
643 666 author--verbose: person
644 667 author--verbose: other@place
645 668 author--verbose: A. N. Other <other@place>
646 669 author--verbose: User Name <user@hostname>
647 670 author--debug: test
648 671 author--debug: User Name <user@hostname>
649 672 author--debug: person
650 673 author--debug: person
651 674 author--debug: person
652 675 author--debug: person
653 676 author--debug: other@place
654 677 author--debug: A. N. Other <other@place>
655 678 author--debug: User Name <user@hostname>
656 679 branch: default
657 680 branch: default
658 681 branch: default
659 682 branch: default
660 683 branch: foo
661 684 branch: default
662 685 branch: default
663 686 branch: default
664 687 branch: default
665 688 branch--verbose: default
666 689 branch--verbose: default
667 690 branch--verbose: default
668 691 branch--verbose: default
669 692 branch--verbose: foo
670 693 branch--verbose: default
671 694 branch--verbose: default
672 695 branch--verbose: default
673 696 branch--verbose: default
674 697 branch--debug: default
675 698 branch--debug: default
676 699 branch--debug: default
677 700 branch--debug: default
678 701 branch--debug: foo
679 702 branch--debug: default
680 703 branch--debug: default
681 704 branch--debug: default
682 705 branch--debug: default
683 706 branches:
684 707 branches:
685 708 branches:
686 709 branches:
687 710 branches: foo
688 711 branches:
689 712 branches:
690 713 branches:
691 714 branches:
692 715 branches--verbose:
693 716 branches--verbose:
694 717 branches--verbose:
695 718 branches--verbose:
696 719 branches--verbose: foo
697 720 branches--verbose:
698 721 branches--verbose:
699 722 branches--verbose:
700 723 branches--verbose:
701 724 branches--debug:
702 725 branches--debug:
703 726 branches--debug:
704 727 branches--debug:
705 728 branches--debug: foo
706 729 branches--debug:
707 730 branches--debug:
708 731 branches--debug:
709 732 branches--debug:
710 733 date: 1577872860.00
711 734 date: 1000000.00
712 735 date: 1500001.00
713 736 date: 1500000.00
714 737 date: 1400000.00
715 738 date: 1300000.00
716 739 date: 1200000.00
717 740 date: 1100000.00
718 741 date: 1000000.00
719 742 date--verbose: 1577872860.00
720 743 date--verbose: 1000000.00
721 744 date--verbose: 1500001.00
722 745 date--verbose: 1500000.00
723 746 date--verbose: 1400000.00
724 747 date--verbose: 1300000.00
725 748 date--verbose: 1200000.00
726 749 date--verbose: 1100000.00
727 750 date--verbose: 1000000.00
728 751 date--debug: 1577872860.00
729 752 date--debug: 1000000.00
730 753 date--debug: 1500001.00
731 754 date--debug: 1500000.00
732 755 date--debug: 1400000.00
733 756 date--debug: 1300000.00
734 757 date--debug: 1200000.00
735 758 date--debug: 1100000.00
736 759 date--debug: 1000000.00
737 760 desc: third
738 761 desc: second
739 762 desc: merge
740 763 desc: new head
741 764 desc: new branch
742 765 desc: no user, no domain
743 766 desc: no person
744 767 desc: other 1
745 768 other 2
746 769
747 770 other 3
748 771 desc: line 1
749 772 line 2
750 773 desc--verbose: third
751 774 desc--verbose: second
752 775 desc--verbose: merge
753 776 desc--verbose: new head
754 777 desc--verbose: new branch
755 778 desc--verbose: no user, no domain
756 779 desc--verbose: no person
757 780 desc--verbose: other 1
758 781 other 2
759 782
760 783 other 3
761 784 desc--verbose: line 1
762 785 line 2
763 786 desc--debug: third
764 787 desc--debug: second
765 788 desc--debug: merge
766 789 desc--debug: new head
767 790 desc--debug: new branch
768 791 desc--debug: no user, no domain
769 792 desc--debug: no person
770 793 desc--debug: other 1
771 794 other 2
772 795
773 796 other 3
774 797 desc--debug: line 1
775 798 line 2
776 799 file_adds: fourth third
777 800 file_adds: second
778 801 file_adds:
779 802 file_adds: d
780 803 file_adds:
781 804 file_adds:
782 805 file_adds: c
783 806 file_adds: b
784 807 file_adds: a
785 808 file_adds--verbose: fourth third
786 809 file_adds--verbose: second
787 810 file_adds--verbose:
788 811 file_adds--verbose: d
789 812 file_adds--verbose:
790 813 file_adds--verbose:
791 814 file_adds--verbose: c
792 815 file_adds--verbose: b
793 816 file_adds--verbose: a
794 817 file_adds--debug: fourth third
795 818 file_adds--debug: second
796 819 file_adds--debug:
797 820 file_adds--debug: d
798 821 file_adds--debug:
799 822 file_adds--debug:
800 823 file_adds--debug: c
801 824 file_adds--debug: b
802 825 file_adds--debug: a
803 826 file_dels: second
804 827 file_dels:
805 828 file_dels:
806 829 file_dels:
807 830 file_dels:
808 831 file_dels:
809 832 file_dels:
810 833 file_dels:
811 834 file_dels:
812 835 file_dels--verbose: second
813 836 file_dels--verbose:
814 837 file_dels--verbose:
815 838 file_dels--verbose:
816 839 file_dels--verbose:
817 840 file_dels--verbose:
818 841 file_dels--verbose:
819 842 file_dels--verbose:
820 843 file_dels--verbose:
821 844 file_dels--debug: second
822 845 file_dels--debug:
823 846 file_dels--debug:
824 847 file_dels--debug:
825 848 file_dels--debug:
826 849 file_dels--debug:
827 850 file_dels--debug:
828 851 file_dels--debug:
829 852 file_dels--debug:
830 853 file_mods:
831 854 file_mods:
832 855 file_mods:
833 856 file_mods:
834 857 file_mods:
835 858 file_mods: c
836 859 file_mods:
837 860 file_mods:
838 861 file_mods:
839 862 file_mods--verbose:
840 863 file_mods--verbose:
841 864 file_mods--verbose:
842 865 file_mods--verbose:
843 866 file_mods--verbose:
844 867 file_mods--verbose: c
845 868 file_mods--verbose:
846 869 file_mods--verbose:
847 870 file_mods--verbose:
848 871 file_mods--debug:
849 872 file_mods--debug:
850 873 file_mods--debug:
851 874 file_mods--debug:
852 875 file_mods--debug:
853 876 file_mods--debug: c
854 877 file_mods--debug:
855 878 file_mods--debug:
856 879 file_mods--debug:
857 880 file_copies: fourth (second)
858 881 file_copies:
859 882 file_copies:
860 883 file_copies:
861 884 file_copies:
862 885 file_copies:
863 886 file_copies:
864 887 file_copies:
865 888 file_copies:
866 889 file_copies--verbose: fourth (second)
867 890 file_copies--verbose:
868 891 file_copies--verbose:
869 892 file_copies--verbose:
870 893 file_copies--verbose:
871 894 file_copies--verbose:
872 895 file_copies--verbose:
873 896 file_copies--verbose:
874 897 file_copies--verbose:
875 898 file_copies--debug: fourth (second)
876 899 file_copies--debug:
877 900 file_copies--debug:
878 901 file_copies--debug:
879 902 file_copies--debug:
880 903 file_copies--debug:
881 904 file_copies--debug:
882 905 file_copies--debug:
883 906 file_copies--debug:
884 907 file_copies_switch:
885 908 file_copies_switch:
886 909 file_copies_switch:
887 910 file_copies_switch:
888 911 file_copies_switch:
889 912 file_copies_switch:
890 913 file_copies_switch:
891 914 file_copies_switch:
892 915 file_copies_switch:
893 916 file_copies_switch--verbose:
894 917 file_copies_switch--verbose:
895 918 file_copies_switch--verbose:
896 919 file_copies_switch--verbose:
897 920 file_copies_switch--verbose:
898 921 file_copies_switch--verbose:
899 922 file_copies_switch--verbose:
900 923 file_copies_switch--verbose:
901 924 file_copies_switch--verbose:
902 925 file_copies_switch--debug:
903 926 file_copies_switch--debug:
904 927 file_copies_switch--debug:
905 928 file_copies_switch--debug:
906 929 file_copies_switch--debug:
907 930 file_copies_switch--debug:
908 931 file_copies_switch--debug:
909 932 file_copies_switch--debug:
910 933 file_copies_switch--debug:
911 934 files: fourth second third
912 935 files: second
913 936 files:
914 937 files: d
915 938 files:
916 939 files: c
917 940 files: c
918 941 files: b
919 942 files: a
920 943 files--verbose: fourth second third
921 944 files--verbose: second
922 945 files--verbose:
923 946 files--verbose: d
924 947 files--verbose:
925 948 files--verbose: c
926 949 files--verbose: c
927 950 files--verbose: b
928 951 files--verbose: a
929 952 files--debug: fourth second third
930 953 files--debug: second
931 954 files--debug:
932 955 files--debug: d
933 956 files--debug:
934 957 files--debug: c
935 958 files--debug: c
936 959 files--debug: b
937 960 files--debug: a
938 961 manifest: 6:94961b75a2da
939 962 manifest: 5:f2dbc354b94e
940 963 manifest: 4:4dc3def4f9b4
941 964 manifest: 4:4dc3def4f9b4
942 965 manifest: 3:cb5a1327723b
943 966 manifest: 3:cb5a1327723b
944 967 manifest: 2:6e0e82995c35
945 968 manifest: 1:4e8d705b1e53
946 969 manifest: 0:a0c8bcbbb45c
947 970 manifest--verbose: 6:94961b75a2da
948 971 manifest--verbose: 5:f2dbc354b94e
949 972 manifest--verbose: 4:4dc3def4f9b4
950 973 manifest--verbose: 4:4dc3def4f9b4
951 974 manifest--verbose: 3:cb5a1327723b
952 975 manifest--verbose: 3:cb5a1327723b
953 976 manifest--verbose: 2:6e0e82995c35
954 977 manifest--verbose: 1:4e8d705b1e53
955 978 manifest--verbose: 0:a0c8bcbbb45c
956 979 manifest--debug: 6:94961b75a2da554b4df6fb599e5bfc7d48de0c64
957 980 manifest--debug: 5:f2dbc354b94e5ec0b4f10680ee0cee816101d0bf
958 981 manifest--debug: 4:4dc3def4f9b4c6e8de820f6ee74737f91e96a216
959 982 manifest--debug: 4:4dc3def4f9b4c6e8de820f6ee74737f91e96a216
960 983 manifest--debug: 3:cb5a1327723bada42f117e4c55a303246eaf9ccc
961 984 manifest--debug: 3:cb5a1327723bada42f117e4c55a303246eaf9ccc
962 985 manifest--debug: 2:6e0e82995c35d0d57a52aca8da4e56139e06b4b1
963 986 manifest--debug: 1:4e8d705b1e53e3f9375e0e60dc7b525d8211fe55
964 987 manifest--debug: 0:a0c8bcbbb45c63b90b70ad007bf38961f64f2af0
965 988 node: 95c24699272ef57d062b8bccc32c878bf841784a
966 989 node: 29114dbae42b9f078cf2714dbe3a86bba8ec7453
967 990 node: d41e714fe50d9e4a5f11b4d595d543481b5f980b
968 991 node: 13207e5a10d9fd28ec424934298e176197f2c67f
969 992 node: bbe44766e73d5f11ed2177f1838de10c53ef3e74
970 993 node: 10e46f2dcbf4823578cf180f33ecf0b957964c47
971 994 node: 97054abb4ab824450e9164180baf491ae0078465
972 995 node: b608e9d1a3f0273ccf70fb85fd6866b3482bf965
973 996 node: 1e4e1b8f71e05681d422154f5421e385fec3454f
974 997 node--verbose: 95c24699272ef57d062b8bccc32c878bf841784a
975 998 node--verbose: 29114dbae42b9f078cf2714dbe3a86bba8ec7453
976 999 node--verbose: d41e714fe50d9e4a5f11b4d595d543481b5f980b
977 1000 node--verbose: 13207e5a10d9fd28ec424934298e176197f2c67f
978 1001 node--verbose: bbe44766e73d5f11ed2177f1838de10c53ef3e74
979 1002 node--verbose: 10e46f2dcbf4823578cf180f33ecf0b957964c47
980 1003 node--verbose: 97054abb4ab824450e9164180baf491ae0078465
981 1004 node--verbose: b608e9d1a3f0273ccf70fb85fd6866b3482bf965
982 1005 node--verbose: 1e4e1b8f71e05681d422154f5421e385fec3454f
983 1006 node--debug: 95c24699272ef57d062b8bccc32c878bf841784a
984 1007 node--debug: 29114dbae42b9f078cf2714dbe3a86bba8ec7453
985 1008 node--debug: d41e714fe50d9e4a5f11b4d595d543481b5f980b
986 1009 node--debug: 13207e5a10d9fd28ec424934298e176197f2c67f
987 1010 node--debug: bbe44766e73d5f11ed2177f1838de10c53ef3e74
988 1011 node--debug: 10e46f2dcbf4823578cf180f33ecf0b957964c47
989 1012 node--debug: 97054abb4ab824450e9164180baf491ae0078465
990 1013 node--debug: b608e9d1a3f0273ccf70fb85fd6866b3482bf965
991 1014 node--debug: 1e4e1b8f71e05681d422154f5421e385fec3454f
992 1015 parents:
993 1016 parents: -1:000000000000
994 1017 parents: 5:13207e5a10d9 4:bbe44766e73d
995 1018 parents: 3:10e46f2dcbf4
996 1019 parents:
997 1020 parents:
998 1021 parents:
999 1022 parents:
1000 1023 parents:
1001 1024 parents--verbose:
1002 1025 parents--verbose: -1:000000000000
1003 1026 parents--verbose: 5:13207e5a10d9 4:bbe44766e73d
1004 1027 parents--verbose: 3:10e46f2dcbf4
1005 1028 parents--verbose:
1006 1029 parents--verbose:
1007 1030 parents--verbose:
1008 1031 parents--verbose:
1009 1032 parents--verbose:
1010 1033 parents--debug: 7:29114dbae42b9f078cf2714dbe3a86bba8ec7453 -1:0000000000000000000000000000000000000000
1011 1034 parents--debug: -1:0000000000000000000000000000000000000000 -1:0000000000000000000000000000000000000000
1012 1035 parents--debug: 5:13207e5a10d9fd28ec424934298e176197f2c67f 4:bbe44766e73d5f11ed2177f1838de10c53ef3e74
1013 1036 parents--debug: 3:10e46f2dcbf4823578cf180f33ecf0b957964c47 -1:0000000000000000000000000000000000000000
1014 1037 parents--debug: 3:10e46f2dcbf4823578cf180f33ecf0b957964c47 -1:0000000000000000000000000000000000000000
1015 1038 parents--debug: 2:97054abb4ab824450e9164180baf491ae0078465 -1:0000000000000000000000000000000000000000
1016 1039 parents--debug: 1:b608e9d1a3f0273ccf70fb85fd6866b3482bf965 -1:0000000000000000000000000000000000000000
1017 1040 parents--debug: 0:1e4e1b8f71e05681d422154f5421e385fec3454f -1:0000000000000000000000000000000000000000
1018 1041 parents--debug: -1:0000000000000000000000000000000000000000 -1:0000000000000000000000000000000000000000
1019 1042 rev: 8
1020 1043 rev: 7
1021 1044 rev: 6
1022 1045 rev: 5
1023 1046 rev: 4
1024 1047 rev: 3
1025 1048 rev: 2
1026 1049 rev: 1
1027 1050 rev: 0
1028 1051 rev--verbose: 8
1029 1052 rev--verbose: 7
1030 1053 rev--verbose: 6
1031 1054 rev--verbose: 5
1032 1055 rev--verbose: 4
1033 1056 rev--verbose: 3
1034 1057 rev--verbose: 2
1035 1058 rev--verbose: 1
1036 1059 rev--verbose: 0
1037 1060 rev--debug: 8
1038 1061 rev--debug: 7
1039 1062 rev--debug: 6
1040 1063 rev--debug: 5
1041 1064 rev--debug: 4
1042 1065 rev--debug: 3
1043 1066 rev--debug: 2
1044 1067 rev--debug: 1
1045 1068 rev--debug: 0
1046 1069 tags: tip
1047 1070 tags:
1048 1071 tags:
1049 1072 tags:
1050 1073 tags:
1051 1074 tags:
1052 1075 tags:
1053 1076 tags:
1054 1077 tags:
1055 1078 tags--verbose: tip
1056 1079 tags--verbose:
1057 1080 tags--verbose:
1058 1081 tags--verbose:
1059 1082 tags--verbose:
1060 1083 tags--verbose:
1061 1084 tags--verbose:
1062 1085 tags--verbose:
1063 1086 tags--verbose:
1064 1087 tags--debug: tip
1065 1088 tags--debug:
1066 1089 tags--debug:
1067 1090 tags--debug:
1068 1091 tags--debug:
1069 1092 tags--debug:
1070 1093 tags--debug:
1071 1094 tags--debug:
1072 1095 tags--debug:
1073 1096 diffstat: 3: +2/-1
1074 1097 diffstat: 1: +1/-0
1075 1098 diffstat: 0: +0/-0
1076 1099 diffstat: 1: +1/-0
1077 1100 diffstat: 0: +0/-0
1078 1101 diffstat: 1: +1/-0
1079 1102 diffstat: 1: +4/-0
1080 1103 diffstat: 1: +2/-0
1081 1104 diffstat: 1: +1/-0
1082 1105 diffstat--verbose: 3: +2/-1
1083 1106 diffstat--verbose: 1: +1/-0
1084 1107 diffstat--verbose: 0: +0/-0
1085 1108 diffstat--verbose: 1: +1/-0
1086 1109 diffstat--verbose: 0: +0/-0
1087 1110 diffstat--verbose: 1: +1/-0
1088 1111 diffstat--verbose: 1: +4/-0
1089 1112 diffstat--verbose: 1: +2/-0
1090 1113 diffstat--verbose: 1: +1/-0
1091 1114 diffstat--debug: 3: +2/-1
1092 1115 diffstat--debug: 1: +1/-0
1093 1116 diffstat--debug: 0: +0/-0
1094 1117 diffstat--debug: 1: +1/-0
1095 1118 diffstat--debug: 0: +0/-0
1096 1119 diffstat--debug: 1: +1/-0
1097 1120 diffstat--debug: 1: +4/-0
1098 1121 diffstat--debug: 1: +2/-0
1099 1122 diffstat--debug: 1: +1/-0
1100 1123 extras: branch=default
1101 1124 extras: branch=default
1102 1125 extras: branch=default
1103 1126 extras: branch=default
1104 1127 extras: branch=foo
1105 1128 extras: branch=default
1106 1129 extras: branch=default
1107 1130 extras: branch=default
1108 1131 extras: branch=default
1109 1132 extras--verbose: branch=default
1110 1133 extras--verbose: branch=default
1111 1134 extras--verbose: branch=default
1112 1135 extras--verbose: branch=default
1113 1136 extras--verbose: branch=foo
1114 1137 extras--verbose: branch=default
1115 1138 extras--verbose: branch=default
1116 1139 extras--verbose: branch=default
1117 1140 extras--verbose: branch=default
1118 1141 extras--debug: branch=default
1119 1142 extras--debug: branch=default
1120 1143 extras--debug: branch=default
1121 1144 extras--debug: branch=default
1122 1145 extras--debug: branch=foo
1123 1146 extras--debug: branch=default
1124 1147 extras--debug: branch=default
1125 1148 extras--debug: branch=default
1126 1149 extras--debug: branch=default
1127 1150 p1rev: 7
1128 1151 p1rev: -1
1129 1152 p1rev: 5
1130 1153 p1rev: 3
1131 1154 p1rev: 3
1132 1155 p1rev: 2
1133 1156 p1rev: 1
1134 1157 p1rev: 0
1135 1158 p1rev: -1
1136 1159 p1rev--verbose: 7
1137 1160 p1rev--verbose: -1
1138 1161 p1rev--verbose: 5
1139 1162 p1rev--verbose: 3
1140 1163 p1rev--verbose: 3
1141 1164 p1rev--verbose: 2
1142 1165 p1rev--verbose: 1
1143 1166 p1rev--verbose: 0
1144 1167 p1rev--verbose: -1
1145 1168 p1rev--debug: 7
1146 1169 p1rev--debug: -1
1147 1170 p1rev--debug: 5
1148 1171 p1rev--debug: 3
1149 1172 p1rev--debug: 3
1150 1173 p1rev--debug: 2
1151 1174 p1rev--debug: 1
1152 1175 p1rev--debug: 0
1153 1176 p1rev--debug: -1
1154 1177 p2rev: -1
1155 1178 p2rev: -1
1156 1179 p2rev: 4
1157 1180 p2rev: -1
1158 1181 p2rev: -1
1159 1182 p2rev: -1
1160 1183 p2rev: -1
1161 1184 p2rev: -1
1162 1185 p2rev: -1
1163 1186 p2rev--verbose: -1
1164 1187 p2rev--verbose: -1
1165 1188 p2rev--verbose: 4
1166 1189 p2rev--verbose: -1
1167 1190 p2rev--verbose: -1
1168 1191 p2rev--verbose: -1
1169 1192 p2rev--verbose: -1
1170 1193 p2rev--verbose: -1
1171 1194 p2rev--verbose: -1
1172 1195 p2rev--debug: -1
1173 1196 p2rev--debug: -1
1174 1197 p2rev--debug: 4
1175 1198 p2rev--debug: -1
1176 1199 p2rev--debug: -1
1177 1200 p2rev--debug: -1
1178 1201 p2rev--debug: -1
1179 1202 p2rev--debug: -1
1180 1203 p2rev--debug: -1
1181 1204 p1node: 29114dbae42b9f078cf2714dbe3a86bba8ec7453
1182 1205 p1node: 0000000000000000000000000000000000000000
1183 1206 p1node: 13207e5a10d9fd28ec424934298e176197f2c67f
1184 1207 p1node: 10e46f2dcbf4823578cf180f33ecf0b957964c47
1185 1208 p1node: 10e46f2dcbf4823578cf180f33ecf0b957964c47
1186 1209 p1node: 97054abb4ab824450e9164180baf491ae0078465
1187 1210 p1node: b608e9d1a3f0273ccf70fb85fd6866b3482bf965
1188 1211 p1node: 1e4e1b8f71e05681d422154f5421e385fec3454f
1189 1212 p1node: 0000000000000000000000000000000000000000
1190 1213 p1node--verbose: 29114dbae42b9f078cf2714dbe3a86bba8ec7453
1191 1214 p1node--verbose: 0000000000000000000000000000000000000000
1192 1215 p1node--verbose: 13207e5a10d9fd28ec424934298e176197f2c67f
1193 1216 p1node--verbose: 10e46f2dcbf4823578cf180f33ecf0b957964c47
1194 1217 p1node--verbose: 10e46f2dcbf4823578cf180f33ecf0b957964c47
1195 1218 p1node--verbose: 97054abb4ab824450e9164180baf491ae0078465
1196 1219 p1node--verbose: b608e9d1a3f0273ccf70fb85fd6866b3482bf965
1197 1220 p1node--verbose: 1e4e1b8f71e05681d422154f5421e385fec3454f
1198 1221 p1node--verbose: 0000000000000000000000000000000000000000
1199 1222 p1node--debug: 29114dbae42b9f078cf2714dbe3a86bba8ec7453
1200 1223 p1node--debug: 0000000000000000000000000000000000000000
1201 1224 p1node--debug: 13207e5a10d9fd28ec424934298e176197f2c67f
1202 1225 p1node--debug: 10e46f2dcbf4823578cf180f33ecf0b957964c47
1203 1226 p1node--debug: 10e46f2dcbf4823578cf180f33ecf0b957964c47
1204 1227 p1node--debug: 97054abb4ab824450e9164180baf491ae0078465
1205 1228 p1node--debug: b608e9d1a3f0273ccf70fb85fd6866b3482bf965
1206 1229 p1node--debug: 1e4e1b8f71e05681d422154f5421e385fec3454f
1207 1230 p1node--debug: 0000000000000000000000000000000000000000
1208 1231 p2node: 0000000000000000000000000000000000000000
1209 1232 p2node: 0000000000000000000000000000000000000000
1210 1233 p2node: bbe44766e73d5f11ed2177f1838de10c53ef3e74
1211 1234 p2node: 0000000000000000000000000000000000000000
1212 1235 p2node: 0000000000000000000000000000000000000000
1213 1236 p2node: 0000000000000000000000000000000000000000
1214 1237 p2node: 0000000000000000000000000000000000000000
1215 1238 p2node: 0000000000000000000000000000000000000000
1216 1239 p2node: 0000000000000000000000000000000000000000
1217 1240 p2node--verbose: 0000000000000000000000000000000000000000
1218 1241 p2node--verbose: 0000000000000000000000000000000000000000
1219 1242 p2node--verbose: bbe44766e73d5f11ed2177f1838de10c53ef3e74
1220 1243 p2node--verbose: 0000000000000000000000000000000000000000
1221 1244 p2node--verbose: 0000000000000000000000000000000000000000
1222 1245 p2node--verbose: 0000000000000000000000000000000000000000
1223 1246 p2node--verbose: 0000000000000000000000000000000000000000
1224 1247 p2node--verbose: 0000000000000000000000000000000000000000
1225 1248 p2node--verbose: 0000000000000000000000000000000000000000
1226 1249 p2node--debug: 0000000000000000000000000000000000000000
1227 1250 p2node--debug: 0000000000000000000000000000000000000000
1228 1251 p2node--debug: bbe44766e73d5f11ed2177f1838de10c53ef3e74
1229 1252 p2node--debug: 0000000000000000000000000000000000000000
1230 1253 p2node--debug: 0000000000000000000000000000000000000000
1231 1254 p2node--debug: 0000000000000000000000000000000000000000
1232 1255 p2node--debug: 0000000000000000000000000000000000000000
1233 1256 p2node--debug: 0000000000000000000000000000000000000000
1234 1257 p2node--debug: 0000000000000000000000000000000000000000
1235 1258
1236 1259 Filters work:
1237 1260
1238 1261 $ hg log --template '{author|domain}\n'
1239 1262
1240 1263 hostname
1241 1264
1242 1265
1243 1266
1244 1267
1245 1268 place
1246 1269 place
1247 1270 hostname
1248 1271
1249 1272 $ hg log --template '{author|person}\n'
1250 1273 test
1251 1274 User Name
1252 1275 person
1253 1276 person
1254 1277 person
1255 1278 person
1256 1279 other
1257 1280 A. N. Other
1258 1281 User Name
1259 1282
1260 1283 $ hg log --template '{author|user}\n'
1261 1284 test
1262 1285 user
1263 1286 person
1264 1287 person
1265 1288 person
1266 1289 person
1267 1290 other
1268 1291 other
1269 1292 user
1270 1293
1271 1294 $ hg log --template '{date|date}\n'
1272 1295 Wed Jan 01 10:01:00 2020 +0000
1273 1296 Mon Jan 12 13:46:40 1970 +0000
1274 1297 Sun Jan 18 08:40:01 1970 +0000
1275 1298 Sun Jan 18 08:40:00 1970 +0000
1276 1299 Sat Jan 17 04:53:20 1970 +0000
1277 1300 Fri Jan 16 01:06:40 1970 +0000
1278 1301 Wed Jan 14 21:20:00 1970 +0000
1279 1302 Tue Jan 13 17:33:20 1970 +0000
1280 1303 Mon Jan 12 13:46:40 1970 +0000
1281 1304
1282 1305 $ hg log --template '{date|isodate}\n'
1283 1306 2020-01-01 10:01 +0000
1284 1307 1970-01-12 13:46 +0000
1285 1308 1970-01-18 08:40 +0000
1286 1309 1970-01-18 08:40 +0000
1287 1310 1970-01-17 04:53 +0000
1288 1311 1970-01-16 01:06 +0000
1289 1312 1970-01-14 21:20 +0000
1290 1313 1970-01-13 17:33 +0000
1291 1314 1970-01-12 13:46 +0000
1292 1315
1293 1316 $ hg log --template '{date|isodatesec}\n'
1294 1317 2020-01-01 10:01:00 +0000
1295 1318 1970-01-12 13:46:40 +0000
1296 1319 1970-01-18 08:40:01 +0000
1297 1320 1970-01-18 08:40:00 +0000
1298 1321 1970-01-17 04:53:20 +0000
1299 1322 1970-01-16 01:06:40 +0000
1300 1323 1970-01-14 21:20:00 +0000
1301 1324 1970-01-13 17:33:20 +0000
1302 1325 1970-01-12 13:46:40 +0000
1303 1326
1304 1327 $ hg log --template '{date|rfc822date}\n'
1305 1328 Wed, 01 Jan 2020 10:01:00 +0000
1306 1329 Mon, 12 Jan 1970 13:46:40 +0000
1307 1330 Sun, 18 Jan 1970 08:40:01 +0000
1308 1331 Sun, 18 Jan 1970 08:40:00 +0000
1309 1332 Sat, 17 Jan 1970 04:53:20 +0000
1310 1333 Fri, 16 Jan 1970 01:06:40 +0000
1311 1334 Wed, 14 Jan 1970 21:20:00 +0000
1312 1335 Tue, 13 Jan 1970 17:33:20 +0000
1313 1336 Mon, 12 Jan 1970 13:46:40 +0000
1314 1337
1315 1338 $ hg log --template '{desc|firstline}\n'
1316 1339 third
1317 1340 second
1318 1341 merge
1319 1342 new head
1320 1343 new branch
1321 1344 no user, no domain
1322 1345 no person
1323 1346 other 1
1324 1347 line 1
1325 1348
1326 1349 $ hg log --template '{node|short}\n'
1327 1350 95c24699272e
1328 1351 29114dbae42b
1329 1352 d41e714fe50d
1330 1353 13207e5a10d9
1331 1354 bbe44766e73d
1332 1355 10e46f2dcbf4
1333 1356 97054abb4ab8
1334 1357 b608e9d1a3f0
1335 1358 1e4e1b8f71e0
1336 1359
1337 1360 $ hg log --template '<changeset author="{author|xmlescape}"/>\n'
1338 1361 <changeset author="test"/>
1339 1362 <changeset author="User Name &lt;user@hostname&gt;"/>
1340 1363 <changeset author="person"/>
1341 1364 <changeset author="person"/>
1342 1365 <changeset author="person"/>
1343 1366 <changeset author="person"/>
1344 1367 <changeset author="other@place"/>
1345 1368 <changeset author="A. N. Other &lt;other@place&gt;"/>
1346 1369 <changeset author="User Name &lt;user@hostname&gt;"/>
1347 1370
1348 1371 $ hg log --template '{rev}: {children}\n'
1349 1372 8:
1350 1373 7: 8:95c24699272e
1351 1374 6:
1352 1375 5: 6:d41e714fe50d
1353 1376 4: 6:d41e714fe50d
1354 1377 3: 4:bbe44766e73d 5:13207e5a10d9
1355 1378 2: 3:10e46f2dcbf4
1356 1379 1: 2:97054abb4ab8
1357 1380 0: 1:b608e9d1a3f0
1358 1381
1359 1382 Formatnode filter works:
1360 1383
1361 1384 $ hg -q log -r 0 --template '{node|formatnode}\n'
1362 1385 1e4e1b8f71e0
1363 1386
1364 1387 $ hg log -r 0 --template '{node|formatnode}\n'
1365 1388 1e4e1b8f71e0
1366 1389
1367 1390 $ hg -v log -r 0 --template '{node|formatnode}\n'
1368 1391 1e4e1b8f71e0
1369 1392
1370 1393 $ hg --debug log -r 0 --template '{node|formatnode}\n'
1371 1394 1e4e1b8f71e05681d422154f5421e385fec3454f
1372 1395
1373 1396 Age filter:
1374 1397
1375 1398 $ hg log --template '{date|age}\n' > /dev/null || exit 1
1376 1399
1377 1400 >>> from datetime import datetime, timedelta
1378 1401 >>> fp = open('a', 'w')
1379 1402 >>> n = datetime.now() + timedelta(366 * 7)
1380 1403 >>> fp.write('%d-%d-%d 00:00' % (n.year, n.month, n.day))
1381 1404 >>> fp.close()
1382 1405 $ hg add a
1383 1406 $ hg commit -m future -d "`cat a`"
1384 1407
1385 1408 $ hg log -l1 --template '{date|age}\n'
1386 1409 7 years from now
1387 1410
1388 1411 Error on syntax:
1389 1412
1390 1413 $ echo 'x = "f' >> t
1391 1414 $ hg log
1392 1415 abort: t:3: unmatched quotes
1393 1416 [255]
1394 1417
1395 1418 Behind the scenes, this will throw TypeError
1396 1419
1397 1420 $ hg log -l 3 --template '{date|obfuscate}\n'
1398 1421 abort: template filter 'obfuscate' is not compatible with keyword 'date'
1399 1422 [255]
1400 1423
1401 1424 Behind the scenes, this will throw a ValueError
1402 1425
1403 1426 $ hg log -l 3 --template 'line: {desc|shortdate}\n'
1404 1427 abort: template filter 'shortdate' is not compatible with keyword 'desc'
1405 1428 [255]
1406 1429
1407 1430 Behind the scenes, this will throw AttributeError
1408 1431
1409 1432 $ hg log -l 3 --template 'line: {date|escape}\n'
1410 1433 abort: template filter 'escape' is not compatible with keyword 'date'
1411 1434 [255]
1412 1435
1413 1436 Behind the scenes, this will throw ValueError
1414 1437
1415 1438 $ hg tip --template '{author|email|date}\n'
1416 1439 abort: template filter 'datefilter' is not compatible with keyword 'author'
1417 1440 [255]
1418 1441
1419 1442 $ cd ..
1420 1443
1421 1444
1422 1445 latesttag:
1423 1446
1424 1447 $ hg init latesttag
1425 1448 $ cd latesttag
1426 1449
1427 1450 $ echo a > file
1428 1451 $ hg ci -Am a -d '0 0'
1429 1452 adding file
1430 1453
1431 1454 $ echo b >> file
1432 1455 $ hg ci -m b -d '1 0'
1433 1456
1434 1457 $ echo c >> head1
1435 1458 $ hg ci -Am h1c -d '2 0'
1436 1459 adding head1
1437 1460
1438 1461 $ hg update -q 1
1439 1462 $ echo d >> head2
1440 1463 $ hg ci -Am h2d -d '3 0'
1441 1464 adding head2
1442 1465 created new head
1443 1466
1444 1467 $ echo e >> head2
1445 1468 $ hg ci -m h2e -d '4 0'
1446 1469
1447 1470 $ hg merge -q
1448 1471 $ hg ci -m merge -d '5 -3600'
1449 1472
1450 1473 No tag set:
1451 1474
1452 1475 $ hg log --template '{rev}: {latesttag}+{latesttagdistance}\n'
1453 1476 5: null+5
1454 1477 4: null+4
1455 1478 3: null+3
1456 1479 2: null+3
1457 1480 1: null+2
1458 1481 0: null+1
1459 1482
1460 1483 One common tag: longuest path wins:
1461 1484
1462 1485 $ hg tag -r 1 -m t1 -d '6 0' t1
1463 1486 $ hg log --template '{rev}: {latesttag}+{latesttagdistance}\n'
1464 1487 6: t1+4
1465 1488 5: t1+3
1466 1489 4: t1+2
1467 1490 3: t1+1
1468 1491 2: t1+1
1469 1492 1: t1+0
1470 1493 0: null+1
1471 1494
1472 1495 One ancestor tag: more recent wins:
1473 1496
1474 1497 $ hg tag -r 2 -m t2 -d '7 0' t2
1475 1498 $ hg log --template '{rev}: {latesttag}+{latesttagdistance}\n'
1476 1499 7: t2+3
1477 1500 6: t2+2
1478 1501 5: t2+1
1479 1502 4: t1+2
1480 1503 3: t1+1
1481 1504 2: t2+0
1482 1505 1: t1+0
1483 1506 0: null+1
1484 1507
1485 1508 Two branch tags: more recent wins:
1486 1509
1487 1510 $ hg tag -r 3 -m t3 -d '8 0' t3
1488 1511 $ hg log --template '{rev}: {latesttag}+{latesttagdistance}\n'
1489 1512 8: t3+5
1490 1513 7: t3+4
1491 1514 6: t3+3
1492 1515 5: t3+2
1493 1516 4: t3+1
1494 1517 3: t3+0
1495 1518 2: t2+0
1496 1519 1: t1+0
1497 1520 0: null+1
1498 1521
1499 1522 Merged tag overrides:
1500 1523
1501 1524 $ hg tag -r 5 -m t5 -d '9 0' t5
1502 1525 $ hg tag -r 3 -m at3 -d '10 0' at3
1503 1526 $ hg log --template '{rev}: {latesttag}+{latesttagdistance}\n'
1504 1527 10: t5+5
1505 1528 9: t5+4
1506 1529 8: t5+3
1507 1530 7: t5+2
1508 1531 6: t5+1
1509 1532 5: t5+0
1510 1533 4: at3:t3+1
1511 1534 3: at3:t3+0
1512 1535 2: t2+0
1513 1536 1: t1+0
1514 1537 0: null+1
1515 1538
1516 1539 $ cd ..
1517 1540
1518 1541
1519 1542 Style path expansion: issue1948 - ui.style option doesn't work on OSX
1520 1543 if it is a relative path
1521 1544
1522 1545 $ mkdir -p home/styles
1523 1546
1524 1547 $ cat > home/styles/teststyle <<EOF
1525 1548 > changeset = 'test {rev}:{node|short}\n'
1526 1549 > EOF
1527 1550
1528 1551 $ HOME=`pwd`/home; export HOME
1529 1552
1530 1553 $ cat > latesttag/.hg/hgrc <<EOF
1531 1554 > [ui]
1532 1555 > style = ~/styles/teststyle
1533 1556 > EOF
1534 1557
1535 1558 $ hg -R latesttag tip
1536 1559 test 10:9b4a630e5f5f
1537 1560
1538 1561 Test recursive showlist template (issue1989):
1539 1562
1540 1563 $ cat > style1989 <<EOF
1541 1564 > changeset = '{file_mods}{manifest}{extras}'
1542 1565 > file_mod = 'M|{author|person}\n'
1543 1566 > manifest = '{rev},{author}\n'
1544 1567 > extra = '{key}: {author}\n'
1545 1568 > EOF
1546 1569
1547 1570 $ hg -R latesttag log -r tip --style=style1989
1548 1571 M|test
1549 1572 10,test
1550 1573 branch: test
1551 1574
1552 1575 Test new-style inline templating:
1553 1576
1554 1577 $ hg log -R latesttag -r tip --template 'modified files: {file_mods % " {file}\n"}\n'
1555 1578 modified files: .hgtags
1556 1579
1557 1580 Test the sub function of templating for expansion:
1558 1581
1559 1582 $ hg log -R latesttag -r 10 --template '{sub("[0-9]", "x", "{rev}")}\n'
1560 1583 xx
1561 1584
1562 1585 Test the strip function with chars specified:
1563 1586
1564 1587 $ hg log -R latesttag --template '{desc}\n'
1565 1588 at3
1566 1589 t5
1567 1590 t3
1568 1591 t2
1569 1592 t1
1570 1593 merge
1571 1594 h2e
1572 1595 h2d
1573 1596 h1c
1574 1597 b
1575 1598 a
1576 1599
1577 1600 $ hg log -R latesttag --template '{strip(desc, "te")}\n'
1578 1601 at3
1579 1602 5
1580 1603 3
1581 1604 2
1582 1605 1
1583 1606 merg
1584 1607 h2
1585 1608 h2d
1586 1609 h1c
1587 1610 b
1588 1611 a
1589 1612
1590 1613 Test date format:
1591 1614
1592 1615 $ hg log -R latesttag --template 'date: {date(date, "%y %m %d %S %z")}\n'
1593 1616 date: 70 01 01 10 +0000
1594 1617 date: 70 01 01 09 +0000
1595 1618 date: 70 01 01 08 +0000
1596 1619 date: 70 01 01 07 +0000
1597 1620 date: 70 01 01 06 +0000
1598 1621 date: 70 01 01 05 +0100
1599 1622 date: 70 01 01 04 +0000
1600 1623 date: 70 01 01 03 +0000
1601 1624 date: 70 01 01 02 +0000
1602 1625 date: 70 01 01 01 +0000
1603 1626 date: 70 01 01 00 +0000
1604 1627
1605 1628 Test string escaping:
1606 1629
1607 1630 $ hg log -R latesttag -r 0 --template '>\n<>\\n<{if(rev, "[>\n<>\\n<]")}>\n<>\\n<\n'
1608 1631 >
1609 1632 <>\n<[>
1610 1633 <>\n<]>
1611 1634 <>\n<
1612 1635
1613 1636 Test recursive evaluation:
1614 1637
1615 1638 $ hg init r
1616 1639 $ cd r
1617 1640 $ echo a > a
1618 1641 $ hg ci -Am '{rev}'
1619 1642 adding a
1620 1643 $ hg log -r 0 --template '{if(rev, desc)}\n'
1621 1644 {rev}
1622 1645 $ hg log -r 0 --template '{if(rev, "{author} {rev}")}\n'
1623 1646 test 0
1624 1647
1625 1648 Test branches inside if statement:
1626 1649
1627 1650 $ hg log -r 0 --template '{if(branches, "yes", "no")}\n'
1628 1651 no
1629 1652
1630 1653 Test shortest(node) function:
1631 1654
1632 1655 $ echo b > b
1633 1656 $ hg ci -qAm b
1634 1657 $ hg log --template '{shortest(node)}\n'
1635 1658 d97c
1636 1659 f776
1637 1660 $ hg log --template '{shortest(node, 10)}\n'
1638 1661 d97c383ae3
1639 1662 f7769ec2ab
1640 1663
1641 1664 Test pad function
1642 1665
1643 1666 $ hg log --template '{pad(rev, 20)} {author|user}\n'
1644 1667 1 test
1645 1668 0 test
1646 1669
1647 1670 $ hg log --template '{pad(rev, 20, " ", True)} {author|user}\n'
1648 1671 1 test
1649 1672 0 test
1650 1673
1651 1674 $ hg log --template '{pad(rev, 20, "-", False)} {author|user}\n'
1652 1675 1------------------- test
1653 1676 0------------------- test
1654 1677
1655 1678 Test ifcontains function
1656 1679
1657 1680 $ hg log --template '{rev} {ifcontains("a", file_adds, "added a", "did not add a")}\n'
1658 1681 1 did not add a
1659 1682 0 added a
1660 1683
1661 1684 Test revset function
1662 1685
1663 1686 $ hg log --template '{rev} {ifcontains(rev, revset("."), "current rev", "not current rev")}\n'
1664 1687 1 current rev
1665 1688 0 not current rev
1666 1689
1667 1690 $ hg log --template '{rev} Parents: {revset("parents(%s)", rev)}\n'
1668 1691 1 Parents: 0
1669 1692 0 Parents:
1670 1693
1671 1694 $ hg log --template 'Rev: {rev}\n{revset("::%s", rev) % "Ancestor: {revision}\n"}\n'
1672 1695 Rev: 1
1673 1696 Ancestor: 0
1674 1697 Ancestor: 1
1675 1698
1676 1699 Rev: 0
1677 1700 Ancestor: 0
1678 1701
1679 1702 Test current bookmark templating
1680 1703
1681 1704 $ hg book foo
1682 1705 $ hg book bar
1683 1706 $ hg log --template "{rev} {bookmarks % '{bookmark}{ifeq(bookmark, current, \"*\")} '}\n"
1684 1707 1 bar* foo
1685 1708 0
General Comments 0
You need to be logged in to leave comments. Login now