##// END OF EJS Templates
fix: include cleanupnodes() in transaction...
Martin von Zweigbergk -
r38439:c1f4364f default
parent child Browse files
Show More
@@ -1,553 +1,553
1 1 # fix - rewrite file content in changesets and working copy
2 2 #
3 3 # Copyright 2018 Google LLC.
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 """rewrite file content in changesets or working copy (EXPERIMENTAL)
8 8
9 9 Provides a command that runs configured tools on the contents of modified files,
10 10 writing back any fixes to the working copy or replacing changesets.
11 11
12 12 Here is an example configuration that causes :hg:`fix` to apply automatic
13 13 formatting fixes to modified lines in C++ code::
14 14
15 15 [fix]
16 16 clang-format:command=clang-format --assume-filename={rootpath}
17 17 clang-format:linerange=--lines={first}:{last}
18 18 clang-format:fileset=set:**.cpp or **.hpp
19 19
20 20 The :command suboption forms the first part of the shell command that will be
21 21 used to fix a file. The content of the file is passed on standard input, and the
22 22 fixed file content is expected on standard output. If there is any output on
23 23 standard error, the file will not be affected. Some values may be substituted
24 24 into the command::
25 25
26 26 {rootpath} The path of the file being fixed, relative to the repo root
27 27 {basename} The name of the file being fixed, without the directory path
28 28
29 29 If the :linerange suboption is set, the tool will only be run if there are
30 30 changed lines in a file. The value of this suboption is appended to the shell
31 31 command once for every range of changed lines in the file. Some values may be
32 32 substituted into the command::
33 33
34 34 {first} The 1-based line number of the first line in the modified range
35 35 {last} The 1-based line number of the last line in the modified range
36 36
37 37 The :fileset suboption determines which files will be passed through each
38 38 configured tool. See :hg:`help fileset` for possible values. If there are file
39 39 arguments to :hg:`fix`, the intersection of these filesets is used.
40 40
41 41 There is also a configurable limit for the maximum size of file that will be
42 42 processed by :hg:`fix`::
43 43
44 44 [fix]
45 45 maxfilesize=2MB
46 46
47 47 """
48 48
49 49 from __future__ import absolute_import
50 50
51 51 import collections
52 52 import itertools
53 53 import os
54 54 import re
55 55 import subprocess
56 56
57 57 from mercurial.i18n import _
58 58 from mercurial.node import nullrev
59 59 from mercurial.node import wdirrev
60 60
61 61 from mercurial import (
62 62 cmdutil,
63 63 context,
64 64 copies,
65 65 error,
66 66 mdiff,
67 67 merge,
68 68 obsolete,
69 69 pycompat,
70 70 registrar,
71 71 scmutil,
72 72 util,
73 73 )
74 74
75 75 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
76 76 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
77 77 # be specifying the version(s) of Mercurial they are tested with, or
78 78 # leave the attribute unspecified.
79 79 testedwith = 'ships-with-hg-core'
80 80
81 81 cmdtable = {}
82 82 command = registrar.command(cmdtable)
83 83
84 84 configtable = {}
85 85 configitem = registrar.configitem(configtable)
86 86
87 87 # Register the suboptions allowed for each configured fixer.
88 88 FIXER_ATTRS = ('command', 'linerange', 'fileset')
89 89
90 90 for key in FIXER_ATTRS:
91 91 configitem('fix', '.*(:%s)?' % key, default=None, generic=True)
92 92
93 93 # A good default size allows most source code files to be fixed, but avoids
94 94 # letting fixer tools choke on huge inputs, which could be surprising to the
95 95 # user.
96 96 configitem('fix', 'maxfilesize', default='2MB')
97 97
98 98 @command('fix',
99 99 [('', 'all', False, _('fix all non-public non-obsolete revisions')),
100 100 ('', 'base', [], _('revisions to diff against (overrides automatic '
101 101 'selection, and applies to every revision being '
102 102 'fixed)'), _('REV')),
103 103 ('r', 'rev', [], _('revisions to fix'), _('REV')),
104 104 ('w', 'working-dir', False, _('fix the working directory')),
105 105 ('', 'whole', False, _('always fix every line of a file'))],
106 106 _('[OPTION]... [FILE]...'))
107 107 def fix(ui, repo, *pats, **opts):
108 108 """rewrite file content in changesets or working directory
109 109
110 110 Runs any configured tools to fix the content of files. Only affects files
111 111 with changes, unless file arguments are provided. Only affects changed lines
112 112 of files, unless the --whole flag is used. Some tools may always affect the
113 113 whole file regardless of --whole.
114 114
115 115 If revisions are specified with --rev, those revisions will be checked, and
116 116 they may be replaced with new revisions that have fixed file content. It is
117 117 desirable to specify all descendants of each specified revision, so that the
118 118 fixes propagate to the descendants. If all descendants are fixed at the same
119 119 time, no merging, rebasing, or evolution will be required.
120 120
121 121 If --working-dir is used, files with uncommitted changes in the working copy
122 122 will be fixed. If the checked-out revision is also fixed, the working
123 123 directory will update to the replacement revision.
124 124
125 125 When determining what lines of each file to fix at each revision, the whole
126 126 set of revisions being fixed is considered, so that fixes to earlier
127 127 revisions are not forgotten in later ones. The --base flag can be used to
128 128 override this default behavior, though it is not usually desirable to do so.
129 129 """
130 130 opts = pycompat.byteskwargs(opts)
131 131 if opts['all']:
132 132 if opts['rev']:
133 133 raise error.Abort(_('cannot specify both "--rev" and "--all"'))
134 134 opts['rev'] = ['not public() and not obsolete()']
135 135 opts['working_dir'] = True
136 with repo.wlock(), repo.lock():
136 with repo.wlock(), repo.lock(), repo.transaction('fix'):
137 137 revstofix = getrevstofix(ui, repo, opts)
138 138 basectxs = getbasectxs(repo, opts, revstofix)
139 139 workqueue, numitems = getworkqueue(ui, repo, pats, opts, revstofix,
140 140 basectxs)
141 141 filedata = collections.defaultdict(dict)
142 142 replacements = {}
143 143 fixers = getfixers(ui)
144 144 # Some day this loop can become a worker pool, but for now it's easier
145 145 # to fix everything serially in topological order.
146 146 for rev, path in sorted(workqueue):
147 147 ctx = repo[rev]
148 148 olddata = ctx[path].data()
149 149 newdata = fixfile(ui, opts, fixers, ctx, path, basectxs[rev])
150 150 if newdata != olddata:
151 151 filedata[rev][path] = newdata
152 152 numitems[rev] -= 1
153 153 if not numitems[rev]:
154 154 if rev == wdirrev:
155 155 writeworkingdir(repo, ctx, filedata[rev], replacements)
156 156 else:
157 157 replacerev(ui, repo, ctx, filedata[rev], replacements)
158 158 del filedata[rev]
159 159
160 160 replacements = {prec: [succ] for prec, succ in replacements.iteritems()}
161 161 scmutil.cleanupnodes(repo, replacements, 'fix')
162 162
163 163 def getworkqueue(ui, repo, pats, opts, revstofix, basectxs):
164 164 """"Constructs the list of files to be fixed at specific revisions
165 165
166 166 It is up to the caller how to consume the work items, and the only
167 167 dependence between them is that replacement revisions must be committed in
168 168 topological order. Each work item represents a file in the working copy or
169 169 in some revision that should be fixed and written back to the working copy
170 170 or into a replacement revision.
171 171 """
172 172 workqueue = []
173 173 numitems = collections.defaultdict(int)
174 174 maxfilesize = ui.configbytes('fix', 'maxfilesize')
175 175 for rev in revstofix:
176 176 fixctx = repo[rev]
177 177 match = scmutil.match(fixctx, pats, opts)
178 178 for path in pathstofix(ui, repo, pats, opts, match, basectxs[rev],
179 179 fixctx):
180 180 if path not in fixctx:
181 181 continue
182 182 fctx = fixctx[path]
183 183 if fctx.islink():
184 184 continue
185 185 if fctx.size() > maxfilesize:
186 186 ui.warn(_('ignoring file larger than %s: %s\n') %
187 187 (util.bytecount(maxfilesize), path))
188 188 continue
189 189 workqueue.append((rev, path))
190 190 numitems[rev] += 1
191 191 return workqueue, numitems
192 192
193 193 def getrevstofix(ui, repo, opts):
194 194 """Returns the set of revision numbers that should be fixed"""
195 195 revs = set(scmutil.revrange(repo, opts['rev']))
196 196 for rev in revs:
197 197 checkfixablectx(ui, repo, repo[rev])
198 198 if revs:
199 199 cmdutil.checkunfinished(repo)
200 200 checknodescendants(repo, revs)
201 201 if opts.get('working_dir'):
202 202 revs.add(wdirrev)
203 203 if list(merge.mergestate.read(repo).unresolved()):
204 204 raise error.Abort('unresolved conflicts', hint="use 'hg resolve'")
205 205 if not revs:
206 206 raise error.Abort(
207 207 'no changesets specified', hint='use --rev or --working-dir')
208 208 return revs
209 209
210 210 def checknodescendants(repo, revs):
211 211 if (not obsolete.isenabled(repo, obsolete.allowunstableopt) and
212 212 repo.revs('(%ld::) - (%ld)', revs, revs)):
213 213 raise error.Abort(_('can only fix a changeset together '
214 214 'with all its descendants'))
215 215
216 216 def checkfixablectx(ui, repo, ctx):
217 217 """Aborts if the revision shouldn't be replaced with a fixed one."""
218 218 if not ctx.mutable():
219 219 raise error.Abort('can\'t fix immutable changeset %s' %
220 220 (scmutil.formatchangeid(ctx),))
221 221 if ctx.obsolete():
222 222 # It would be better to actually check if the revision has a successor.
223 223 allowdivergence = ui.configbool('experimental',
224 224 'evolution.allowdivergence')
225 225 if not allowdivergence:
226 226 raise error.Abort('fixing obsolete revision could cause divergence')
227 227
228 228 def pathstofix(ui, repo, pats, opts, match, basectxs, fixctx):
229 229 """Returns the set of files that should be fixed in a context
230 230
231 231 The result depends on the base contexts; we include any file that has
232 232 changed relative to any of the base contexts. Base contexts should be
233 233 ancestors of the context being fixed.
234 234 """
235 235 files = set()
236 236 for basectx in basectxs:
237 237 stat = repo.status(
238 238 basectx, fixctx, match=match, clean=bool(pats), unknown=bool(pats))
239 239 files.update(
240 240 set(itertools.chain(stat.added, stat.modified, stat.clean,
241 241 stat.unknown)))
242 242 return files
243 243
244 244 def lineranges(opts, path, basectxs, fixctx, content2):
245 245 """Returns the set of line ranges that should be fixed in a file
246 246
247 247 Of the form [(10, 20), (30, 40)].
248 248
249 249 This depends on the given base contexts; we must consider lines that have
250 250 changed versus any of the base contexts, and whether the file has been
251 251 renamed versus any of them.
252 252
253 253 Another way to understand this is that we exclude line ranges that are
254 254 common to the file in all base contexts.
255 255 """
256 256 if opts.get('whole'):
257 257 # Return a range containing all lines. Rely on the diff implementation's
258 258 # idea of how many lines are in the file, instead of reimplementing it.
259 259 return difflineranges('', content2)
260 260
261 261 rangeslist = []
262 262 for basectx in basectxs:
263 263 basepath = copies.pathcopies(basectx, fixctx).get(path, path)
264 264 if basepath in basectx:
265 265 content1 = basectx[basepath].data()
266 266 else:
267 267 content1 = ''
268 268 rangeslist.extend(difflineranges(content1, content2))
269 269 return unionranges(rangeslist)
270 270
271 271 def unionranges(rangeslist):
272 272 """Return the union of some closed intervals
273 273
274 274 >>> unionranges([])
275 275 []
276 276 >>> unionranges([(1, 100)])
277 277 [(1, 100)]
278 278 >>> unionranges([(1, 100), (1, 100)])
279 279 [(1, 100)]
280 280 >>> unionranges([(1, 100), (2, 100)])
281 281 [(1, 100)]
282 282 >>> unionranges([(1, 99), (1, 100)])
283 283 [(1, 100)]
284 284 >>> unionranges([(1, 100), (40, 60)])
285 285 [(1, 100)]
286 286 >>> unionranges([(1, 49), (50, 100)])
287 287 [(1, 100)]
288 288 >>> unionranges([(1, 48), (50, 100)])
289 289 [(1, 48), (50, 100)]
290 290 >>> unionranges([(1, 2), (3, 4), (5, 6)])
291 291 [(1, 6)]
292 292 """
293 293 rangeslist = sorted(set(rangeslist))
294 294 unioned = []
295 295 if rangeslist:
296 296 unioned, rangeslist = [rangeslist[0]], rangeslist[1:]
297 297 for a, b in rangeslist:
298 298 c, d = unioned[-1]
299 299 if a > d + 1:
300 300 unioned.append((a, b))
301 301 else:
302 302 unioned[-1] = (c, max(b, d))
303 303 return unioned
304 304
305 305 def difflineranges(content1, content2):
306 306 """Return list of line number ranges in content2 that differ from content1.
307 307
308 308 Line numbers are 1-based. The numbers are the first and last line contained
309 309 in the range. Single-line ranges have the same line number for the first and
310 310 last line. Excludes any empty ranges that result from lines that are only
311 311 present in content1. Relies on mdiff's idea of where the line endings are in
312 312 the string.
313 313
314 314 >>> from mercurial import pycompat
315 315 >>> lines = lambda s: b'\\n'.join([c for c in pycompat.iterbytestr(s)])
316 316 >>> difflineranges2 = lambda a, b: difflineranges(lines(a), lines(b))
317 317 >>> difflineranges2(b'', b'')
318 318 []
319 319 >>> difflineranges2(b'a', b'')
320 320 []
321 321 >>> difflineranges2(b'', b'A')
322 322 [(1, 1)]
323 323 >>> difflineranges2(b'a', b'a')
324 324 []
325 325 >>> difflineranges2(b'a', b'A')
326 326 [(1, 1)]
327 327 >>> difflineranges2(b'ab', b'')
328 328 []
329 329 >>> difflineranges2(b'', b'AB')
330 330 [(1, 2)]
331 331 >>> difflineranges2(b'abc', b'ac')
332 332 []
333 333 >>> difflineranges2(b'ab', b'aCb')
334 334 [(2, 2)]
335 335 >>> difflineranges2(b'abc', b'aBc')
336 336 [(2, 2)]
337 337 >>> difflineranges2(b'ab', b'AB')
338 338 [(1, 2)]
339 339 >>> difflineranges2(b'abcde', b'aBcDe')
340 340 [(2, 2), (4, 4)]
341 341 >>> difflineranges2(b'abcde', b'aBCDe')
342 342 [(2, 4)]
343 343 """
344 344 ranges = []
345 345 for lines, kind in mdiff.allblocks(content1, content2):
346 346 firstline, lastline = lines[2:4]
347 347 if kind == '!' and firstline != lastline:
348 348 ranges.append((firstline + 1, lastline))
349 349 return ranges
350 350
351 351 def getbasectxs(repo, opts, revstofix):
352 352 """Returns a map of the base contexts for each revision
353 353
354 354 The base contexts determine which lines are considered modified when we
355 355 attempt to fix just the modified lines in a file.
356 356 """
357 357 # The --base flag overrides the usual logic, and we give every revision
358 358 # exactly the set of baserevs that the user specified.
359 359 if opts.get('base'):
360 360 baserevs = set(scmutil.revrange(repo, opts.get('base')))
361 361 if not baserevs:
362 362 baserevs = {nullrev}
363 363 basectxs = {repo[rev] for rev in baserevs}
364 364 return {rev: basectxs for rev in revstofix}
365 365
366 366 # Proceed in topological order so that we can easily determine each
367 367 # revision's baserevs by looking at its parents and their baserevs.
368 368 basectxs = collections.defaultdict(set)
369 369 for rev in sorted(revstofix):
370 370 ctx = repo[rev]
371 371 for pctx in ctx.parents():
372 372 if pctx.rev() in basectxs:
373 373 basectxs[rev].update(basectxs[pctx.rev()])
374 374 else:
375 375 basectxs[rev].add(pctx)
376 376 return basectxs
377 377
378 378 def fixfile(ui, opts, fixers, fixctx, path, basectxs):
379 379 """Run any configured fixers that should affect the file in this context
380 380
381 381 Returns the file content that results from applying the fixers in some order
382 382 starting with the file's content in the fixctx. Fixers that support line
383 383 ranges will affect lines that have changed relative to any of the basectxs
384 384 (i.e. they will only avoid lines that are common to all basectxs).
385 385 """
386 386 newdata = fixctx[path].data()
387 387 for fixername, fixer in fixers.iteritems():
388 388 if fixer.affects(opts, fixctx, path):
389 389 ranges = lineranges(opts, path, basectxs, fixctx, newdata)
390 390 command = fixer.command(ui, path, ranges)
391 391 if command is None:
392 392 continue
393 393 ui.debug('subprocess: %s\n' % (command,))
394 394 proc = subprocess.Popen(
395 395 command,
396 396 shell=True,
397 397 cwd='/',
398 398 stdin=subprocess.PIPE,
399 399 stdout=subprocess.PIPE,
400 400 stderr=subprocess.PIPE)
401 401 newerdata, stderr = proc.communicate(newdata)
402 402 if stderr:
403 403 showstderr(ui, fixctx.rev(), fixername, stderr)
404 404 else:
405 405 newdata = newerdata
406 406 return newdata
407 407
408 408 def showstderr(ui, rev, fixername, stderr):
409 409 """Writes the lines of the stderr string as warnings on the ui
410 410
411 411 Uses the revision number and fixername to give more context to each line of
412 412 the error message. Doesn't include file names, since those take up a lot of
413 413 space and would tend to be included in the error message if they were
414 414 relevant.
415 415 """
416 416 for line in re.split('[\r\n]+', stderr):
417 417 if line:
418 418 ui.warn(('['))
419 419 if rev is None:
420 420 ui.warn(_('wdir'), label='evolve.rev')
421 421 else:
422 422 ui.warn((str(rev)), label='evolve.rev')
423 423 ui.warn(('] %s: %s\n') % (fixername, line))
424 424
425 425 def writeworkingdir(repo, ctx, filedata, replacements):
426 426 """Write new content to the working copy and check out the new p1 if any
427 427
428 428 We check out a new revision if and only if we fixed something in both the
429 429 working directory and its parent revision. This avoids the need for a full
430 430 update/merge, and means that the working directory simply isn't affected
431 431 unless the --working-dir flag is given.
432 432
433 433 Directly updates the dirstate for the affected files.
434 434 """
435 435 for path, data in filedata.iteritems():
436 436 fctx = ctx[path]
437 437 fctx.write(data, fctx.flags())
438 438 if repo.dirstate[path] == 'n':
439 439 repo.dirstate.normallookup(path)
440 440
441 441 oldparentnodes = repo.dirstate.parents()
442 442 newparentnodes = [replacements.get(n, n) for n in oldparentnodes]
443 443 if newparentnodes != oldparentnodes:
444 444 repo.setparents(*newparentnodes)
445 445
446 446 def replacerev(ui, repo, ctx, filedata, replacements):
447 447 """Commit a new revision like the given one, but with file content changes
448 448
449 449 "ctx" is the original revision to be replaced by a modified one.
450 450
451 451 "filedata" is a dict that maps paths to their new file content. All other
452 452 paths will be recreated from the original revision without changes.
453 453 "filedata" may contain paths that didn't exist in the original revision;
454 454 they will be added.
455 455
456 456 "replacements" is a dict that maps a single node to a single node, and it is
457 457 updated to indicate the original revision is replaced by the newly created
458 458 one. No entry is added if the replacement's node already exists.
459 459
460 460 The new revision has the same parents as the old one, unless those parents
461 461 have already been replaced, in which case those replacements are the parents
462 462 of this new revision. Thus, if revisions are replaced in topological order,
463 463 there is no need to rebase them into the original topology later.
464 464 """
465 465
466 466 p1rev, p2rev = repo.changelog.parentrevs(ctx.rev())
467 467 p1ctx, p2ctx = repo[p1rev], repo[p2rev]
468 468 newp1node = replacements.get(p1ctx.node(), p1ctx.node())
469 469 newp2node = replacements.get(p2ctx.node(), p2ctx.node())
470 470
471 471 def filectxfn(repo, memctx, path):
472 472 if path not in ctx:
473 473 return None
474 474 fctx = ctx[path]
475 475 copied = fctx.renamed()
476 476 if copied:
477 477 copied = copied[0]
478 478 return context.memfilectx(
479 479 repo,
480 480 memctx,
481 481 path=fctx.path(),
482 482 data=filedata.get(path, fctx.data()),
483 483 islink=fctx.islink(),
484 484 isexec=fctx.isexec(),
485 485 copied=copied)
486 486
487 487 overrides = {('phases', 'new-commit'): ctx.phase()}
488 488 with ui.configoverride(overrides, source='fix'):
489 489 memctx = context.memctx(
490 490 repo,
491 491 parents=(newp1node, newp2node),
492 492 text=ctx.description(),
493 493 files=set(ctx.files()) | set(filedata.keys()),
494 494 filectxfn=filectxfn,
495 495 user=ctx.user(),
496 496 date=ctx.date(),
497 497 extra=ctx.extra(),
498 498 branch=ctx.branch(),
499 499 editor=None)
500 500 sucnode = memctx.commit()
501 501 prenode = ctx.node()
502 502 if prenode == sucnode:
503 503 ui.debug('node %s already existed\n' % (ctx.hex()))
504 504 else:
505 505 replacements[ctx.node()] = sucnode
506 506
507 507 def getfixers(ui):
508 508 """Returns a map of configured fixer tools indexed by their names
509 509
510 510 Each value is a Fixer object with methods that implement the behavior of the
511 511 fixer's config suboptions. Does not validate the config values.
512 512 """
513 513 result = {}
514 514 for name in fixernames(ui):
515 515 result[name] = Fixer()
516 516 attrs = ui.configsuboptions('fix', name)[1]
517 517 for key in FIXER_ATTRS:
518 518 setattr(result[name], pycompat.sysstr('_' + key),
519 519 attrs.get(key, ''))
520 520 return result
521 521
522 522 def fixernames(ui):
523 523 """Returns the names of [fix] config options that have suboptions"""
524 524 names = set()
525 525 for k, v in ui.configitems('fix'):
526 526 if ':' in k:
527 527 names.add(k.split(':', 1)[0])
528 528 return names
529 529
530 530 class Fixer(object):
531 531 """Wraps the raw config values for a fixer with methods"""
532 532
533 533 def affects(self, opts, fixctx, path):
534 534 """Should this fixer run on the file at the given path and context?"""
535 535 return scmutil.match(fixctx, [self._fileset], opts)(path)
536 536
537 537 def command(self, ui, path, ranges):
538 538 """A shell command to use to invoke this fixer on the given file/lines
539 539
540 540 May return None if there is no appropriate command to run for the given
541 541 parameters.
542 542 """
543 543 expand = cmdutil.rendercommandtemplate
544 544 parts = [expand(ui, self._command,
545 545 {'rootpath': path, 'basename': os.path.basename(path)})]
546 546 if self._linerange:
547 547 if not ranges:
548 548 # No line ranges to fix, so don't run the fixer.
549 549 return None
550 550 for first, last in ranges:
551 551 parts.append(expand(ui, self._linerange,
552 552 {'first': first, 'last': last}))
553 553 return ' '.join(parts)
@@ -1,417 +1,416
1 1 A script that implements uppercasing all letters in a file.
2 2
3 3 $ UPPERCASEPY="$TESTTMP/uppercase.py"
4 4 $ cat > $UPPERCASEPY <<EOF
5 5 > import sys
6 6 > from mercurial.utils.procutil import setbinary
7 7 > setbinary(sys.stdin)
8 8 > setbinary(sys.stdout)
9 9 > sys.stdout.write(sys.stdin.read().upper())
10 10 > EOF
11 11 $ TESTLINES="foo\nbar\nbaz\n"
12 12 $ printf $TESTLINES | $PYTHON $UPPERCASEPY
13 13 FOO
14 14 BAR
15 15 BAZ
16 16
17 17 Tests for the fix extension's behavior around non-trivial history topologies.
18 18 Looks for correct incremental fixing and reproduction of parent/child
19 19 relationships. We indicate fixed file content by uppercasing it.
20 20
21 21 $ cat >> $HGRCPATH <<EOF
22 22 > [extensions]
23 23 > fix =
24 24 > [fix]
25 25 > uppercase-whole-file:command=$PYTHON $UPPERCASEPY
26 26 > uppercase-whole-file:fileset=set:**
27 27 > EOF
28 28
29 29 This tests the only behavior that should really be affected by obsolescence, so
30 30 we'll test it with evolution off and on. This only changes the revision
31 31 numbers, if all is well.
32 32
33 33 #testcases obsstore-off obsstore-on
34 34 #if obsstore-on
35 35 $ cat >> $HGRCPATH <<EOF
36 36 > [experimental]
37 37 > evolution.createmarkers=True
38 38 > evolution.allowunstable=True
39 39 > EOF
40 40 #endif
41 41
42 42 Setting up the test topology. Scroll down to see the graph produced. We make it
43 43 clear which files were modified in each revision. It's enough to test at the
44 44 file granularity, because that demonstrates which baserevs were diffed against.
45 45 The computation of changed lines is orthogonal and tested separately.
46 46
47 47 $ hg init repo
48 48 $ cd repo
49 49
50 50 $ printf "aaaa\n" > a
51 51 $ hg commit -Am "change A"
52 52 adding a
53 53 $ printf "bbbb\n" > b
54 54 $ hg commit -Am "change B"
55 55 adding b
56 56 $ printf "cccc\n" > c
57 57 $ hg commit -Am "change C"
58 58 adding c
59 59 $ hg checkout 0
60 60 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
61 61 $ printf "dddd\n" > d
62 62 $ hg commit -Am "change D"
63 63 adding d
64 64 created new head
65 65 $ hg merge -r 2
66 66 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
67 67 (branch merge, don't forget to commit)
68 68 $ printf "eeee\n" > e
69 69 $ hg commit -Am "change E"
70 70 adding e
71 71 $ hg checkout 0
72 72 0 files updated, 0 files merged, 4 files removed, 0 files unresolved
73 73 $ printf "ffff\n" > f
74 74 $ hg commit -Am "change F"
75 75 adding f
76 76 created new head
77 77 $ hg checkout 0
78 78 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
79 79 $ printf "gggg\n" > g
80 80 $ hg commit -Am "change G"
81 81 adding g
82 82 created new head
83 83 $ hg merge -r 5
84 84 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
85 85 (branch merge, don't forget to commit)
86 86 $ printf "hhhh\n" > h
87 87 $ hg commit -Am "change H"
88 88 adding h
89 89 $ hg merge -r 4
90 90 4 files updated, 0 files merged, 0 files removed, 0 files unresolved
91 91 (branch merge, don't forget to commit)
92 92 $ printf "iiii\n" > i
93 93 $ hg commit -Am "change I"
94 94 adding i
95 95 $ hg checkout 2
96 96 0 files updated, 0 files merged, 6 files removed, 0 files unresolved
97 97 $ printf "jjjj\n" > j
98 98 $ hg commit -Am "change J"
99 99 adding j
100 100 created new head
101 101 $ hg checkout 7
102 102 3 files updated, 0 files merged, 3 files removed, 0 files unresolved
103 103 $ printf "kkkk\n" > k
104 104 $ hg add
105 105 adding k
106 106
107 107 $ hg log --graph --template '{rev} {desc}\n'
108 108 o 9 change J
109 109 |
110 110 | o 8 change I
111 111 | |\
112 112 | | @ 7 change H
113 113 | | |\
114 114 | | | o 6 change G
115 115 | | | |
116 116 | | o | 5 change F
117 117 | | |/
118 118 | o | 4 change E
119 119 |/| |
120 120 | o | 3 change D
121 121 | |/
122 122 o | 2 change C
123 123 | |
124 124 o | 1 change B
125 125 |/
126 126 o 0 change A
127 127
128 128
129 129 Fix all but the root revision and its four children.
130 130
131 131 #if obsstore-on
132 132 $ hg fix -r '2|4|7|8|9' --working-dir
133 133 #else
134 134 $ hg fix -r '2|4|7|8|9' --working-dir
135 135 saved backup bundle to * (glob)
136 136 #endif
137 137
138 138 The five revisions remain, but the other revisions were fixed and replaced. All
139 139 parent pointers have been accurately set to reproduce the previous topology
140 140 (though it is rendered in a slightly different order now).
141 141
142 142 #if obsstore-on
143 143 $ hg log --graph --template '{rev} {desc}\n'
144 144 o 14 change J
145 145 |
146 146 | o 13 change I
147 147 | |\
148 148 | | @ 12 change H
149 149 | | |\
150 150 | o | | 11 change E
151 151 |/| | |
152 152 o | | | 10 change C
153 153 | | | |
154 154 | | | o 6 change G
155 155 | | | |
156 156 | | o | 5 change F
157 157 | | |/
158 158 | o / 3 change D
159 159 | |/
160 160 o / 1 change B
161 161 |/
162 162 o 0 change A
163 163
164 164 $ C=10
165 165 $ E=11
166 166 $ H=12
167 167 $ I=13
168 168 $ J=14
169 169 #else
170 170 $ hg log --graph --template '{rev} {desc}\n'
171 171 o 9 change J
172 172 |
173 173 | o 8 change I
174 174 | |\
175 175 | | @ 7 change H
176 176 | | |\
177 177 | o | | 6 change E
178 178 |/| | |
179 179 o | | | 5 change C
180 180 | | | |
181 181 | | | o 4 change G
182 182 | | | |
183 183 | | o | 3 change F
184 184 | | |/
185 185 | o / 2 change D
186 186 | |/
187 187 o / 1 change B
188 188 |/
189 189 o 0 change A
190 190
191 191 $ C=5
192 192 $ E=6
193 193 $ H=7
194 194 $ I=8
195 195 $ J=9
196 196 #endif
197 197
198 198 Change C is a root of the set being fixed, so all we fix is what has changed
199 199 since its parent. That parent, change B, is its baserev.
200 200
201 201 $ hg cat -r $C 'set:**'
202 202 aaaa
203 203 bbbb
204 204 CCCC
205 205
206 206 Change E is a merge with only one parent being fixed. Its baserevs are the
207 207 unfixed parent plus the baserevs of the other parent. This evaluates to changes
208 208 B and D. We now have to decide what it means to incrementally fix a merge
209 209 commit. We choose to fix anything that has changed versus any baserev. Only the
210 210 undisturbed content of the common ancestor, change A, is unfixed.
211 211
212 212 $ hg cat -r $E 'set:**'
213 213 aaaa
214 214 BBBB
215 215 CCCC
216 216 DDDD
217 217 EEEE
218 218
219 219 Change H is a merge with neither parent being fixed. This is essentially
220 220 equivalent to the previous case because there is still only one baserev for
221 221 each parent of the merge.
222 222
223 223 $ hg cat -r $H 'set:**'
224 224 aaaa
225 225 FFFF
226 226 GGGG
227 227 HHHH
228 228
229 229 Change I is a merge that has four baserevs; two from each parent. We handle
230 230 multiple baserevs in the same way regardless of how many came from each parent.
231 231 So, fixing change H will fix any files that were not exactly the same in each
232 232 baserev.
233 233
234 234 $ hg cat -r $I 'set:**'
235 235 aaaa
236 236 BBBB
237 237 CCCC
238 238 DDDD
239 239 EEEE
240 240 FFFF
241 241 GGGG
242 242 HHHH
243 243 IIII
244 244
245 245 Change J is a simple case with one baserev, but its baserev is not its parent,
246 246 change C. Its baserev is its grandparent, change B.
247 247
248 248 $ hg cat -r $J 'set:**'
249 249 aaaa
250 250 bbbb
251 251 CCCC
252 252 JJJJ
253 253
254 254 The working copy was dirty, so it is treated much like a revision. The baserevs
255 255 for the working copy are inherited from its parent, change H, because it is
256 256 also being fixed.
257 257
258 258 $ cat *
259 259 aaaa
260 260 FFFF
261 261 GGGG
262 262 HHHH
263 263 KKKK
264 264
265 265 Change A was never a baserev because none of its children were to be fixed.
266 266
267 267 $ cd ..
268 268
269 269 The --all flag should fix anything that wouldn't cause a problem if you fixed
270 270 it, including the working copy. Obsolete revisions are not fixed because that
271 271 could cause divergence. Public revisions would cause an abort because they are
272 272 immutable. We can fix orphans because their successors are still just orphans
273 273 of the original obsolete parent. When obsolesence is off, we're just fixing and
274 274 replacing anything that isn't public.
275 275
276 276 $ hg init fixall
277 277 $ cd fixall
278 278
279 279 #if obsstore-on
280 280 $ printf "one\n" > foo.whole
281 281 $ hg commit -Aqm "first"
282 282 $ hg phase --public
283 283 $ hg tag --local root
284 284 $ printf "two\n" > foo.whole
285 285 $ hg commit -m "second"
286 286 $ printf "three\n" > foo.whole
287 287 $ hg commit -m "third" --secret
288 288 $ hg tag --local secret
289 289 $ hg checkout root
290 290 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
291 291 $ printf "four\n" > foo.whole
292 292 $ hg commit -m "fourth"
293 293 created new head
294 294 $ printf "five\n" > foo.whole
295 295 $ hg commit -m "fifth"
296 296 $ hg tag --local replaced
297 297 $ printf "six\n" > foo.whole
298 298 $ hg commit -m "sixth"
299 299 $ hg checkout replaced
300 300 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
301 301 $ printf "seven\n" > foo.whole
302 302 $ hg commit --amend
303 303 1 new orphan changesets
304 304 $ hg checkout secret
305 305 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
306 306 $ printf "uncommitted\n" > foo.whole
307 307
308 308 $ hg log --graph --template '{rev} {desc} {phase}\n'
309 309 o 6 fifth draft
310 310 |
311 311 | * 5 sixth draft
312 312 | |
313 313 | x 4 fifth draft
314 314 |/
315 315 o 3 fourth draft
316 316 |
317 317 | @ 2 third secret
318 318 | |
319 319 | o 1 second draft
320 320 |/
321 321 o 0 first public
322 322
323 323
324 324 $ hg fix --all
325 1 new orphan changesets
326 325
327 326 $ hg log --graph --template '{rev} {desc}\n' -r 'sort(all(), topo)' --hidden
328 327 o 11 fifth
329 328 |
330 329 o 9 fourth
331 330 |
332 331 | @ 8 third
333 332 | |
334 333 | o 7 second
335 334 |/
336 335 | * 10 sixth
337 336 | |
338 337 | | x 5 sixth
339 338 | |/
340 339 | x 4 fifth
341 340 | |
342 341 | | x 6 fifth
343 342 | |/
344 343 | x 3 fourth
345 344 |/
346 345 | x 2 third
347 346 | |
348 347 | x 1 second
349 348 |/
350 349 o 0 first
351 350
352 351
353 352 $ hg cat -r 7 foo.whole
354 353 TWO
355 354 $ hg cat -r 8 foo.whole
356 355 THREE
357 356 $ hg cat -r 9 foo.whole
358 357 FOUR
359 358 $ hg cat -r 10 foo.whole
360 359 SIX
361 360 $ hg cat -r 11 foo.whole
362 361 SEVEN
363 362 $ cat foo.whole
364 363 UNCOMMITTED
365 364 #else
366 365 $ printf "one\n" > foo.whole
367 366 $ hg commit -Aqm "first"
368 367 $ hg phase --public
369 368 $ hg tag --local root
370 369 $ printf "two\n" > foo.whole
371 370 $ hg commit -m "second"
372 371 $ printf "three\n" > foo.whole
373 372 $ hg commit -m "third" --secret
374 373 $ hg tag --local secret
375 374 $ hg checkout root
376 375 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
377 376 $ printf "four\n" > foo.whole
378 377 $ hg commit -m "fourth"
379 378 created new head
380 379 $ printf "uncommitted\n" > foo.whole
381 380
382 381 $ hg log --graph --template '{rev} {desc} {phase}\n'
383 382 @ 3 fourth draft
384 383 |
385 384 | o 2 third secret
386 385 | |
387 386 | o 1 second draft
388 387 |/
389 388 o 0 first public
390 389
391 390
392 391 $ hg fix --all
393 392 saved backup bundle to * (glob)
394 393
395 394 $ hg log --graph --template '{rev} {desc} {phase}\n'
396 395 @ 3 fourth draft
397 396 |
398 397 | o 2 third secret
399 398 | |
400 399 | o 1 second draft
401 400 |/
402 401 o 0 first public
403 402
404 403 $ hg cat -r 0 foo.whole
405 404 one
406 405 $ hg cat -r 1 foo.whole
407 406 TWO
408 407 $ hg cat -r 2 foo.whole
409 408 THREE
410 409 $ hg cat -r 3 foo.whole
411 410 FOUR
412 411 $ cat foo.whole
413 412 UNCOMMITTED
414 413 #endif
415 414
416 415 $ cd ..
417 416
General Comments 0
You need to be logged in to leave comments. Login now