##// END OF EJS Templates
logcmdutil: also check for copies in null revision and working copy...
Martin von Zweigbergk -
r42702:c929f612 default
parent child Browse files
Show More
@@ -1,943 +1,943 b''
1 1 # logcmdutil.py - utility for log-like commands
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import itertools
11 11 import os
12 12 import posixpath
13 13
14 14 from .i18n import _
15 15 from .node import (
16 16 nullid,
17 17 wdirid,
18 18 wdirrev,
19 19 )
20 20
21 21 from . import (
22 22 dagop,
23 23 error,
24 24 formatter,
25 25 graphmod,
26 26 match as matchmod,
27 27 mdiff,
28 28 patch,
29 29 pathutil,
30 30 pycompat,
31 31 revset,
32 32 revsetlang,
33 33 scmutil,
34 34 smartset,
35 35 templatekw,
36 36 templater,
37 37 util,
38 38 )
39 39 from .utils import (
40 40 dateutil,
41 41 stringutil,
42 42 )
43 43
44 44 def getlimit(opts):
45 45 """get the log limit according to option -l/--limit"""
46 46 limit = opts.get('limit')
47 47 if limit:
48 48 try:
49 49 limit = int(limit)
50 50 except ValueError:
51 51 raise error.Abort(_('limit must be a positive integer'))
52 52 if limit <= 0:
53 53 raise error.Abort(_('limit must be positive'))
54 54 else:
55 55 limit = None
56 56 return limit
57 57
58 58 def diffordiffstat(ui, repo, diffopts, node1, node2, match,
59 59 changes=None, stat=False, fp=None, graphwidth=0,
60 60 prefix='', root='', listsubrepos=False, hunksfilterfn=None):
61 61 '''show diff or diffstat.'''
62 62 ctx1 = repo[node1]
63 63 ctx2 = repo[node2]
64 64 if root:
65 65 relroot = pathutil.canonpath(repo.root, repo.getcwd(), root)
66 66 else:
67 67 relroot = ''
68 68 copysourcematch = None
69 69 def compose(f, g):
70 70 return lambda x: f(g(x))
71 71 def pathfn(f):
72 72 return posixpath.join(prefix, f)
73 73 if relroot != '':
74 74 # XXX relative roots currently don't work if the root is within a
75 75 # subrepo
76 76 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=True)
77 77 uirelroot = uipathfn(pathfn(relroot))
78 78 relroot += '/'
79 79 for matchroot in match.files():
80 80 if not matchroot.startswith(relroot):
81 81 ui.warn(_('warning: %s not inside relative root %s\n') %
82 82 (uipathfn(pathfn(matchroot)), uirelroot))
83 83
84 84 relrootmatch = scmutil.match(ctx2, pats=[relroot], default='path')
85 85 match = matchmod.intersectmatchers(match, relrootmatch)
86 86 copysourcematch = relrootmatch
87 87
88 88 checkroot = (repo.ui.configbool('devel', 'all-warnings') or
89 89 repo.ui.configbool('devel', 'check-relroot'))
90 90 def relrootpathfn(f):
91 91 if checkroot and not f.startswith(relroot):
92 92 raise AssertionError(
93 93 "file %s doesn't start with relroot %s" % (f, relroot))
94 94 return f[len(relroot):]
95 95 pathfn = compose(relrootpathfn, pathfn)
96 96
97 97 if stat:
98 98 diffopts = diffopts.copy(context=0, noprefix=False)
99 99 width = 80
100 100 if not ui.plain():
101 101 width = ui.termwidth() - graphwidth
102 102 # If an explicit --root was given, don't respect ui.relative-paths
103 103 if not relroot:
104 104 pathfn = compose(scmutil.getuipathfn(repo), pathfn)
105 105
106 106 chunks = ctx2.diff(ctx1, match, changes, opts=diffopts, pathfn=pathfn,
107 107 copysourcematch=copysourcematch,
108 108 hunksfilterfn=hunksfilterfn)
109 109
110 110 if fp is not None or ui.canwritewithoutlabels():
111 111 out = fp or ui
112 112 if stat:
113 113 chunks = [patch.diffstat(util.iterlines(chunks), width=width)]
114 114 for chunk in util.filechunkiter(util.chunkbuffer(chunks)):
115 115 out.write(chunk)
116 116 else:
117 117 if stat:
118 118 chunks = patch.diffstatui(util.iterlines(chunks), width=width)
119 119 else:
120 120 chunks = patch.difflabel(lambda chunks, **kwargs: chunks, chunks,
121 121 opts=diffopts)
122 122 if ui.canbatchlabeledwrites():
123 123 def gen():
124 124 for chunk, label in chunks:
125 125 yield ui.label(chunk, label=label)
126 126 for chunk in util.filechunkiter(util.chunkbuffer(gen())):
127 127 ui.write(chunk)
128 128 else:
129 129 for chunk, label in chunks:
130 130 ui.write(chunk, label=label)
131 131
132 132 for subpath, sub in scmutil.itersubrepos(ctx1, ctx2):
133 133 tempnode2 = node2
134 134 try:
135 135 if node2 is not None:
136 136 tempnode2 = ctx2.substate[subpath][1]
137 137 except KeyError:
138 138 # A subrepo that existed in node1 was deleted between node1 and
139 139 # node2 (inclusive). Thus, ctx2's substate won't contain that
140 140 # subpath. The best we can do is to ignore it.
141 141 tempnode2 = None
142 142 submatch = matchmod.subdirmatcher(subpath, match)
143 143 subprefix = repo.wvfs.reljoin(prefix, subpath)
144 144 if listsubrepos or match.exact(subpath) or any(submatch.files()):
145 145 sub.diff(ui, diffopts, tempnode2, submatch, changes=changes,
146 146 stat=stat, fp=fp, prefix=subprefix)
147 147
148 148 class changesetdiffer(object):
149 149 """Generate diff of changeset with pre-configured filtering functions"""
150 150
151 151 def _makefilematcher(self, ctx):
152 152 return scmutil.matchall(ctx.repo())
153 153
154 154 def _makehunksfilter(self, ctx):
155 155 return None
156 156
157 157 def showdiff(self, ui, ctx, diffopts, graphwidth=0, stat=False):
158 158 repo = ctx.repo()
159 159 node = ctx.node()
160 160 prev = ctx.p1().node()
161 161 diffordiffstat(ui, repo, diffopts, prev, node,
162 162 match=self._makefilematcher(ctx), stat=stat,
163 163 graphwidth=graphwidth,
164 164 hunksfilterfn=self._makehunksfilter(ctx))
165 165
166 166 def changesetlabels(ctx):
167 167 labels = ['log.changeset', 'changeset.%s' % ctx.phasestr()]
168 168 if ctx.obsolete():
169 169 labels.append('changeset.obsolete')
170 170 if ctx.isunstable():
171 171 labels.append('changeset.unstable')
172 172 for instability in ctx.instabilities():
173 173 labels.append('instability.%s' % instability)
174 174 return ' '.join(labels)
175 175
176 176 class changesetprinter(object):
177 177 '''show changeset information when templating not requested.'''
178 178
179 179 def __init__(self, ui, repo, differ=None, diffopts=None, buffered=False):
180 180 self.ui = ui
181 181 self.repo = repo
182 182 self.buffered = buffered
183 183 self._differ = differ or changesetdiffer()
184 184 self._diffopts = patch.diffallopts(ui, diffopts)
185 185 self._includestat = diffopts and diffopts.get('stat')
186 186 self._includediff = diffopts and diffopts.get('patch')
187 187 self.header = {}
188 188 self.hunk = {}
189 189 self.lastheader = None
190 190 self.footer = None
191 191 self._columns = templatekw.getlogcolumns()
192 192
193 193 def flush(self, ctx):
194 194 rev = ctx.rev()
195 195 if rev in self.header:
196 196 h = self.header[rev]
197 197 if h != self.lastheader:
198 198 self.lastheader = h
199 199 self.ui.write(h)
200 200 del self.header[rev]
201 201 if rev in self.hunk:
202 202 self.ui.write(self.hunk[rev])
203 203 del self.hunk[rev]
204 204
205 205 def close(self):
206 206 if self.footer:
207 207 self.ui.write(self.footer)
208 208
209 209 def show(self, ctx, copies=None, **props):
210 210 props = pycompat.byteskwargs(props)
211 211 if self.buffered:
212 212 self.ui.pushbuffer(labeled=True)
213 213 self._show(ctx, copies, props)
214 214 self.hunk[ctx.rev()] = self.ui.popbuffer()
215 215 else:
216 216 self._show(ctx, copies, props)
217 217
218 218 def _show(self, ctx, copies, props):
219 219 '''show a single changeset or file revision'''
220 220 changenode = ctx.node()
221 221 graphwidth = props.get('graphwidth', 0)
222 222
223 223 if self.ui.quiet:
224 224 self.ui.write("%s\n" % scmutil.formatchangeid(ctx),
225 225 label='log.node')
226 226 return
227 227
228 228 columns = self._columns
229 229 self.ui.write(columns['changeset'] % scmutil.formatchangeid(ctx),
230 230 label=changesetlabels(ctx))
231 231
232 232 # branches are shown first before any other names due to backwards
233 233 # compatibility
234 234 branch = ctx.branch()
235 235 # don't show the default branch name
236 236 if branch != 'default':
237 237 self.ui.write(columns['branch'] % branch, label='log.branch')
238 238
239 239 for nsname, ns in self.repo.names.iteritems():
240 240 # branches has special logic already handled above, so here we just
241 241 # skip it
242 242 if nsname == 'branches':
243 243 continue
244 244 # we will use the templatename as the color name since those two
245 245 # should be the same
246 246 for name in ns.names(self.repo, changenode):
247 247 self.ui.write(ns.logfmt % name,
248 248 label='log.%s' % ns.colorname)
249 249 if self.ui.debugflag:
250 250 self.ui.write(columns['phase'] % ctx.phasestr(), label='log.phase')
251 251 for pctx in scmutil.meaningfulparents(self.repo, ctx):
252 252 label = 'log.parent changeset.%s' % pctx.phasestr()
253 253 self.ui.write(columns['parent'] % scmutil.formatchangeid(pctx),
254 254 label=label)
255 255
256 256 if self.ui.debugflag:
257 257 mnode = ctx.manifestnode()
258 258 if mnode is None:
259 259 mnode = wdirid
260 260 mrev = wdirrev
261 261 else:
262 262 mrev = self.repo.manifestlog.rev(mnode)
263 263 self.ui.write(columns['manifest']
264 264 % scmutil.formatrevnode(self.ui, mrev, mnode),
265 265 label='ui.debug log.manifest')
266 266 self.ui.write(columns['user'] % ctx.user(), label='log.user')
267 267 self.ui.write(columns['date'] % dateutil.datestr(ctx.date()),
268 268 label='log.date')
269 269
270 270 if ctx.isunstable():
271 271 instabilities = ctx.instabilities()
272 272 self.ui.write(columns['instability'] % ', '.join(instabilities),
273 273 label='log.instability')
274 274
275 275 elif ctx.obsolete():
276 276 self._showobsfate(ctx)
277 277
278 278 self._exthook(ctx)
279 279
280 280 if self.ui.debugflag:
281 281 files = ctx.p1().status(ctx)[:3]
282 282 for key, value in zip(['files', 'files+', 'files-'], files):
283 283 if value:
284 284 self.ui.write(columns[key] % " ".join(value),
285 285 label='ui.debug log.files')
286 286 elif ctx.files() and self.ui.verbose:
287 287 self.ui.write(columns['files'] % " ".join(ctx.files()),
288 288 label='ui.note log.files')
289 289 if copies and self.ui.verbose:
290 290 copies = ['%s (%s)' % c for c in copies]
291 291 self.ui.write(columns['copies'] % ' '.join(copies),
292 292 label='ui.note log.copies')
293 293
294 294 extra = ctx.extra()
295 295 if extra and self.ui.debugflag:
296 296 for key, value in sorted(extra.items()):
297 297 self.ui.write(columns['extra']
298 298 % (key, stringutil.escapestr(value)),
299 299 label='ui.debug log.extra')
300 300
301 301 description = ctx.description().strip()
302 302 if description:
303 303 if self.ui.verbose:
304 304 self.ui.write(_("description:\n"),
305 305 label='ui.note log.description')
306 306 self.ui.write(description,
307 307 label='ui.note log.description')
308 308 self.ui.write("\n\n")
309 309 else:
310 310 self.ui.write(columns['summary'] % description.splitlines()[0],
311 311 label='log.summary')
312 312 self.ui.write("\n")
313 313
314 314 self._showpatch(ctx, graphwidth)
315 315
316 316 def _showobsfate(self, ctx):
317 317 # TODO: do not depend on templater
318 318 tres = formatter.templateresources(self.repo.ui, self.repo)
319 319 t = formatter.maketemplater(self.repo.ui, '{join(obsfate, "\n")}',
320 320 defaults=templatekw.keywords,
321 321 resources=tres)
322 322 obsfate = t.renderdefault({'ctx': ctx}).splitlines()
323 323
324 324 if obsfate:
325 325 for obsfateline in obsfate:
326 326 self.ui.write(self._columns['obsolete'] % obsfateline,
327 327 label='log.obsfate')
328 328
329 329 def _exthook(self, ctx):
330 330 '''empty method used by extension as a hook point
331 331 '''
332 332
333 333 def _showpatch(self, ctx, graphwidth=0):
334 334 if self._includestat:
335 335 self._differ.showdiff(self.ui, ctx, self._diffopts,
336 336 graphwidth, stat=True)
337 337 if self._includestat and self._includediff:
338 338 self.ui.write("\n")
339 339 if self._includediff:
340 340 self._differ.showdiff(self.ui, ctx, self._diffopts,
341 341 graphwidth, stat=False)
342 342 if self._includestat or self._includediff:
343 343 self.ui.write("\n")
344 344
345 345 class changesetformatter(changesetprinter):
346 346 """Format changeset information by generic formatter"""
347 347
348 348 def __init__(self, ui, repo, fm, differ=None, diffopts=None,
349 349 buffered=False):
350 350 changesetprinter.__init__(self, ui, repo, differ, diffopts, buffered)
351 351 self._diffopts = patch.difffeatureopts(ui, diffopts, git=True)
352 352 self._fm = fm
353 353
354 354 def close(self):
355 355 self._fm.end()
356 356
357 357 def _show(self, ctx, copies, props):
358 358 '''show a single changeset or file revision'''
359 359 fm = self._fm
360 360 fm.startitem()
361 361 fm.context(ctx=ctx)
362 362 fm.data(rev=scmutil.intrev(ctx),
363 363 node=fm.hexfunc(scmutil.binnode(ctx)))
364 364
365 365 if self.ui.quiet:
366 366 return
367 367
368 368 fm.data(branch=ctx.branch(),
369 369 phase=ctx.phasestr(),
370 370 user=ctx.user(),
371 371 date=fm.formatdate(ctx.date()),
372 372 desc=ctx.description(),
373 373 bookmarks=fm.formatlist(ctx.bookmarks(), name='bookmark'),
374 374 tags=fm.formatlist(ctx.tags(), name='tag'),
375 375 parents=fm.formatlist([fm.hexfunc(c.node())
376 376 for c in ctx.parents()], name='node'))
377 377
378 378 if self.ui.debugflag:
379 379 fm.data(manifest=fm.hexfunc(ctx.manifestnode() or wdirid),
380 380 extra=fm.formatdict(ctx.extra()))
381 381
382 382 files = ctx.p1().status(ctx)
383 383 fm.data(modified=fm.formatlist(files[0], name='file'),
384 384 added=fm.formatlist(files[1], name='file'),
385 385 removed=fm.formatlist(files[2], name='file'))
386 386
387 387 elif self.ui.verbose:
388 388 fm.data(files=fm.formatlist(ctx.files(), name='file'))
389 389 if copies:
390 390 fm.data(copies=fm.formatdict(copies,
391 391 key='name', value='source'))
392 392
393 393 if self._includestat:
394 394 self.ui.pushbuffer()
395 395 self._differ.showdiff(self.ui, ctx, self._diffopts, stat=True)
396 396 fm.data(diffstat=self.ui.popbuffer())
397 397 if self._includediff:
398 398 self.ui.pushbuffer()
399 399 self._differ.showdiff(self.ui, ctx, self._diffopts, stat=False)
400 400 fm.data(diff=self.ui.popbuffer())
401 401
402 402 class changesettemplater(changesetprinter):
403 403 '''format changeset information.
404 404
405 405 Note: there are a variety of convenience functions to build a
406 406 changesettemplater for common cases. See functions such as:
407 407 maketemplater, changesetdisplayer, buildcommittemplate, or other
408 408 functions that use changesest_templater.
409 409 '''
410 410
411 411 # Arguments before "buffered" used to be positional. Consider not
412 412 # adding/removing arguments before "buffered" to not break callers.
413 413 def __init__(self, ui, repo, tmplspec, differ=None, diffopts=None,
414 414 buffered=False):
415 415 changesetprinter.__init__(self, ui, repo, differ, diffopts, buffered)
416 416 # tres is shared with _graphnodeformatter()
417 417 self._tresources = tres = formatter.templateresources(ui, repo)
418 418 self.t = formatter.loadtemplater(ui, tmplspec,
419 419 defaults=templatekw.keywords,
420 420 resources=tres,
421 421 cache=templatekw.defaulttempl)
422 422 self._counter = itertools.count()
423 423
424 424 self._tref = tmplspec.ref
425 425 self._parts = {'header': '', 'footer': '',
426 426 tmplspec.ref: tmplspec.ref,
427 427 'docheader': '', 'docfooter': '',
428 428 'separator': ''}
429 429 if tmplspec.mapfile:
430 430 # find correct templates for current mode, for backward
431 431 # compatibility with 'log -v/-q/--debug' using a mapfile
432 432 tmplmodes = [
433 433 (True, ''),
434 434 (self.ui.verbose, '_verbose'),
435 435 (self.ui.quiet, '_quiet'),
436 436 (self.ui.debugflag, '_debug'),
437 437 ]
438 438 for mode, postfix in tmplmodes:
439 439 for t in self._parts:
440 440 cur = t + postfix
441 441 if mode and cur in self.t:
442 442 self._parts[t] = cur
443 443 else:
444 444 partnames = [p for p in self._parts.keys() if p != tmplspec.ref]
445 445 m = formatter.templatepartsmap(tmplspec, self.t, partnames)
446 446 self._parts.update(m)
447 447
448 448 if self._parts['docheader']:
449 449 self.ui.write(self.t.render(self._parts['docheader'], {}))
450 450
451 451 def close(self):
452 452 if self._parts['docfooter']:
453 453 if not self.footer:
454 454 self.footer = ""
455 455 self.footer += self.t.render(self._parts['docfooter'], {})
456 456 return super(changesettemplater, self).close()
457 457
458 458 def _show(self, ctx, copies, props):
459 459 '''show a single changeset or file revision'''
460 460 props = props.copy()
461 461 props['ctx'] = ctx
462 462 props['index'] = index = next(self._counter)
463 463 props['revcache'] = {'copies': copies}
464 464 graphwidth = props.get('graphwidth', 0)
465 465
466 466 # write separator, which wouldn't work well with the header part below
467 467 # since there's inherently a conflict between header (across items) and
468 468 # separator (per item)
469 469 if self._parts['separator'] and index > 0:
470 470 self.ui.write(self.t.render(self._parts['separator'], {}))
471 471
472 472 # write header
473 473 if self._parts['header']:
474 474 h = self.t.render(self._parts['header'], props)
475 475 if self.buffered:
476 476 self.header[ctx.rev()] = h
477 477 else:
478 478 if self.lastheader != h:
479 479 self.lastheader = h
480 480 self.ui.write(h)
481 481
482 482 # write changeset metadata, then patch if requested
483 483 key = self._parts[self._tref]
484 484 self.ui.write(self.t.render(key, props))
485 485 self._showpatch(ctx, graphwidth)
486 486
487 487 if self._parts['footer']:
488 488 if not self.footer:
489 489 self.footer = self.t.render(self._parts['footer'], props)
490 490
491 491 def templatespec(tmpl, mapfile):
492 492 if pycompat.ispy3:
493 493 assert not isinstance(tmpl, str), 'tmpl must not be a str'
494 494 if mapfile:
495 495 return formatter.templatespec('changeset', tmpl, mapfile)
496 496 else:
497 497 return formatter.templatespec('', tmpl, None)
498 498
499 499 def _lookuptemplate(ui, tmpl, style):
500 500 """Find the template matching the given template spec or style
501 501
502 502 See formatter.lookuptemplate() for details.
503 503 """
504 504
505 505 # ui settings
506 506 if not tmpl and not style: # template are stronger than style
507 507 tmpl = ui.config('ui', 'logtemplate')
508 508 if tmpl:
509 509 return templatespec(templater.unquotestring(tmpl), None)
510 510 else:
511 511 style = util.expandpath(ui.config('ui', 'style'))
512 512
513 513 if not tmpl and style:
514 514 mapfile = style
515 515 if not os.path.split(mapfile)[0]:
516 516 mapname = (templater.templatepath('map-cmdline.' + mapfile)
517 517 or templater.templatepath(mapfile))
518 518 if mapname:
519 519 mapfile = mapname
520 520 return templatespec(None, mapfile)
521 521
522 522 if not tmpl:
523 523 return templatespec(None, None)
524 524
525 525 return formatter.lookuptemplate(ui, 'changeset', tmpl)
526 526
527 527 def maketemplater(ui, repo, tmpl, buffered=False):
528 528 """Create a changesettemplater from a literal template 'tmpl'
529 529 byte-string."""
530 530 spec = templatespec(tmpl, None)
531 531 return changesettemplater(ui, repo, spec, buffered=buffered)
532 532
533 533 def changesetdisplayer(ui, repo, opts, differ=None, buffered=False):
534 534 """show one changeset using template or regular display.
535 535
536 536 Display format will be the first non-empty hit of:
537 537 1. option 'template'
538 538 2. option 'style'
539 539 3. [ui] setting 'logtemplate'
540 540 4. [ui] setting 'style'
541 541 If all of these values are either the unset or the empty string,
542 542 regular display via changesetprinter() is done.
543 543 """
544 544 postargs = (differ, opts, buffered)
545 545 if opts.get('template') in {'cbor', 'json'}:
546 546 fm = ui.formatter('log', opts)
547 547 return changesetformatter(ui, repo, fm, *postargs)
548 548
549 549 spec = _lookuptemplate(ui, opts.get('template'), opts.get('style'))
550 550
551 551 if not spec.ref and not spec.tmpl and not spec.mapfile:
552 552 return changesetprinter(ui, repo, *postargs)
553 553
554 554 return changesettemplater(ui, repo, spec, *postargs)
555 555
556 556 def _makematcher(repo, revs, pats, opts):
557 557 """Build matcher and expanded patterns from log options
558 558
559 559 If --follow, revs are the revisions to follow from.
560 560
561 561 Returns (match, pats, slowpath) where
562 562 - match: a matcher built from the given pats and -I/-X opts
563 563 - pats: patterns used (globs are expanded on Windows)
564 564 - slowpath: True if patterns aren't as simple as scanning filelogs
565 565 """
566 566 # pats/include/exclude are passed to match.match() directly in
567 567 # _matchfiles() revset but walkchangerevs() builds its matcher with
568 568 # scmutil.match(). The difference is input pats are globbed on
569 569 # platforms without shell expansion (windows).
570 570 wctx = repo[None]
571 571 match, pats = scmutil.matchandpats(wctx, pats, opts)
572 572 slowpath = match.anypats() or (not match.always() and opts.get('removed'))
573 573 if not slowpath:
574 574 follow = opts.get('follow') or opts.get('follow_first')
575 575 startctxs = []
576 576 if follow and opts.get('rev'):
577 577 startctxs = [repo[r] for r in revs]
578 578 for f in match.files():
579 579 if follow and startctxs:
580 580 # No idea if the path was a directory at that revision, so
581 581 # take the slow path.
582 582 if any(f not in c for c in startctxs):
583 583 slowpath = True
584 584 continue
585 585 elif follow and f not in wctx:
586 586 # If the file exists, it may be a directory, so let it
587 587 # take the slow path.
588 588 if os.path.exists(repo.wjoin(f)):
589 589 slowpath = True
590 590 continue
591 591 else:
592 592 raise error.Abort(_('cannot follow file not in parent '
593 593 'revision: "%s"') % f)
594 594 filelog = repo.file(f)
595 595 if not filelog:
596 596 # A zero count may be a directory or deleted file, so
597 597 # try to find matching entries on the slow path.
598 598 if follow:
599 599 raise error.Abort(
600 600 _('cannot follow nonexistent file: "%s"') % f)
601 601 slowpath = True
602 602
603 603 # We decided to fall back to the slowpath because at least one
604 604 # of the paths was not a file. Check to see if at least one of them
605 605 # existed in history - in that case, we'll continue down the
606 606 # slowpath; otherwise, we can turn off the slowpath
607 607 if slowpath:
608 608 for path in match.files():
609 609 if path == '.' or path in repo.store:
610 610 break
611 611 else:
612 612 slowpath = False
613 613
614 614 return match, pats, slowpath
615 615
616 616 def _fileancestors(repo, revs, match, followfirst):
617 617 fctxs = []
618 618 for r in revs:
619 619 ctx = repo[r]
620 620 fctxs.extend(ctx[f].introfilectx() for f in ctx.walk(match))
621 621
622 622 # When displaying a revision with --patch --follow FILE, we have
623 623 # to know which file of the revision must be diffed. With
624 624 # --follow, we want the names of the ancestors of FILE in the
625 625 # revision, stored in "fcache". "fcache" is populated as a side effect
626 626 # of the graph traversal.
627 627 fcache = {}
628 628 def filematcher(ctx):
629 629 return scmutil.matchfiles(repo, fcache.get(ctx.rev(), []))
630 630
631 631 def revgen():
632 632 for rev, cs in dagop.filectxancestors(fctxs, followfirst=followfirst):
633 633 fcache[rev] = [c.path() for c in cs]
634 634 yield rev
635 635 return smartset.generatorset(revgen(), iterasc=False), filematcher
636 636
637 637 def _makenofollowfilematcher(repo, pats, opts):
638 638 '''hook for extensions to override the filematcher for non-follow cases'''
639 639 return None
640 640
641 641 _opt2logrevset = {
642 642 'no_merges': ('not merge()', None),
643 643 'only_merges': ('merge()', None),
644 644 '_matchfiles': (None, '_matchfiles(%ps)'),
645 645 'date': ('date(%s)', None),
646 646 'branch': ('branch(%s)', '%lr'),
647 647 '_patslog': ('filelog(%s)', '%lr'),
648 648 'keyword': ('keyword(%s)', '%lr'),
649 649 'prune': ('ancestors(%s)', 'not %lr'),
650 650 'user': ('user(%s)', '%lr'),
651 651 }
652 652
653 653 def _makerevset(repo, match, pats, slowpath, opts):
654 654 """Return a revset string built from log options and file patterns"""
655 655 opts = dict(opts)
656 656 # follow or not follow?
657 657 follow = opts.get('follow') or opts.get('follow_first')
658 658
659 659 # branch and only_branch are really aliases and must be handled at
660 660 # the same time
661 661 opts['branch'] = opts.get('branch', []) + opts.get('only_branch', [])
662 662 opts['branch'] = [repo.lookupbranch(b) for b in opts['branch']]
663 663
664 664 if slowpath:
665 665 # See walkchangerevs() slow path.
666 666 #
667 667 # pats/include/exclude cannot be represented as separate
668 668 # revset expressions as their filtering logic applies at file
669 669 # level. For instance "-I a -X b" matches a revision touching
670 670 # "a" and "b" while "file(a) and not file(b)" does
671 671 # not. Besides, filesets are evaluated against the working
672 672 # directory.
673 673 matchargs = ['r:', 'd:relpath']
674 674 for p in pats:
675 675 matchargs.append('p:' + p)
676 676 for p in opts.get('include', []):
677 677 matchargs.append('i:' + p)
678 678 for p in opts.get('exclude', []):
679 679 matchargs.append('x:' + p)
680 680 opts['_matchfiles'] = matchargs
681 681 elif not follow:
682 682 opts['_patslog'] = list(pats)
683 683
684 684 expr = []
685 685 for op, val in sorted(opts.iteritems()):
686 686 if not val:
687 687 continue
688 688 if op not in _opt2logrevset:
689 689 continue
690 690 revop, listop = _opt2logrevset[op]
691 691 if revop and '%' not in revop:
692 692 expr.append(revop)
693 693 elif not listop:
694 694 expr.append(revsetlang.formatspec(revop, val))
695 695 else:
696 696 if revop:
697 697 val = [revsetlang.formatspec(revop, v) for v in val]
698 698 expr.append(revsetlang.formatspec(listop, val))
699 699
700 700 if expr:
701 701 expr = '(' + ' and '.join(expr) + ')'
702 702 else:
703 703 expr = None
704 704 return expr
705 705
706 706 def _initialrevs(repo, opts):
707 707 """Return the initial set of revisions to be filtered or followed"""
708 708 follow = opts.get('follow') or opts.get('follow_first')
709 709 if opts.get('rev'):
710 710 revs = scmutil.revrange(repo, opts['rev'])
711 711 elif follow and repo.dirstate.p1() == nullid:
712 712 revs = smartset.baseset()
713 713 elif follow:
714 714 revs = repo.revs('.')
715 715 else:
716 716 revs = smartset.spanset(repo)
717 717 revs.reverse()
718 718 return revs
719 719
720 720 def getrevs(repo, pats, opts):
721 721 """Return (revs, differ) where revs is a smartset
722 722
723 723 differ is a changesetdiffer with pre-configured file matcher.
724 724 """
725 725 follow = opts.get('follow') or opts.get('follow_first')
726 726 followfirst = opts.get('follow_first')
727 727 limit = getlimit(opts)
728 728 revs = _initialrevs(repo, opts)
729 729 if not revs:
730 730 return smartset.baseset(), None
731 731 match, pats, slowpath = _makematcher(repo, revs, pats, opts)
732 732 filematcher = None
733 733 if follow:
734 734 if slowpath or match.always():
735 735 revs = dagop.revancestors(repo, revs, followfirst=followfirst)
736 736 else:
737 737 revs, filematcher = _fileancestors(repo, revs, match, followfirst)
738 738 revs.reverse()
739 739 if filematcher is None:
740 740 filematcher = _makenofollowfilematcher(repo, pats, opts)
741 741 if filematcher is None:
742 742 def filematcher(ctx):
743 743 return match
744 744
745 745 expr = _makerevset(repo, match, pats, slowpath, opts)
746 746 if opts.get('graph'):
747 747 # User-specified revs might be unsorted, but don't sort before
748 748 # _makerevset because it might depend on the order of revs
749 749 if repo.ui.configbool('experimental', 'log.topo'):
750 750 if not revs.istopo():
751 751 revs = dagop.toposort(revs, repo.changelog.parentrevs)
752 752 # TODO: try to iterate the set lazily
753 753 revs = revset.baseset(list(revs), istopo=True)
754 754 elif not (revs.isdescending() or revs.istopo()):
755 755 revs.sort(reverse=True)
756 756 if expr:
757 757 matcher = revset.match(None, expr)
758 758 revs = matcher(repo, revs)
759 759 if limit is not None:
760 760 revs = revs.slice(0, limit)
761 761
762 762 differ = changesetdiffer()
763 763 differ._makefilematcher = filematcher
764 764 return revs, differ
765 765
766 766 def _parselinerangeopt(repo, opts):
767 767 """Parse --line-range log option and return a list of tuples (filename,
768 768 (fromline, toline)).
769 769 """
770 770 linerangebyfname = []
771 771 for pat in opts.get('line_range', []):
772 772 try:
773 773 pat, linerange = pat.rsplit(',', 1)
774 774 except ValueError:
775 775 raise error.Abort(_('malformatted line-range pattern %s') % pat)
776 776 try:
777 777 fromline, toline = map(int, linerange.split(':'))
778 778 except ValueError:
779 779 raise error.Abort(_("invalid line range for %s") % pat)
780 780 msg = _("line range pattern '%s' must match exactly one file") % pat
781 781 fname = scmutil.parsefollowlinespattern(repo, None, pat, msg)
782 782 linerangebyfname.append(
783 783 (fname, util.processlinerange(fromline, toline)))
784 784 return linerangebyfname
785 785
786 786 def getlinerangerevs(repo, userrevs, opts):
787 787 """Return (revs, differ).
788 788
789 789 "revs" are revisions obtained by processing "line-range" log options and
790 790 walking block ancestors of each specified file/line-range.
791 791
792 792 "differ" is a changesetdiffer with pre-configured file matcher and hunks
793 793 filter.
794 794 """
795 795 wctx = repo[None]
796 796
797 797 # Two-levels map of "rev -> file ctx -> [line range]".
798 798 linerangesbyrev = {}
799 799 for fname, (fromline, toline) in _parselinerangeopt(repo, opts):
800 800 if fname not in wctx:
801 801 raise error.Abort(_('cannot follow file not in parent '
802 802 'revision: "%s"') % fname)
803 803 fctx = wctx.filectx(fname)
804 804 for fctx, linerange in dagop.blockancestors(fctx, fromline, toline):
805 805 rev = fctx.introrev()
806 806 if rev not in userrevs:
807 807 continue
808 808 linerangesbyrev.setdefault(
809 809 rev, {}).setdefault(
810 810 fctx.path(), []).append(linerange)
811 811
812 812 def nofilterhunksfn(fctx, hunks):
813 813 return hunks
814 814
815 815 def hunksfilter(ctx):
816 816 fctxlineranges = linerangesbyrev.get(ctx.rev())
817 817 if fctxlineranges is None:
818 818 return nofilterhunksfn
819 819
820 820 def filterfn(fctx, hunks):
821 821 lineranges = fctxlineranges.get(fctx.path())
822 822 if lineranges is not None:
823 823 for hr, lines in hunks:
824 824 if hr is None: # binary
825 825 yield hr, lines
826 826 continue
827 827 if any(mdiff.hunkinrange(hr[2:], lr)
828 828 for lr in lineranges):
829 829 yield hr, lines
830 830 else:
831 831 for hunk in hunks:
832 832 yield hunk
833 833
834 834 return filterfn
835 835
836 836 def filematcher(ctx):
837 837 files = list(linerangesbyrev.get(ctx.rev(), []))
838 838 return scmutil.matchfiles(repo, files)
839 839
840 840 revs = sorted(linerangesbyrev, reverse=True)
841 841
842 842 differ = changesetdiffer()
843 843 differ._makefilematcher = filematcher
844 844 differ._makehunksfilter = hunksfilter
845 845 return revs, differ
846 846
847 847 def _graphnodeformatter(ui, displayer):
848 848 spec = ui.config('ui', 'graphnodetemplate')
849 849 if not spec:
850 850 return templatekw.getgraphnode # fast path for "{graphnode}"
851 851
852 852 spec = templater.unquotestring(spec)
853 853 if isinstance(displayer, changesettemplater):
854 854 # reuse cache of slow templates
855 855 tres = displayer._tresources
856 856 else:
857 857 tres = formatter.templateresources(ui)
858 858 templ = formatter.maketemplater(ui, spec, defaults=templatekw.keywords,
859 859 resources=tres)
860 860 def formatnode(repo, ctx):
861 861 props = {'ctx': ctx, 'repo': repo}
862 862 return templ.renderdefault(props)
863 863 return formatnode
864 864
865 865 def displaygraph(ui, repo, dag, displayer, edgefn, getrenamed=None, props=None):
866 866 props = props or {}
867 867 formatnode = _graphnodeformatter(ui, displayer)
868 868 state = graphmod.asciistate()
869 869 styles = state['styles']
870 870
871 871 # only set graph styling if HGPLAIN is not set.
872 872 if ui.plain('graph'):
873 873 # set all edge styles to |, the default pre-3.8 behaviour
874 874 styles.update(dict.fromkeys(styles, '|'))
875 875 else:
876 876 edgetypes = {
877 877 'parent': graphmod.PARENT,
878 878 'grandparent': graphmod.GRANDPARENT,
879 879 'missing': graphmod.MISSINGPARENT
880 880 }
881 881 for name, key in edgetypes.items():
882 882 # experimental config: experimental.graphstyle.*
883 883 styles[key] = ui.config('experimental', 'graphstyle.%s' % name,
884 884 styles[key])
885 885 if not styles[key]:
886 886 styles[key] = None
887 887
888 888 # experimental config: experimental.graphshorten
889 889 state['graphshorten'] = ui.configbool('experimental', 'graphshorten')
890 890
891 891 for rev, type, ctx, parents in dag:
892 892 char = formatnode(repo, ctx)
893 893 copies = None
894 if getrenamed and ctx.rev():
894 if getrenamed:
895 895 copies = []
896 896 for fn in ctx.files():
897 897 rename = getrenamed(fn, ctx.rev())
898 898 if rename:
899 899 copies.append((fn, rename))
900 900 edges = edgefn(type, char, state, rev, parents)
901 901 firstedge = next(edges)
902 902 width = firstedge[2]
903 903 displayer.show(ctx, copies=copies,
904 904 graphwidth=width, **pycompat.strkwargs(props))
905 905 lines = displayer.hunk.pop(rev).split('\n')
906 906 if not lines[-1]:
907 907 del lines[-1]
908 908 displayer.flush(ctx)
909 909 for type, char, width, coldata in itertools.chain([firstedge], edges):
910 910 graphmod.ascii(ui, state, type, char, lines, coldata)
911 911 lines = []
912 912 displayer.close()
913 913
914 914 def displaygraphrevs(ui, repo, revs, displayer, getrenamed):
915 915 revdag = graphmod.dagwalker(repo, revs)
916 916 displaygraph(ui, repo, revdag, displayer, graphmod.asciiedges, getrenamed)
917 917
918 918 def displayrevs(ui, repo, revs, displayer, getrenamed):
919 919 for rev in revs:
920 920 ctx = repo[rev]
921 921 copies = None
922 if getrenamed is not None and rev:
922 if getrenamed is not None:
923 923 copies = []
924 924 for fn in ctx.files():
925 925 rename = getrenamed(fn, rev)
926 926 if rename:
927 927 copies.append((fn, rename))
928 928 displayer.show(ctx, copies=copies)
929 929 displayer.flush(ctx)
930 930 displayer.close()
931 931
932 932 def checkunsupportedgraphflags(pats, opts):
933 933 for op in ["newest_first"]:
934 934 if op in opts and opts[op]:
935 935 raise error.Abort(_("-G/--graph option is incompatible with --%s")
936 936 % op.replace("_", "-"))
937 937
938 938 def graphrevs(repo, nodes, opts):
939 939 limit = getlimit(opts)
940 940 nodes.reverse()
941 941 if limit is not None:
942 942 nodes = nodes[:limit]
943 943 return graphmod.nodes(repo, nodes)
@@ -1,665 +1,665 b''
1 1 $ hg init repo1
2 2 $ cd repo1
3 3 $ mkdir a b a/1 b/1 b/2
4 4 $ touch in_root a/in_a b/in_b a/1/in_a_1 b/1/in_b_1 b/2/in_b_2
5 5
6 6 hg status in repo root:
7 7
8 8 $ hg status
9 9 ? a/1/in_a_1
10 10 ? a/in_a
11 11 ? b/1/in_b_1
12 12 ? b/2/in_b_2
13 13 ? b/in_b
14 14 ? in_root
15 15
16 16 hg status . in repo root:
17 17
18 18 $ hg status .
19 19 ? a/1/in_a_1
20 20 ? a/in_a
21 21 ? b/1/in_b_1
22 22 ? b/2/in_b_2
23 23 ? b/in_b
24 24 ? in_root
25 25
26 26 $ hg status --cwd a
27 27 ? a/1/in_a_1
28 28 ? a/in_a
29 29 ? b/1/in_b_1
30 30 ? b/2/in_b_2
31 31 ? b/in_b
32 32 ? in_root
33 33 $ hg status --cwd a .
34 34 ? 1/in_a_1
35 35 ? in_a
36 36 $ hg status --cwd a ..
37 37 ? 1/in_a_1
38 38 ? in_a
39 39 ? ../b/1/in_b_1
40 40 ? ../b/2/in_b_2
41 41 ? ../b/in_b
42 42 ? ../in_root
43 43
44 44 $ hg status --cwd b
45 45 ? a/1/in_a_1
46 46 ? a/in_a
47 47 ? b/1/in_b_1
48 48 ? b/2/in_b_2
49 49 ? b/in_b
50 50 ? in_root
51 51 $ hg status --cwd b .
52 52 ? 1/in_b_1
53 53 ? 2/in_b_2
54 54 ? in_b
55 55 $ hg status --cwd b ..
56 56 ? ../a/1/in_a_1
57 57 ? ../a/in_a
58 58 ? 1/in_b_1
59 59 ? 2/in_b_2
60 60 ? in_b
61 61 ? ../in_root
62 62
63 63 $ hg status --cwd a/1
64 64 ? a/1/in_a_1
65 65 ? a/in_a
66 66 ? b/1/in_b_1
67 67 ? b/2/in_b_2
68 68 ? b/in_b
69 69 ? in_root
70 70 $ hg status --cwd a/1 .
71 71 ? in_a_1
72 72 $ hg status --cwd a/1 ..
73 73 ? in_a_1
74 74 ? ../in_a
75 75
76 76 $ hg status --cwd b/1
77 77 ? a/1/in_a_1
78 78 ? a/in_a
79 79 ? b/1/in_b_1
80 80 ? b/2/in_b_2
81 81 ? b/in_b
82 82 ? in_root
83 83 $ hg status --cwd b/1 .
84 84 ? in_b_1
85 85 $ hg status --cwd b/1 ..
86 86 ? in_b_1
87 87 ? ../2/in_b_2
88 88 ? ../in_b
89 89
90 90 $ hg status --cwd b/2
91 91 ? a/1/in_a_1
92 92 ? a/in_a
93 93 ? b/1/in_b_1
94 94 ? b/2/in_b_2
95 95 ? b/in_b
96 96 ? in_root
97 97 $ hg status --cwd b/2 .
98 98 ? in_b_2
99 99 $ hg status --cwd b/2 ..
100 100 ? ../1/in_b_1
101 101 ? in_b_2
102 102 ? ../in_b
103 103
104 104 combining patterns with root and patterns without a root works
105 105
106 106 $ hg st a/in_a re:.*b$
107 107 ? a/in_a
108 108 ? b/in_b
109 109
110 110 tweaking defaults works
111 111 $ hg status --cwd a --config ui.tweakdefaults=yes
112 112 ? 1/in_a_1
113 113 ? in_a
114 114 ? ../b/1/in_b_1
115 115 ? ../b/2/in_b_2
116 116 ? ../b/in_b
117 117 ? ../in_root
118 118 $ HGPLAIN=1 hg status --cwd a --config ui.tweakdefaults=yes
119 119 ? a/1/in_a_1 (glob)
120 120 ? a/in_a (glob)
121 121 ? b/1/in_b_1 (glob)
122 122 ? b/2/in_b_2 (glob)
123 123 ? b/in_b (glob)
124 124 ? in_root
125 125 $ HGPLAINEXCEPT=tweakdefaults hg status --cwd a --config ui.tweakdefaults=yes
126 126 ? 1/in_a_1
127 127 ? in_a
128 128 ? ../b/1/in_b_1
129 129 ? ../b/2/in_b_2
130 130 ? ../b/in_b
131 131 ? ../in_root (glob)
132 132
133 133 relative paths can be requested
134 134
135 135 $ hg status --cwd a --config ui.relative-paths=yes
136 136 ? 1/in_a_1
137 137 ? in_a
138 138 ? ../b/1/in_b_1
139 139 ? ../b/2/in_b_2
140 140 ? ../b/in_b
141 141 ? ../in_root
142 142
143 143 $ hg status --cwd a . --config ui.relative-paths=legacy
144 144 ? 1/in_a_1
145 145 ? in_a
146 146 $ hg status --cwd a . --config ui.relative-paths=no
147 147 ? a/1/in_a_1
148 148 ? a/in_a
149 149
150 150 commands.status.relative overrides ui.relative-paths
151 151
152 152 $ cat >> $HGRCPATH <<EOF
153 153 > [ui]
154 154 > relative-paths = False
155 155 > [commands]
156 156 > status.relative = True
157 157 > EOF
158 158 $ hg status --cwd a
159 159 ? 1/in_a_1
160 160 ? in_a
161 161 ? ../b/1/in_b_1
162 162 ? ../b/2/in_b_2
163 163 ? ../b/in_b
164 164 ? ../in_root
165 165 $ HGPLAIN=1 hg status --cwd a
166 166 ? a/1/in_a_1 (glob)
167 167 ? a/in_a (glob)
168 168 ? b/1/in_b_1 (glob)
169 169 ? b/2/in_b_2 (glob)
170 170 ? b/in_b (glob)
171 171 ? in_root
172 172
173 173 if relative paths are explicitly off, tweakdefaults doesn't change it
174 174 $ cat >> $HGRCPATH <<EOF
175 175 > [commands]
176 176 > status.relative = False
177 177 > EOF
178 178 $ hg status --cwd a --config ui.tweakdefaults=yes
179 179 ? a/1/in_a_1
180 180 ? a/in_a
181 181 ? b/1/in_b_1
182 182 ? b/2/in_b_2
183 183 ? b/in_b
184 184 ? in_root
185 185
186 186 $ cd ..
187 187
188 188 $ hg init repo2
189 189 $ cd repo2
190 190 $ touch modified removed deleted ignored
191 191 $ echo "^ignored$" > .hgignore
192 192 $ hg ci -A -m 'initial checkin'
193 193 adding .hgignore
194 194 adding deleted
195 195 adding modified
196 196 adding removed
197 197 $ touch modified added unknown ignored
198 198 $ hg add added
199 199 $ hg remove removed
200 200 $ rm deleted
201 201
202 202 hg status:
203 203
204 204 $ hg status
205 205 A added
206 206 R removed
207 207 ! deleted
208 208 ? unknown
209 209
210 210 hg status modified added removed deleted unknown never-existed ignored:
211 211
212 212 $ hg status modified added removed deleted unknown never-existed ignored
213 213 never-existed: * (glob)
214 214 A added
215 215 R removed
216 216 ! deleted
217 217 ? unknown
218 218
219 219 $ hg copy modified copied
220 220
221 221 hg status -C:
222 222
223 223 $ hg status -C
224 224 A added
225 225 A copied
226 226 modified
227 227 R removed
228 228 ! deleted
229 229 ? unknown
230 230
231 231 hg status -A:
232 232
233 233 $ hg status -A
234 234 A added
235 235 A copied
236 236 modified
237 237 R removed
238 238 ! deleted
239 239 ? unknown
240 240 I ignored
241 241 C .hgignore
242 242 C modified
243 243
244 244 $ hg status -A -T '{status} {path} {node|shortest}\n'
245 245 A added ffff
246 246 A copied ffff
247 247 R removed ffff
248 248 ! deleted ffff
249 249 ? unknown ffff
250 250 I ignored ffff
251 251 C .hgignore ffff
252 252 C modified ffff
253 253
254 254 $ hg status -A -Tjson
255 255 [
256 256 {
257 257 "path": "added",
258 258 "status": "A"
259 259 },
260 260 {
261 261 "path": "copied",
262 262 "source": "modified",
263 263 "status": "A"
264 264 },
265 265 {
266 266 "path": "removed",
267 267 "status": "R"
268 268 },
269 269 {
270 270 "path": "deleted",
271 271 "status": "!"
272 272 },
273 273 {
274 274 "path": "unknown",
275 275 "status": "?"
276 276 },
277 277 {
278 278 "path": "ignored",
279 279 "status": "I"
280 280 },
281 281 {
282 282 "path": ".hgignore",
283 283 "status": "C"
284 284 },
285 285 {
286 286 "path": "modified",
287 287 "status": "C"
288 288 }
289 289 ]
290 290
291 291 $ hg status -A -Tpickle > pickle
292 292 >>> from __future__ import print_function
293 293 >>> from mercurial import util
294 294 >>> pickle = util.pickle
295 295 >>> data = sorted((x[b'status'].decode(), x[b'path'].decode()) for x in pickle.load(open("pickle", r"rb")))
296 296 >>> for s, p in data: print("%s %s" % (s, p))
297 297 ! deleted
298 298 ? pickle
299 299 ? unknown
300 300 A added
301 301 A copied
302 302 C .hgignore
303 303 C modified
304 304 I ignored
305 305 R removed
306 306 $ rm pickle
307 307
308 308 $ echo "^ignoreddir$" > .hgignore
309 309 $ mkdir ignoreddir
310 310 $ touch ignoreddir/file
311 311
312 312 Test templater support:
313 313
314 314 $ hg status -AT "[{status}]\t{if(source, '{source} -> ')}{path}\n"
315 315 [M] .hgignore
316 316 [A] added
317 317 [A] modified -> copied
318 318 [R] removed
319 319 [!] deleted
320 320 [?] ignored
321 321 [?] unknown
322 322 [I] ignoreddir/file
323 323 [C] modified
324 324 $ hg status -AT default
325 325 M .hgignore
326 326 A added
327 327 A copied
328 328 modified
329 329 R removed
330 330 ! deleted
331 331 ? ignored
332 332 ? unknown
333 333 I ignoreddir/file
334 334 C modified
335 335 $ hg status -T compact
336 336 abort: "status" not in template map
337 337 [255]
338 338
339 339 hg status ignoreddir/file:
340 340
341 341 $ hg status ignoreddir/file
342 342
343 343 hg status -i ignoreddir/file:
344 344
345 345 $ hg status -i ignoreddir/file
346 346 I ignoreddir/file
347 347 $ cd ..
348 348
349 349 Check 'status -q' and some combinations
350 350
351 351 $ hg init repo3
352 352 $ cd repo3
353 353 $ touch modified removed deleted ignored
354 354 $ echo "^ignored$" > .hgignore
355 355 $ hg commit -A -m 'initial checkin'
356 356 adding .hgignore
357 357 adding deleted
358 358 adding modified
359 359 adding removed
360 360 $ touch added unknown ignored
361 361 $ hg add added
362 362 $ echo "test" >> modified
363 363 $ hg remove removed
364 364 $ rm deleted
365 365 $ hg copy modified copied
366 366
367 367 Specify working directory revision explicitly, that should be the same as
368 368 "hg status"
369 369
370 370 $ hg status --change "wdir()"
371 371 M modified
372 372 A added
373 373 A copied
374 374 R removed
375 375 ! deleted
376 376 ? unknown
377 377
378 378 Run status with 2 different flags.
379 379 Check if result is the same or different.
380 380 If result is not as expected, raise error
381 381
382 382 $ assert() {
383 383 > hg status $1 > ../a
384 384 > hg status $2 > ../b
385 385 > if diff ../a ../b > /dev/null; then
386 386 > out=0
387 387 > else
388 388 > out=1
389 389 > fi
390 390 > if [ $3 -eq 0 ]; then
391 391 > df="same"
392 392 > else
393 393 > df="different"
394 394 > fi
395 395 > if [ $out -ne $3 ]; then
396 396 > echo "Error on $1 and $2, should be $df."
397 397 > fi
398 398 > }
399 399
400 400 Assert flag1 flag2 [0-same | 1-different]
401 401
402 402 $ assert "-q" "-mard" 0
403 403 $ assert "-A" "-marduicC" 0
404 404 $ assert "-qA" "-mardcC" 0
405 405 $ assert "-qAui" "-A" 0
406 406 $ assert "-qAu" "-marducC" 0
407 407 $ assert "-qAi" "-mardicC" 0
408 408 $ assert "-qu" "-u" 0
409 409 $ assert "-q" "-u" 1
410 410 $ assert "-m" "-a" 1
411 411 $ assert "-r" "-d" 1
412 412 $ cd ..
413 413
414 414 $ hg init repo4
415 415 $ cd repo4
416 416 $ touch modified removed deleted
417 417 $ hg ci -q -A -m 'initial checkin'
418 418 $ touch added unknown
419 419 $ hg add added
420 420 $ hg remove removed
421 421 $ rm deleted
422 422 $ echo x > modified
423 423 $ hg copy modified copied
424 424 $ hg ci -m 'test checkin' -d "1000001 0"
425 425 $ rm *
426 426 $ touch unrelated
427 427 $ hg ci -q -A -m 'unrelated checkin' -d "1000002 0"
428 428
429 429 hg status --change 1:
430 430
431 431 $ hg status --change 1
432 432 M modified
433 433 A added
434 434 A copied
435 435 R removed
436 436
437 437 hg status --change 1 unrelated:
438 438
439 439 $ hg status --change 1 unrelated
440 440
441 441 hg status -C --change 1 added modified copied removed deleted:
442 442
443 443 $ hg status -C --change 1 added modified copied removed deleted
444 444 M modified
445 445 A added
446 446 A copied
447 447 modified
448 448 R removed
449 449
450 450 hg status -A --change 1 and revset:
451 451
452 452 $ hg status -A --change '1|1'
453 453 M modified
454 454 A added
455 455 A copied
456 456 modified
457 457 R removed
458 458 C deleted
459 459
460 460 $ cd ..
461 461
462 462 hg status with --rev and reverted changes:
463 463
464 464 $ hg init reverted-changes-repo
465 465 $ cd reverted-changes-repo
466 466 $ echo a > file
467 467 $ hg add file
468 468 $ hg ci -m a
469 469 $ echo b > file
470 470 $ hg ci -m b
471 471
472 472 reverted file should appear clean
473 473
474 474 $ hg revert -r 0 .
475 475 reverting file
476 476 $ hg status -A --rev 0
477 477 C file
478 478
479 479 #if execbit
480 480 reverted file with changed flag should appear modified
481 481
482 482 $ chmod +x file
483 483 $ hg status -A --rev 0
484 484 M file
485 485
486 486 $ hg revert -r 0 .
487 487 reverting file
488 488
489 489 reverted and committed file with changed flag should appear modified
490 490
491 491 $ hg co -C .
492 492 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
493 493 $ chmod +x file
494 494 $ hg ci -m 'change flag'
495 495 $ hg status -A --rev 1 --rev 2
496 496 M file
497 497 $ hg diff -r 1 -r 2
498 498
499 499 #endif
500 500
501 501 $ cd ..
502 502
503 503 hg status of binary file starting with '\1\n', a separator for metadata:
504 504
505 505 $ hg init repo5
506 506 $ cd repo5
507 507 >>> open("010a", r"wb").write(b"\1\nfoo") and None
508 508 $ hg ci -q -A -m 'initial checkin'
509 509 $ hg status -A
510 510 C 010a
511 511
512 512 >>> open("010a", r"wb").write(b"\1\nbar") and None
513 513 $ hg status -A
514 514 M 010a
515 515 $ hg ci -q -m 'modify 010a'
516 516 $ hg status -A --rev 0:1
517 517 M 010a
518 518
519 519 $ touch empty
520 520 $ hg ci -q -A -m 'add another file'
521 521 $ hg status -A --rev 1:2 010a
522 522 C 010a
523 523
524 524 $ cd ..
525 525
526 526 test "hg status" with "directory pattern" which matches against files
527 527 only known on target revision.
528 528
529 529 $ hg init repo6
530 530 $ cd repo6
531 531
532 532 $ echo a > a.txt
533 533 $ hg add a.txt
534 534 $ hg commit -m '#0'
535 535 $ mkdir -p 1/2/3/4/5
536 536 $ echo b > 1/2/3/4/5/b.txt
537 537 $ hg add 1/2/3/4/5/b.txt
538 538 $ hg commit -m '#1'
539 539
540 540 $ hg update -C 0 > /dev/null
541 541 $ hg status -A
542 542 C a.txt
543 543
544 544 the directory matching against specified pattern should be removed,
545 545 because directory existence prevents 'dirstate.walk()' from showing
546 546 warning message about such pattern.
547 547
548 548 $ test ! -d 1
549 549 $ hg status -A --rev 1 1/2/3/4/5/b.txt
550 550 R 1/2/3/4/5/b.txt
551 551 $ hg status -A --rev 1 1/2/3/4/5
552 552 R 1/2/3/4/5/b.txt
553 553 $ hg status -A --rev 1 1/2/3
554 554 R 1/2/3/4/5/b.txt
555 555 $ hg status -A --rev 1 1
556 556 R 1/2/3/4/5/b.txt
557 557
558 558 $ hg status --config ui.formatdebug=True --rev 1 1
559 559 status = [
560 560 {
561 561 'path': '1/2/3/4/5/b.txt',
562 562 'status': 'R'
563 563 },
564 564 ]
565 565
566 566 #if windows
567 567 $ hg --config ui.slash=false status -A --rev 1 1
568 568 R 1\2\3\4\5\b.txt
569 569 #endif
570 570
571 571 $ cd ..
572 572
573 573 Status after move overwriting a file (issue4458)
574 574 =================================================
575 575
576 576
577 577 $ hg init issue4458
578 578 $ cd issue4458
579 579 $ echo a > a
580 580 $ echo b > b
581 581 $ hg commit -Am base
582 582 adding a
583 583 adding b
584 584
585 585
586 586 with --force
587 587
588 588 $ hg mv b --force a
589 589 $ hg st --copies
590 590 M a
591 591 b
592 592 R b
593 593 $ hg revert --all
594 594 reverting a
595 595 undeleting b
596 596 $ rm *.orig
597 597
598 598 without force
599 599
600 600 $ hg rm a
601 601 $ hg st --copies
602 602 R a
603 603 $ hg mv b a
604 604 $ hg st --copies
605 605 M a
606 606 b
607 607 R b
608 608
609 609 using ui.statuscopies setting
610 610 $ hg st --config ui.statuscopies=true
611 611 M a
612 612 b
613 613 R b
614 614 $ hg st --config ui.statuscopies=false
615 615 M a
616 616 R b
617 617 $ hg st --config ui.tweakdefaults=yes
618 618 M a
619 619 b
620 620 R b
621 621
622 622 using log status template (issue5155)
623 623 $ hg log -Tstatus -r 'wdir()' -C
624 624 changeset: 2147483647:ffffffffffff
625 625 parent: 0:8c55c58b4c0e
626 626 user: test
627 627 date: * (glob)
628 628 files:
629 629 M a
630 630 b
631 631 R b
632 632
633 BROKEN: as above, 'a' should be marked a copy
634 633 $ hg log -GTstatus -r 'wdir()' -C
635 634 o changeset: 2147483647:ffffffffffff
636 635 | parent: 0:8c55c58b4c0e
637 636 ~ user: test
638 637 date: * (glob)
639 638 files:
640 639 M a
640 b
641 641 R b
642 642
643 643
644 644 Other "bug" highlight, the revision status does not report the copy information.
645 645 This is buggy behavior.
646 646
647 647 $ hg commit -m 'blah'
648 648 $ hg st --copies --change .
649 649 M a
650 650 R b
651 651
652 652 using log status template, the copy information is displayed correctly.
653 653 $ hg log -Tstatus -r. -C
654 654 changeset: 1:6685fde43d21
655 655 tag: tip
656 656 user: test
657 657 date: * (glob)
658 658 summary: blah
659 659 files:
660 660 M a
661 661 b
662 662 R b
663 663
664 664
665 665 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now