##// END OF EJS Templates
fix: add --all flag to fix non-public non-obsolete revisions...
Danny Hooper -
r37613:e2506748 default
parent child Browse files
Show More
@@ -1,542 +1,548
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 registrar,
70 70 scmutil,
71 71 util,
72 72 )
73 73
74 74 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
75 75 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
76 76 # be specifying the version(s) of Mercurial they are tested with, or
77 77 # leave the attribute unspecified.
78 78 testedwith = 'ships-with-hg-core'
79 79
80 80 cmdtable = {}
81 81 command = registrar.command(cmdtable)
82 82
83 83 configtable = {}
84 84 configitem = registrar.configitem(configtable)
85 85
86 86 # Register the suboptions allowed for each configured fixer.
87 87 FIXER_ATTRS = ('command', 'linerange', 'fileset')
88 88
89 89 for key in FIXER_ATTRS:
90 90 configitem('fix', '.*(:%s)?' % key, default=None, generic=True)
91 91
92 92 # A good default size allows most source code files to be fixed, but avoids
93 93 # letting fixer tools choke on huge inputs, which could be surprising to the
94 94 # user.
95 95 configitem('fix', 'maxfilesize', default='2MB')
96 96
97 97 @command('fix',
98 [('', 'base', [], _('revisions to diff against (overrides automatic '
98 [('', 'all', False, _('fix all non-public non-obsolete revisions')),
99 ('', 'base', [], _('revisions to diff against (overrides automatic '
99 100 'selection, and applies to every revision being '
100 101 'fixed)'), _('REV')),
101 102 ('r', 'rev', [], _('revisions to fix'), _('REV')),
102 103 ('w', 'working-dir', False, _('fix the working directory')),
103 104 ('', 'whole', False, _('always fix every line of a file'))],
104 105 _('[OPTION]... [FILE]...'))
105 106 def fix(ui, repo, *pats, **opts):
106 107 """rewrite file content in changesets or working directory
107 108
108 109 Runs any configured tools to fix the content of files. Only affects files
109 110 with changes, unless file arguments are provided. Only affects changed lines
110 111 of files, unless the --whole flag is used. Some tools may always affect the
111 112 whole file regardless of --whole.
112 113
113 114 If revisions are specified with --rev, those revisions will be checked, and
114 115 they may be replaced with new revisions that have fixed file content. It is
115 116 desirable to specify all descendants of each specified revision, so that the
116 117 fixes propagate to the descendants. If all descendants are fixed at the same
117 118 time, no merging, rebasing, or evolution will be required.
118 119
119 120 If --working-dir is used, files with uncommitted changes in the working copy
120 121 will be fixed. If the checked-out revision is also fixed, the working
121 122 directory will update to the replacement revision.
122 123
123 124 When determining what lines of each file to fix at each revision, the whole
124 125 set of revisions being fixed is considered, so that fixes to earlier
125 126 revisions are not forgotten in later ones. The --base flag can be used to
126 127 override this default behavior, though it is not usually desirable to do so.
127 128 """
129 if opts['all']:
130 if opts['rev']:
131 raise error.Abort(_('cannot specify both "--rev" and "--all"'))
132 opts['rev'] = ['not public() and not obsolete()']
133 opts['working_dir'] = True
128 134 with repo.wlock(), repo.lock():
129 135 revstofix = getrevstofix(ui, repo, opts)
130 136 basectxs = getbasectxs(repo, opts, revstofix)
131 137 workqueue, numitems = getworkqueue(ui, repo, pats, opts, revstofix,
132 138 basectxs)
133 139 filedata = collections.defaultdict(dict)
134 140 replacements = {}
135 141 fixers = getfixers(ui)
136 142 # Some day this loop can become a worker pool, but for now it's easier
137 143 # to fix everything serially in topological order.
138 144 for rev, path in sorted(workqueue):
139 145 ctx = repo[rev]
140 146 olddata = ctx[path].data()
141 147 newdata = fixfile(ui, opts, fixers, ctx, path, basectxs[rev])
142 148 if newdata != olddata:
143 149 filedata[rev][path] = newdata
144 150 numitems[rev] -= 1
145 151 if not numitems[rev]:
146 152 if rev == wdirrev:
147 153 writeworkingdir(repo, ctx, filedata[rev], replacements)
148 154 else:
149 155 replacerev(ui, repo, ctx, filedata[rev], replacements)
150 156 del filedata[rev]
151 157
152 158 replacements = {prec: [succ] for prec, succ in replacements.iteritems()}
153 159 scmutil.cleanupnodes(repo, replacements, 'fix')
154 160
155 161 def getworkqueue(ui, repo, pats, opts, revstofix, basectxs):
156 162 """"Constructs the list of files to be fixed at specific revisions
157 163
158 164 It is up to the caller how to consume the work items, and the only
159 165 dependence between them is that replacement revisions must be committed in
160 166 topological order. Each work item represents a file in the working copy or
161 167 in some revision that should be fixed and written back to the working copy
162 168 or into a replacement revision.
163 169 """
164 170 workqueue = []
165 171 numitems = collections.defaultdict(int)
166 172 maxfilesize = ui.configbytes('fix', 'maxfilesize')
167 173 for rev in revstofix:
168 174 fixctx = repo[rev]
169 175 match = scmutil.match(fixctx, pats, opts)
170 176 for path in pathstofix(ui, repo, pats, opts, match, basectxs[rev],
171 177 fixctx):
172 178 if path not in fixctx:
173 179 continue
174 180 fctx = fixctx[path]
175 181 if fctx.islink():
176 182 continue
177 183 if fctx.size() > maxfilesize:
178 184 ui.warn(_('ignoring file larger than %s: %s\n') %
179 185 (util.bytecount(maxfilesize), path))
180 186 continue
181 187 workqueue.append((rev, path))
182 188 numitems[rev] += 1
183 189 return workqueue, numitems
184 190
185 191 def getrevstofix(ui, repo, opts):
186 192 """Returns the set of revision numbers that should be fixed"""
187 193 revs = set(scmutil.revrange(repo, opts['rev']))
188 194 for rev in revs:
189 195 checkfixablectx(ui, repo, repo[rev])
190 196 if revs:
191 197 cmdutil.checkunfinished(repo)
192 198 checknodescendants(repo, revs)
193 199 if opts.get('working_dir'):
194 200 revs.add(wdirrev)
195 201 if list(merge.mergestate.read(repo).unresolved()):
196 202 raise error.Abort('unresolved conflicts', hint="use 'hg resolve'")
197 203 if not revs:
198 204 raise error.Abort(
199 205 'no changesets specified', hint='use --rev or --working-dir')
200 206 return revs
201 207
202 208 def checknodescendants(repo, revs):
203 209 if (not obsolete.isenabled(repo, obsolete.allowunstableopt) and
204 210 repo.revs('(%ld::) - (%ld)', revs, revs)):
205 211 raise error.Abort(_('can only fix a changeset together '
206 212 'with all its descendants'))
207 213
208 214 def checkfixablectx(ui, repo, ctx):
209 215 """Aborts if the revision shouldn't be replaced with a fixed one."""
210 216 if not ctx.mutable():
211 217 raise error.Abort('can\'t fix immutable changeset %s' %
212 218 (scmutil.formatchangeid(ctx),))
213 219 if ctx.obsolete():
214 220 # It would be better to actually check if the revision has a successor.
215 221 allowdivergence = ui.configbool('experimental',
216 222 'evolution.allowdivergence')
217 223 if not allowdivergence:
218 224 raise error.Abort('fixing obsolete revision could cause divergence')
219 225
220 226 def pathstofix(ui, repo, pats, opts, match, basectxs, fixctx):
221 227 """Returns the set of files that should be fixed in a context
222 228
223 229 The result depends on the base contexts; we include any file that has
224 230 changed relative to any of the base contexts. Base contexts should be
225 231 ancestors of the context being fixed.
226 232 """
227 233 files = set()
228 234 for basectx in basectxs:
229 235 stat = repo.status(
230 236 basectx, fixctx, match=match, clean=bool(pats), unknown=bool(pats))
231 237 files.update(
232 238 set(itertools.chain(stat.added, stat.modified, stat.clean,
233 239 stat.unknown)))
234 240 return files
235 241
236 242 def lineranges(opts, path, basectxs, fixctx, content2):
237 243 """Returns the set of line ranges that should be fixed in a file
238 244
239 245 Of the form [(10, 20), (30, 40)].
240 246
241 247 This depends on the given base contexts; we must consider lines that have
242 248 changed versus any of the base contexts, and whether the file has been
243 249 renamed versus any of them.
244 250
245 251 Another way to understand this is that we exclude line ranges that are
246 252 common to the file in all base contexts.
247 253 """
248 254 if opts.get('whole'):
249 255 # Return a range containing all lines. Rely on the diff implementation's
250 256 # idea of how many lines are in the file, instead of reimplementing it.
251 257 return difflineranges('', content2)
252 258
253 259 rangeslist = []
254 260 for basectx in basectxs:
255 261 basepath = copies.pathcopies(basectx, fixctx).get(path, path)
256 262 if basepath in basectx:
257 263 content1 = basectx[basepath].data()
258 264 else:
259 265 content1 = ''
260 266 rangeslist.extend(difflineranges(content1, content2))
261 267 return unionranges(rangeslist)
262 268
263 269 def unionranges(rangeslist):
264 270 """Return the union of some closed intervals
265 271
266 272 >>> unionranges([])
267 273 []
268 274 >>> unionranges([(1, 100)])
269 275 [(1, 100)]
270 276 >>> unionranges([(1, 100), (1, 100)])
271 277 [(1, 100)]
272 278 >>> unionranges([(1, 100), (2, 100)])
273 279 [(1, 100)]
274 280 >>> unionranges([(1, 99), (1, 100)])
275 281 [(1, 100)]
276 282 >>> unionranges([(1, 100), (40, 60)])
277 283 [(1, 100)]
278 284 >>> unionranges([(1, 49), (50, 100)])
279 285 [(1, 100)]
280 286 >>> unionranges([(1, 48), (50, 100)])
281 287 [(1, 48), (50, 100)]
282 288 >>> unionranges([(1, 2), (3, 4), (5, 6)])
283 289 [(1, 6)]
284 290 """
285 291 rangeslist = sorted(set(rangeslist))
286 292 unioned = []
287 293 if rangeslist:
288 294 unioned, rangeslist = [rangeslist[0]], rangeslist[1:]
289 295 for a, b in rangeslist:
290 296 c, d = unioned[-1]
291 297 if a > d + 1:
292 298 unioned.append((a, b))
293 299 else:
294 300 unioned[-1] = (c, max(b, d))
295 301 return unioned
296 302
297 303 def difflineranges(content1, content2):
298 304 """Return list of line number ranges in content2 that differ from content1.
299 305
300 306 Line numbers are 1-based. The numbers are the first and last line contained
301 307 in the range. Single-line ranges have the same line number for the first and
302 308 last line. Excludes any empty ranges that result from lines that are only
303 309 present in content1. Relies on mdiff's idea of where the line endings are in
304 310 the string.
305 311
306 312 >>> from mercurial import pycompat
307 313 >>> lines = lambda s: b'\\n'.join([c for c in pycompat.iterbytestr(s)])
308 314 >>> difflineranges2 = lambda a, b: difflineranges(lines(a), lines(b))
309 315 >>> difflineranges2(b'', b'')
310 316 []
311 317 >>> difflineranges2(b'a', b'')
312 318 []
313 319 >>> difflineranges2(b'', b'A')
314 320 [(1, 1)]
315 321 >>> difflineranges2(b'a', b'a')
316 322 []
317 323 >>> difflineranges2(b'a', b'A')
318 324 [(1, 1)]
319 325 >>> difflineranges2(b'ab', b'')
320 326 []
321 327 >>> difflineranges2(b'', b'AB')
322 328 [(1, 2)]
323 329 >>> difflineranges2(b'abc', b'ac')
324 330 []
325 331 >>> difflineranges2(b'ab', b'aCb')
326 332 [(2, 2)]
327 333 >>> difflineranges2(b'abc', b'aBc')
328 334 [(2, 2)]
329 335 >>> difflineranges2(b'ab', b'AB')
330 336 [(1, 2)]
331 337 >>> difflineranges2(b'abcde', b'aBcDe')
332 338 [(2, 2), (4, 4)]
333 339 >>> difflineranges2(b'abcde', b'aBCDe')
334 340 [(2, 4)]
335 341 """
336 342 ranges = []
337 343 for lines, kind in mdiff.allblocks(content1, content2):
338 344 firstline, lastline = lines[2:4]
339 345 if kind == '!' and firstline != lastline:
340 346 ranges.append((firstline + 1, lastline))
341 347 return ranges
342 348
343 349 def getbasectxs(repo, opts, revstofix):
344 350 """Returns a map of the base contexts for each revision
345 351
346 352 The base contexts determine which lines are considered modified when we
347 353 attempt to fix just the modified lines in a file.
348 354 """
349 355 # The --base flag overrides the usual logic, and we give every revision
350 356 # exactly the set of baserevs that the user specified.
351 357 if opts.get('base'):
352 358 baserevs = set(scmutil.revrange(repo, opts.get('base')))
353 359 if not baserevs:
354 360 baserevs = {nullrev}
355 361 basectxs = {repo[rev] for rev in baserevs}
356 362 return {rev: basectxs for rev in revstofix}
357 363
358 364 # Proceed in topological order so that we can easily determine each
359 365 # revision's baserevs by looking at its parents and their baserevs.
360 366 basectxs = collections.defaultdict(set)
361 367 for rev in sorted(revstofix):
362 368 ctx = repo[rev]
363 369 for pctx in ctx.parents():
364 370 if pctx.rev() in basectxs:
365 371 basectxs[rev].update(basectxs[pctx.rev()])
366 372 else:
367 373 basectxs[rev].add(pctx)
368 374 return basectxs
369 375
370 376 def fixfile(ui, opts, fixers, fixctx, path, basectxs):
371 377 """Run any configured fixers that should affect the file in this context
372 378
373 379 Returns the file content that results from applying the fixers in some order
374 380 starting with the file's content in the fixctx. Fixers that support line
375 381 ranges will affect lines that have changed relative to any of the basectxs
376 382 (i.e. they will only avoid lines that are common to all basectxs).
377 383 """
378 384 newdata = fixctx[path].data()
379 385 for fixername, fixer in fixers.iteritems():
380 386 if fixer.affects(opts, fixctx, path):
381 387 ranges = lineranges(opts, path, basectxs, fixctx, newdata)
382 388 command = fixer.command(path, ranges)
383 389 if command is None:
384 390 continue
385 391 ui.debug('subprocess: %s\n' % (command,))
386 392 proc = subprocess.Popen(
387 393 command,
388 394 shell=True,
389 395 cwd='/',
390 396 stdin=subprocess.PIPE,
391 397 stdout=subprocess.PIPE,
392 398 stderr=subprocess.PIPE)
393 399 newerdata, stderr = proc.communicate(newdata)
394 400 if stderr:
395 401 showstderr(ui, fixctx.rev(), fixername, stderr)
396 402 else:
397 403 newdata = newerdata
398 404 return newdata
399 405
400 406 def showstderr(ui, rev, fixername, stderr):
401 407 """Writes the lines of the stderr string as warnings on the ui
402 408
403 409 Uses the revision number and fixername to give more context to each line of
404 410 the error message. Doesn't include file names, since those take up a lot of
405 411 space and would tend to be included in the error message if they were
406 412 relevant.
407 413 """
408 414 for line in re.split('[\r\n]+', stderr):
409 415 if line:
410 416 ui.warn(('['))
411 417 if rev is None:
412 418 ui.warn(_('wdir'), label='evolve.rev')
413 419 else:
414 420 ui.warn((str(rev)), label='evolve.rev')
415 421 ui.warn(('] %s: %s\n') % (fixername, line))
416 422
417 423 def writeworkingdir(repo, ctx, filedata, replacements):
418 424 """Write new content to the working copy and check out the new p1 if any
419 425
420 426 We check out a new revision if and only if we fixed something in both the
421 427 working directory and its parent revision. This avoids the need for a full
422 428 update/merge, and means that the working directory simply isn't affected
423 429 unless the --working-dir flag is given.
424 430
425 431 Directly updates the dirstate for the affected files.
426 432 """
427 433 for path, data in filedata.iteritems():
428 434 fctx = ctx[path]
429 435 fctx.write(data, fctx.flags())
430 436 if repo.dirstate[path] == 'n':
431 437 repo.dirstate.normallookup(path)
432 438
433 439 oldparentnodes = repo.dirstate.parents()
434 440 newparentnodes = [replacements.get(n, n) for n in oldparentnodes]
435 441 if newparentnodes != oldparentnodes:
436 442 repo.setparents(*newparentnodes)
437 443
438 444 def replacerev(ui, repo, ctx, filedata, replacements):
439 445 """Commit a new revision like the given one, but with file content changes
440 446
441 447 "ctx" is the original revision to be replaced by a modified one.
442 448
443 449 "filedata" is a dict that maps paths to their new file content. All other
444 450 paths will be recreated from the original revision without changes.
445 451 "filedata" may contain paths that didn't exist in the original revision;
446 452 they will be added.
447 453
448 454 "replacements" is a dict that maps a single node to a single node, and it is
449 455 updated to indicate the original revision is replaced by the newly created
450 456 one. No entry is added if the replacement's node already exists.
451 457
452 458 The new revision has the same parents as the old one, unless those parents
453 459 have already been replaced, in which case those replacements are the parents
454 460 of this new revision. Thus, if revisions are replaced in topological order,
455 461 there is no need to rebase them into the original topology later.
456 462 """
457 463
458 464 p1rev, p2rev = repo.changelog.parentrevs(ctx.rev())
459 465 p1ctx, p2ctx = repo[p1rev], repo[p2rev]
460 466 newp1node = replacements.get(p1ctx.node(), p1ctx.node())
461 467 newp2node = replacements.get(p2ctx.node(), p2ctx.node())
462 468
463 469 def filectxfn(repo, memctx, path):
464 470 if path not in ctx:
465 471 return None
466 472 fctx = ctx[path]
467 473 copied = fctx.renamed()
468 474 if copied:
469 475 copied = copied[0]
470 476 return context.memfilectx(
471 477 repo,
472 478 memctx,
473 479 path=fctx.path(),
474 480 data=filedata.get(path, fctx.data()),
475 481 islink=fctx.islink(),
476 482 isexec=fctx.isexec(),
477 483 copied=copied)
478 484
479 485 overrides = {('phases', 'new-commit'): ctx.phase()}
480 486 with ui.configoverride(overrides, source='fix'):
481 487 memctx = context.memctx(
482 488 repo,
483 489 parents=(newp1node, newp2node),
484 490 text=ctx.description(),
485 491 files=set(ctx.files()) | set(filedata.keys()),
486 492 filectxfn=filectxfn,
487 493 user=ctx.user(),
488 494 date=ctx.date(),
489 495 extra=ctx.extra(),
490 496 branch=ctx.branch(),
491 497 editor=None)
492 498 sucnode = memctx.commit()
493 499 prenode = ctx.node()
494 500 if prenode == sucnode:
495 501 ui.debug('node %s already existed\n' % (ctx.hex()))
496 502 else:
497 503 replacements[ctx.node()] = sucnode
498 504
499 505 def getfixers(ui):
500 506 """Returns a map of configured fixer tools indexed by their names
501 507
502 508 Each value is a Fixer object with methods that implement the behavior of the
503 509 fixer's config suboptions. Does not validate the config values.
504 510 """
505 511 result = {}
506 512 for name in fixernames(ui):
507 513 result[name] = Fixer()
508 514 attrs = ui.configsuboptions('fix', name)[1]
509 515 for key in FIXER_ATTRS:
510 516 setattr(result[name], '_' + key, attrs.get(key, ''))
511 517 return result
512 518
513 519 def fixernames(ui):
514 520 """Returns the names of [fix] config options that have suboptions"""
515 521 names = set()
516 522 for k, v in ui.configitems('fix'):
517 523 if ':' in k:
518 524 names.add(k.split(':', 1)[0])
519 525 return names
520 526
521 527 class Fixer(object):
522 528 """Wraps the raw config values for a fixer with methods"""
523 529
524 530 def affects(self, opts, fixctx, path):
525 531 """Should this fixer run on the file at the given path and context?"""
526 532 return scmutil.match(fixctx, [self._fileset], opts)(path)
527 533
528 534 def command(self, path, ranges):
529 535 """A shell command to use to invoke this fixer on the given file/lines
530 536
531 537 May return None if there is no appropriate command to run for the given
532 538 parameters.
533 539 """
534 540 parts = [self._command.format(rootpath=path,
535 541 basename=os.path.basename(path))]
536 542 if self._linerange:
537 543 if not ranges:
538 544 # No line ranges to fix, so don't run the fixer.
539 545 return None
540 546 for first, last in ranges:
541 547 parts.append(self._linerange.format(first=first, last=last))
542 548 return ' '.join(parts)
@@ -1,268 +1,417
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 The --all flag should fix anything that wouldn't cause a problem if you fixed
270 it, including the working copy. Obsolete revisions are not fixed because that
271 could cause divergence. Public revisions would cause an abort because they are
272 immutable. We can fix orphans because their successors are still just orphans
273 of the original obsolete parent. When obsolesence is off, we're just fixing and
274 replacing anything that isn't public.
275
276 $ hg init fixall
277 $ cd fixall
278
279 #if obsstore-on
280 $ printf "one\n" > foo.whole
281 $ hg commit -Aqm "first"
282 $ hg phase --public
283 $ hg tag --local root
284 $ printf "two\n" > foo.whole
285 $ hg commit -m "second"
286 $ printf "three\n" > foo.whole
287 $ hg commit -m "third" --secret
288 $ hg tag --local secret
289 $ hg checkout root
290 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
291 $ printf "four\n" > foo.whole
292 $ hg commit -m "fourth"
293 created new head
294 $ printf "five\n" > foo.whole
295 $ hg commit -m "fifth"
296 $ hg tag --local replaced
297 $ printf "six\n" > foo.whole
298 $ hg commit -m "sixth"
299 $ hg checkout replaced
300 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
301 $ printf "seven\n" > foo.whole
302 $ hg commit --amend
303 1 new orphan changesets
304 $ hg checkout secret
305 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
306 $ printf "uncommitted\n" > foo.whole
307
308 $ hg log --graph --template '{rev} {desc} {phase}\n'
309 o 6 fifth draft
310 |
311 | * 5 sixth draft
312 | |
313 | x 4 fifth draft
314 |/
315 o 3 fourth draft
316 |
317 | @ 2 third secret
318 | |
319 | o 1 second draft
320 |/
321 o 0 first public
322
323
324 $ hg fix --all
325 1 new orphan changesets
326
327 $ hg log --graph --template '{rev} {desc}\n' -r 'sort(all(), topo)' --hidden
328 o 11 fifth
329 |
330 o 9 fourth
331 |
332 | @ 8 third
333 | |
334 | o 7 second
335 |/
336 | * 10 sixth
337 | |
338 | | x 5 sixth
339 | |/
340 | x 4 fifth
341 | |
342 | | x 6 fifth
343 | |/
344 | x 3 fourth
345 |/
346 | x 2 third
347 | |
348 | x 1 second
349 |/
350 o 0 first
351
352
353 $ hg cat -r 7 foo.whole
354 TWO
355 $ hg cat -r 8 foo.whole
356 THREE
357 $ hg cat -r 9 foo.whole
358 FOUR
359 $ hg cat -r 10 foo.whole
360 SIX
361 $ hg cat -r 11 foo.whole
362 SEVEN
363 $ cat foo.whole
364 UNCOMMITTED
365 #else
366 $ printf "one\n" > foo.whole
367 $ hg commit -Aqm "first"
368 $ hg phase --public
369 $ hg tag --local root
370 $ printf "two\n" > foo.whole
371 $ hg commit -m "second"
372 $ printf "three\n" > foo.whole
373 $ hg commit -m "third" --secret
374 $ hg tag --local secret
375 $ hg checkout root
376 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
377 $ printf "four\n" > foo.whole
378 $ hg commit -m "fourth"
379 created new head
380 $ printf "uncommitted\n" > foo.whole
381
382 $ hg log --graph --template '{rev} {desc} {phase}\n'
383 @ 3 fourth draft
384 |
385 | o 2 third secret
386 | |
387 | o 1 second draft
388 |/
389 o 0 first public
390
391
392 $ hg fix --all
393 saved backup bundle to * (glob)
394
395 $ hg log --graph --template '{rev} {desc} {phase}\n'
396 @ 3 fourth draft
397 |
398 | o 2 third secret
399 | |
400 | o 1 second draft
401 |/
402 o 0 first public
403
404 $ hg cat -r 0 foo.whole
405 one
406 $ hg cat -r 1 foo.whole
407 TWO
408 $ hg cat -r 2 foo.whole
409 THREE
410 $ hg cat -r 3 foo.whole
411 FOUR
412 $ cat foo.whole
413 UNCOMMITTED
414 #endif
415
416 $ cd ..
417
@@ -1,1024 +1,1025
1 1 A script that implements uppercasing of specific lines in a file. This
2 2 approximates the behavior of code formatters well enough for our tests.
3 3
4 4 $ UPPERCASEPY="$TESTTMP/uppercase.py"
5 5 $ cat > $UPPERCASEPY <<EOF
6 6 > import sys
7 7 > from mercurial.utils.procutil import setbinary
8 8 > setbinary(sys.stdin)
9 9 > setbinary(sys.stdout)
10 10 > lines = set()
11 11 > for arg in sys.argv[1:]:
12 12 > if arg == 'all':
13 13 > sys.stdout.write(sys.stdin.read().upper())
14 14 > sys.exit(0)
15 15 > else:
16 16 > first, last = arg.split('-')
17 17 > lines.update(range(int(first), int(last) + 1))
18 18 > for i, line in enumerate(sys.stdin.readlines()):
19 19 > if i + 1 in lines:
20 20 > sys.stdout.write(line.upper())
21 21 > else:
22 22 > sys.stdout.write(line)
23 23 > EOF
24 24 $ TESTLINES="foo\nbar\nbaz\nqux\n"
25 25 $ printf $TESTLINES | $PYTHON $UPPERCASEPY
26 26 foo
27 27 bar
28 28 baz
29 29 qux
30 30 $ printf $TESTLINES | $PYTHON $UPPERCASEPY all
31 31 FOO
32 32 BAR
33 33 BAZ
34 34 QUX
35 35 $ printf $TESTLINES | $PYTHON $UPPERCASEPY 1-1
36 36 FOO
37 37 bar
38 38 baz
39 39 qux
40 40 $ printf $TESTLINES | $PYTHON $UPPERCASEPY 1-2
41 41 FOO
42 42 BAR
43 43 baz
44 44 qux
45 45 $ printf $TESTLINES | $PYTHON $UPPERCASEPY 2-3
46 46 foo
47 47 BAR
48 48 BAZ
49 49 qux
50 50 $ printf $TESTLINES | $PYTHON $UPPERCASEPY 2-2 4-4
51 51 foo
52 52 BAR
53 53 baz
54 54 QUX
55 55
56 56 Set up the config with two simple fixers: one that fixes specific line ranges,
57 57 and one that always fixes the whole file. They both "fix" files by converting
58 58 letters to uppercase. They use different file extensions, so each test case can
59 59 choose which behavior to use by naming files.
60 60
61 61 $ cat >> $HGRCPATH <<EOF
62 62 > [extensions]
63 63 > fix =
64 64 > [experimental]
65 65 > evolution.createmarkers=True
66 66 > evolution.allowunstable=True
67 67 > [fix]
68 68 > uppercase-whole-file:command=$PYTHON $UPPERCASEPY all
69 69 > uppercase-whole-file:fileset=set:**.whole
70 70 > uppercase-changed-lines:command=$PYTHON $UPPERCASEPY
71 71 > uppercase-changed-lines:linerange={first}-{last}
72 72 > uppercase-changed-lines:fileset=set:**.changed
73 73 > EOF
74 74
75 75 Help text for fix.
76 76
77 77 $ hg help fix
78 78 hg fix [OPTION]... [FILE]...
79 79
80 80 rewrite file content in changesets or working directory
81 81
82 82 Runs any configured tools to fix the content of files. Only affects files
83 83 with changes, unless file arguments are provided. Only affects changed
84 84 lines of files, unless the --whole flag is used. Some tools may always
85 85 affect the whole file regardless of --whole.
86 86
87 87 If revisions are specified with --rev, those revisions will be checked,
88 88 and they may be replaced with new revisions that have fixed file content.
89 89 It is desirable to specify all descendants of each specified revision, so
90 90 that the fixes propagate to the descendants. If all descendants are fixed
91 91 at the same time, no merging, rebasing, or evolution will be required.
92 92
93 93 If --working-dir is used, files with uncommitted changes in the working
94 94 copy will be fixed. If the checked-out revision is also fixed, the working
95 95 directory will update to the replacement revision.
96 96
97 97 When determining what lines of each file to fix at each revision, the
98 98 whole set of revisions being fixed is considered, so that fixes to earlier
99 99 revisions are not forgotten in later ones. The --base flag can be used to
100 100 override this default behavior, though it is not usually desirable to do
101 101 so.
102 102
103 103 (use 'hg help -e fix' to show help for the fix extension)
104 104
105 105 options ([+] can be repeated):
106 106
107 --all fix all non-public non-obsolete revisions
107 108 --base REV [+] revisions to diff against (overrides automatic selection,
108 109 and applies to every revision being fixed)
109 110 -r --rev REV [+] revisions to fix
110 111 -w --working-dir fix the working directory
111 112 --whole always fix every line of a file
112 113
113 114 (some details hidden, use --verbose to show complete help)
114 115
115 116 $ hg help -e fix
116 117 fix extension - rewrite file content in changesets or working copy
117 118 (EXPERIMENTAL)
118 119
119 120 Provides a command that runs configured tools on the contents of modified
120 121 files, writing back any fixes to the working copy or replacing changesets.
121 122
122 123 Here is an example configuration that causes 'hg fix' to apply automatic
123 124 formatting fixes to modified lines in C++ code:
124 125
125 126 [fix]
126 127 clang-format:command=clang-format --assume-filename={rootpath}
127 128 clang-format:linerange=--lines={first}:{last}
128 129 clang-format:fileset=set:**.cpp or **.hpp
129 130
130 131 The :command suboption forms the first part of the shell command that will be
131 132 used to fix a file. The content of the file is passed on standard input, and
132 133 the fixed file content is expected on standard output. If there is any output
133 134 on standard error, the file will not be affected. Some values may be
134 135 substituted into the command:
135 136
136 137 {rootpath} The path of the file being fixed, relative to the repo root
137 138 {basename} The name of the file being fixed, without the directory path
138 139
139 140 If the :linerange suboption is set, the tool will only be run if there are
140 141 changed lines in a file. The value of this suboption is appended to the shell
141 142 command once for every range of changed lines in the file. Some values may be
142 143 substituted into the command:
143 144
144 145 {first} The 1-based line number of the first line in the modified range
145 146 {last} The 1-based line number of the last line in the modified range
146 147
147 148 The :fileset suboption determines which files will be passed through each
148 149 configured tool. See 'hg help fileset' for possible values. If there are file
149 150 arguments to 'hg fix', the intersection of these filesets is used.
150 151
151 152 There is also a configurable limit for the maximum size of file that will be
152 153 processed by 'hg fix':
153 154
154 155 [fix]
155 156 maxfilesize=2MB
156 157
157 158 list of commands:
158 159
159 160 fix rewrite file content in changesets or working directory
160 161
161 162 (use 'hg help -v -e fix' to show built-in aliases and global options)
162 163
163 164 There is no default behavior in the absence of --rev and --working-dir.
164 165
165 166 $ hg init badusage
166 167 $ cd badusage
167 168
168 169 $ hg fix
169 170 abort: no changesets specified
170 171 (use --rev or --working-dir)
171 172 [255]
172 173 $ hg fix --whole
173 174 abort: no changesets specified
174 175 (use --rev or --working-dir)
175 176 [255]
176 177 $ hg fix --base 0
177 178 abort: no changesets specified
178 179 (use --rev or --working-dir)
179 180 [255]
180 181
181 182 Fixing a public revision isn't allowed. It should abort early enough that
182 183 nothing happens, even to the working directory.
183 184
184 185 $ printf "hello\n" > hello.whole
185 186 $ hg commit -Aqm "hello"
186 187 $ hg phase -r 0 --public
187 188 $ hg fix -r 0
188 189 abort: can't fix immutable changeset 0:6470986d2e7b
189 190 [255]
190 191 $ hg fix -r 0 --working-dir
191 192 abort: can't fix immutable changeset 0:6470986d2e7b
192 193 [255]
193 194 $ hg cat -r tip hello.whole
194 195 hello
195 196 $ cat hello.whole
196 197 hello
197 198
198 199 $ cd ..
199 200
200 201 Fixing a clean working directory should do nothing. Even the --whole flag
201 202 shouldn't cause any clean files to be fixed. Specifying a clean file explicitly
202 203 should only fix it if the fixer always fixes the whole file. The combination of
203 204 an explicit filename and --whole should format the entire file regardless.
204 205
205 206 $ hg init fixcleanwdir
206 207 $ cd fixcleanwdir
207 208
208 209 $ printf "hello\n" > hello.changed
209 210 $ printf "world\n" > hello.whole
210 211 $ hg commit -Aqm "foo"
211 212 $ hg fix --working-dir
212 213 $ hg diff
213 214 $ hg fix --working-dir --whole
214 215 $ hg diff
215 216 $ hg fix --working-dir *
216 217 $ cat *
217 218 hello
218 219 WORLD
219 220 $ hg revert --all --no-backup
220 221 reverting hello.whole
221 222 $ hg fix --working-dir * --whole
222 223 $ cat *
223 224 HELLO
224 225 WORLD
225 226
226 227 The same ideas apply to fixing a revision, so we create a revision that doesn't
227 228 modify either of the files in question and try fixing it. This also tests that
228 229 we ignore a file that doesn't match any configured fixer.
229 230
230 231 $ hg revert --all --no-backup
231 232 reverting hello.changed
232 233 reverting hello.whole
233 234 $ printf "unimportant\n" > some.file
234 235 $ hg commit -Aqm "some other file"
235 236
236 237 $ hg fix -r .
237 238 $ hg cat -r tip *
238 239 hello
239 240 world
240 241 unimportant
241 242 $ hg fix -r . --whole
242 243 $ hg cat -r tip *
243 244 hello
244 245 world
245 246 unimportant
246 247 $ hg fix -r . *
247 248 $ hg cat -r tip *
248 249 hello
249 250 WORLD
250 251 unimportant
251 252 $ hg fix -r . * --whole --config experimental.evolution.allowdivergence=true
252 253 2 new content-divergent changesets
253 254 $ hg cat -r tip *
254 255 HELLO
255 256 WORLD
256 257 unimportant
257 258
258 259 $ cd ..
259 260
260 261 Fixing the working directory should still work if there are no revisions.
261 262
262 263 $ hg init norevisions
263 264 $ cd norevisions
264 265
265 266 $ printf "something\n" > something.whole
266 267 $ hg add
267 268 adding something.whole
268 269 $ hg fix --working-dir
269 270 $ cat something.whole
270 271 SOMETHING
271 272
272 273 $ cd ..
273 274
274 275 Test the effect of fixing the working directory for each possible status, with
275 276 and without providing explicit file arguments.
276 277
277 278 $ hg init implicitlyfixstatus
278 279 $ cd implicitlyfixstatus
279 280
280 281 $ printf "modified\n" > modified.whole
281 282 $ printf "removed\n" > removed.whole
282 283 $ printf "deleted\n" > deleted.whole
283 284 $ printf "clean\n" > clean.whole
284 285 $ printf "ignored.whole" > .hgignore
285 286 $ hg commit -Aqm "stuff"
286 287
287 288 $ printf "modified!!!\n" > modified.whole
288 289 $ printf "unknown\n" > unknown.whole
289 290 $ printf "ignored\n" > ignored.whole
290 291 $ printf "added\n" > added.whole
291 292 $ hg add added.whole
292 293 $ hg remove removed.whole
293 294 $ rm deleted.whole
294 295
295 296 $ hg status --all
296 297 M modified.whole
297 298 A added.whole
298 299 R removed.whole
299 300 ! deleted.whole
300 301 ? unknown.whole
301 302 I ignored.whole
302 303 C .hgignore
303 304 C clean.whole
304 305
305 306 $ hg fix --working-dir
306 307
307 308 $ hg status --all
308 309 M modified.whole
309 310 A added.whole
310 311 R removed.whole
311 312 ! deleted.whole
312 313 ? unknown.whole
313 314 I ignored.whole
314 315 C .hgignore
315 316 C clean.whole
316 317
317 318 $ cat *.whole
318 319 ADDED
319 320 clean
320 321 ignored
321 322 MODIFIED!!!
322 323 unknown
323 324
324 325 $ printf "modified!!!\n" > modified.whole
325 326 $ printf "added\n" > added.whole
326 327 $ hg fix --working-dir *.whole
327 328
328 329 $ hg status --all
329 330 M clean.whole
330 331 M modified.whole
331 332 A added.whole
332 333 R removed.whole
333 334 ! deleted.whole
334 335 ? unknown.whole
335 336 I ignored.whole
336 337 C .hgignore
337 338
338 339 It would be better if this also fixed the unknown file.
339 340 $ cat *.whole
340 341 ADDED
341 342 CLEAN
342 343 ignored
343 344 MODIFIED!!!
344 345 unknown
345 346
346 347 $ cd ..
347 348
348 349 Test that incremental fixing works on files with additions, deletions, and
349 350 changes in multiple line ranges. Note that deletions do not generally cause
350 351 neighboring lines to be fixed, so we don't return a line range for purely
351 352 deleted sections. In the future we should support a :deletion config that
352 353 allows fixers to know where deletions are located.
353 354
354 355 $ hg init incrementalfixedlines
355 356 $ cd incrementalfixedlines
356 357
357 358 $ printf "a\nb\nc\nd\ne\nf\ng\n" > foo.txt
358 359 $ hg commit -Aqm "foo"
359 360 $ printf "zz\na\nc\ndd\nee\nff\nf\ngg\n" > foo.txt
360 361
361 362 $ hg --config "fix.fail:command=echo" \
362 363 > --config "fix.fail:linerange={first}:{last}" \
363 364 > --config "fix.fail:fileset=foo.txt" \
364 365 > fix --working-dir
365 366 $ cat foo.txt
366 367 1:1 4:6 8:8
367 368
368 369 $ cd ..
369 370
370 371 Test that --whole fixes all lines regardless of the diffs present.
371 372
372 373 $ hg init wholeignoresdiffs
373 374 $ cd wholeignoresdiffs
374 375
375 376 $ printf "a\nb\nc\nd\ne\nf\ng\n" > foo.changed
376 377 $ hg commit -Aqm "foo"
377 378 $ printf "zz\na\nc\ndd\nee\nff\nf\ngg\n" > foo.changed
378 379 $ hg fix --working-dir --whole
379 380 $ cat foo.changed
380 381 ZZ
381 382 A
382 383 C
383 384 DD
384 385 EE
385 386 FF
386 387 F
387 388 GG
388 389
389 390 $ cd ..
390 391
391 392 We should do nothing with symlinks, and their targets should be unaffected. Any
392 393 other behavior would be more complicated to implement and harder to document.
393 394
394 395 #if symlink
395 396 $ hg init dontmesswithsymlinks
396 397 $ cd dontmesswithsymlinks
397 398
398 399 $ printf "hello\n" > hello.whole
399 400 $ ln -s hello.whole hellolink
400 401 $ hg add
401 402 adding hello.whole
402 403 adding hellolink
403 404 $ hg fix --working-dir hellolink
404 405 $ hg status
405 406 A hello.whole
406 407 A hellolink
407 408
408 409 $ cd ..
409 410 #endif
410 411
411 412 We should allow fixers to run on binary files, even though this doesn't sound
412 413 like a common use case. There's not much benefit to disallowing it, and users
413 414 can add "and not binary()" to their filesets if needed. The Mercurial
414 415 philosophy is generally to not handle binary files specially anyway.
415 416
416 417 $ hg init cantouchbinaryfiles
417 418 $ cd cantouchbinaryfiles
418 419
419 420 $ printf "hello\0\n" > hello.whole
420 421 $ hg add
421 422 adding hello.whole
422 423 $ hg fix --working-dir 'set:binary()'
423 424 $ cat hello.whole
424 425 HELLO\x00 (esc)
425 426
426 427 $ cd ..
427 428
428 429 We have a config for the maximum size of file we will attempt to fix. This can
429 430 be helpful to avoid running unsuspecting fixer tools on huge inputs, which
430 431 could happen by accident without a well considered configuration. A more
431 432 precise configuration could use the size() fileset function if one global limit
432 433 is undesired.
433 434
434 435 $ hg init maxfilesize
435 436 $ cd maxfilesize
436 437
437 438 $ printf "this file is huge\n" > hello.whole
438 439 $ hg add
439 440 adding hello.whole
440 441 $ hg --config fix.maxfilesize=10 fix --working-dir
441 442 ignoring file larger than 10 bytes: hello.whole
442 443 $ cat hello.whole
443 444 this file is huge
444 445
445 446 $ cd ..
446 447
447 448 If we specify a file to fix, other files should be left alone, even if they
448 449 have changes.
449 450
450 451 $ hg init fixonlywhatitellyouto
451 452 $ cd fixonlywhatitellyouto
452 453
453 454 $ printf "fix me!\n" > fixme.whole
454 455 $ printf "not me.\n" > notme.whole
455 456 $ hg add
456 457 adding fixme.whole
457 458 adding notme.whole
458 459 $ hg fix --working-dir fixme.whole
459 460 $ cat *.whole
460 461 FIX ME!
461 462 not me.
462 463
463 464 $ cd ..
464 465
465 466 Specifying a directory name should fix all its files and subdirectories.
466 467
467 468 $ hg init fixdirectory
468 469 $ cd fixdirectory
469 470
470 471 $ mkdir -p dir1/dir2
471 472 $ printf "foo\n" > foo.whole
472 473 $ printf "bar\n" > dir1/bar.whole
473 474 $ printf "baz\n" > dir1/dir2/baz.whole
474 475 $ hg add
475 476 adding dir1/bar.whole
476 477 adding dir1/dir2/baz.whole
477 478 adding foo.whole
478 479 $ hg fix --working-dir dir1
479 480 $ cat foo.whole dir1/bar.whole dir1/dir2/baz.whole
480 481 foo
481 482 BAR
482 483 BAZ
483 484
484 485 $ cd ..
485 486
486 487 Fixing a file in the working directory that needs no fixes should not actually
487 488 write back to the file, so for example the mtime shouldn't change.
488 489
489 490 $ hg init donttouchunfixedfiles
490 491 $ cd donttouchunfixedfiles
491 492
492 493 $ printf "NO FIX NEEDED\n" > foo.whole
493 494 $ hg add
494 495 adding foo.whole
495 496 $ cp foo.whole foo.whole.orig
496 497 $ sleep 2 # mtime has a resolution of one or two seconds.
497 498 $ hg fix --working-dir
498 499 $ f foo.whole --newer foo.whole.orig
499 500 foo.whole: older than foo.whole.orig
500 501
501 502 $ cd ..
502 503
503 504 When a fixer prints to stderr, we assume that it has failed. We should show the
504 505 error messages to the user, and we should not let the failing fixer affect the
505 506 file it was fixing (many code formatters might emit error messages on stderr
506 507 and nothing on stdout, which would cause us the clear the file). We show the
507 508 user which fixer failed and which revision, but we assume that the fixer will
508 509 print the filename if it is relevant.
509 510
510 511 $ hg init showstderr
511 512 $ cd showstderr
512 513
513 514 $ printf "hello\n" > hello.txt
514 515 $ hg add
515 516 adding hello.txt
516 517 $ hg --config "fix.fail:command=printf 'HELLO\n' ; \
517 518 > printf '{rootpath}: some\nerror' >&2" \
518 519 > --config "fix.fail:fileset=hello.txt" \
519 520 > fix --working-dir
520 521 [wdir] fail: hello.txt: some
521 522 [wdir] fail: error
522 523 $ cat hello.txt
523 524 hello
524 525
525 526 $ cd ..
526 527
527 528 Fixing the working directory and its parent revision at the same time should
528 529 check out the replacement revision for the parent. This prevents any new
529 530 uncommitted changes from appearing. We test this for a clean working directory
530 531 and a dirty one. In both cases, all lines/files changed since the grandparent
531 532 will be fixed. The grandparent is the "baserev" for both the parent and the
532 533 working copy.
533 534
534 535 $ hg init fixdotandcleanwdir
535 536 $ cd fixdotandcleanwdir
536 537
537 538 $ printf "hello\n" > hello.whole
538 539 $ printf "world\n" > world.whole
539 540 $ hg commit -Aqm "the parent commit"
540 541
541 542 $ hg parents --template '{rev} {desc}\n'
542 543 0 the parent commit
543 544 $ hg fix --working-dir -r .
544 545 $ hg parents --template '{rev} {desc}\n'
545 546 1 the parent commit
546 547 $ hg cat -r . *.whole
547 548 HELLO
548 549 WORLD
549 550 $ cat *.whole
550 551 HELLO
551 552 WORLD
552 553 $ hg status
553 554
554 555 $ cd ..
555 556
556 557 Same test with a dirty working copy.
557 558
558 559 $ hg init fixdotanddirtywdir
559 560 $ cd fixdotanddirtywdir
560 561
561 562 $ printf "hello\n" > hello.whole
562 563 $ printf "world\n" > world.whole
563 564 $ hg commit -Aqm "the parent commit"
564 565
565 566 $ printf "hello,\n" > hello.whole
566 567 $ printf "world!\n" > world.whole
567 568
568 569 $ hg parents --template '{rev} {desc}\n'
569 570 0 the parent commit
570 571 $ hg fix --working-dir -r .
571 572 $ hg parents --template '{rev} {desc}\n'
572 573 1 the parent commit
573 574 $ hg cat -r . *.whole
574 575 HELLO
575 576 WORLD
576 577 $ cat *.whole
577 578 HELLO,
578 579 WORLD!
579 580 $ hg status
580 581 M hello.whole
581 582 M world.whole
582 583
583 584 $ cd ..
584 585
585 586 When we have a chain of commits that change mutually exclusive lines of code,
586 587 we should be able to do incremental fixing that causes each commit in the chain
587 588 to include fixes made to the previous commits. This prevents children from
588 589 backing out the fixes made in their parents. A dirty working directory is
589 590 conceptually similar to another commit in the chain.
590 591
591 592 $ hg init incrementallyfixchain
592 593 $ cd incrementallyfixchain
593 594
594 595 $ cat > file.changed <<EOF
595 596 > first
596 597 > second
597 598 > third
598 599 > fourth
599 600 > fifth
600 601 > EOF
601 602 $ hg commit -Aqm "the common ancestor (the baserev)"
602 603 $ cat > file.changed <<EOF
603 604 > first (changed)
604 605 > second
605 606 > third
606 607 > fourth
607 608 > fifth
608 609 > EOF
609 610 $ hg commit -Aqm "the first commit to fix"
610 611 $ cat > file.changed <<EOF
611 612 > first (changed)
612 613 > second
613 614 > third (changed)
614 615 > fourth
615 616 > fifth
616 617 > EOF
617 618 $ hg commit -Aqm "the second commit to fix"
618 619 $ cat > file.changed <<EOF
619 620 > first (changed)
620 621 > second
621 622 > third (changed)
622 623 > fourth
623 624 > fifth (changed)
624 625 > EOF
625 626
626 627 $ hg fix -r . -r '.^' --working-dir
627 628
628 629 $ hg parents --template '{rev}\n'
629 630 4
630 631 $ hg cat -r '.^^' file.changed
631 632 first
632 633 second
633 634 third
634 635 fourth
635 636 fifth
636 637 $ hg cat -r '.^' file.changed
637 638 FIRST (CHANGED)
638 639 second
639 640 third
640 641 fourth
641 642 fifth
642 643 $ hg cat -r . file.changed
643 644 FIRST (CHANGED)
644 645 second
645 646 THIRD (CHANGED)
646 647 fourth
647 648 fifth
648 649 $ cat file.changed
649 650 FIRST (CHANGED)
650 651 second
651 652 THIRD (CHANGED)
652 653 fourth
653 654 FIFTH (CHANGED)
654 655
655 656 $ cd ..
656 657
657 658 If we incrementally fix a merge commit, we should fix any lines that changed
658 659 versus either parent. You could imagine only fixing the intersection or some
659 660 other subset, but this is necessary if either parent is being fixed. It
660 661 prevents us from forgetting fixes made in either parent.
661 662
662 663 $ hg init incrementallyfixmergecommit
663 664 $ cd incrementallyfixmergecommit
664 665
665 666 $ printf "a\nb\nc\n" > file.changed
666 667 $ hg commit -Aqm "ancestor"
667 668
668 669 $ printf "aa\nb\nc\n" > file.changed
669 670 $ hg commit -m "change a"
670 671
671 672 $ hg checkout '.^'
672 673 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
673 674 $ printf "a\nb\ncc\n" > file.changed
674 675 $ hg commit -m "change c"
675 676 created new head
676 677
677 678 $ hg merge
678 679 merging file.changed
679 680 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
680 681 (branch merge, don't forget to commit)
681 682 $ hg commit -m "merge"
682 683 $ hg cat -r . file.changed
683 684 aa
684 685 b
685 686 cc
686 687
687 688 $ hg fix -r . --working-dir
688 689 $ hg cat -r . file.changed
689 690 AA
690 691 b
691 692 CC
692 693
693 694 $ cd ..
694 695
695 696 Abort fixing revisions if there is an unfinished operation. We don't want to
696 697 make things worse by editing files or stripping/obsoleting things. Also abort
697 698 fixing the working directory if there are unresolved merge conflicts.
698 699
699 700 $ hg init abortunresolved
700 701 $ cd abortunresolved
701 702
702 703 $ echo "foo1" > foo.whole
703 704 $ hg commit -Aqm "foo 1"
704 705
705 706 $ hg update null
706 707 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
707 708 $ echo "foo2" > foo.whole
708 709 $ hg commit -Aqm "foo 2"
709 710
710 711 $ hg --config extensions.rebase= rebase -r 1 -d 0
711 712 rebasing 1:c3b6dc0e177a "foo 2" (tip)
712 713 merging foo.whole
713 714 warning: conflicts while merging foo.whole! (edit, then use 'hg resolve --mark')
714 715 unresolved conflicts (see hg resolve, then hg rebase --continue)
715 716 [1]
716 717
717 718 $ hg --config extensions.rebase= fix --working-dir
718 719 abort: unresolved conflicts
719 720 (use 'hg resolve')
720 721 [255]
721 722
722 723 $ hg --config extensions.rebase= fix -r .
723 724 abort: rebase in progress
724 725 (use 'hg rebase --continue' or 'hg rebase --abort')
725 726 [255]
726 727
727 728 When fixing a file that was renamed, we should diff against the source of the
728 729 rename for incremental fixing and we should correctly reproduce the rename in
729 730 the replacement revision.
730 731
731 732 $ hg init fixrenamecommit
732 733 $ cd fixrenamecommit
733 734
734 735 $ printf "a\nb\nc\n" > source.changed
735 736 $ hg commit -Aqm "source revision"
736 737 $ hg move source.changed dest.changed
737 738 $ printf "a\nb\ncc\n" > dest.changed
738 739 $ hg commit -m "dest revision"
739 740
740 741 $ hg fix -r .
741 742 $ hg log -r tip --copies --template "{file_copies}\n"
742 743 dest.changed (source.changed)
743 744 $ hg cat -r tip dest.changed
744 745 a
745 746 b
746 747 CC
747 748
748 749 $ cd ..
749 750
750 751 When fixing revisions that remove files we must ensure that the replacement
751 752 actually removes the file, whereas it could accidentally leave it unchanged or
752 753 write an empty string to it.
753 754
754 755 $ hg init fixremovedfile
755 756 $ cd fixremovedfile
756 757
757 758 $ printf "foo\n" > foo.whole
758 759 $ printf "bar\n" > bar.whole
759 760 $ hg commit -Aqm "add files"
760 761 $ hg remove bar.whole
761 762 $ hg commit -m "remove file"
762 763 $ hg status --change .
763 764 R bar.whole
764 765 $ hg fix -r . foo.whole
765 766 $ hg status --change tip
766 767 M foo.whole
767 768 R bar.whole
768 769
769 770 $ cd ..
770 771
771 772 If fixing a revision finds no fixes to make, no replacement revision should be
772 773 created.
773 774
774 775 $ hg init nofixesneeded
775 776 $ cd nofixesneeded
776 777
777 778 $ printf "FOO\n" > foo.whole
778 779 $ hg commit -Aqm "add file"
779 780 $ hg log --template '{rev}\n'
780 781 0
781 782 $ hg fix -r .
782 783 $ hg log --template '{rev}\n'
783 784 0
784 785
785 786 $ cd ..
786 787
787 788 If fixing a commit reverts all the changes in the commit, we replace it with a
788 789 commit that changes no files.
789 790
790 791 $ hg init nochangesleft
791 792 $ cd nochangesleft
792 793
793 794 $ printf "FOO\n" > foo.whole
794 795 $ hg commit -Aqm "add file"
795 796 $ printf "foo\n" > foo.whole
796 797 $ hg commit -m "edit file"
797 798 $ hg status --change .
798 799 M foo.whole
799 800 $ hg fix -r .
800 801 $ hg status --change tip
801 802
802 803 $ cd ..
803 804
804 805 If we fix a parent and child revision together, the child revision must be
805 806 replaced if the parent is replaced, even if the diffs of the child needed no
806 807 fixes. However, we're free to not replace revisions that need no fixes and have
807 808 no ancestors that are replaced.
808 809
809 810 $ hg init mustreplacechild
810 811 $ cd mustreplacechild
811 812
812 813 $ printf "FOO\n" > foo.whole
813 814 $ hg commit -Aqm "add foo"
814 815 $ printf "foo\n" > foo.whole
815 816 $ hg commit -m "edit foo"
816 817 $ printf "BAR\n" > bar.whole
817 818 $ hg commit -Aqm "add bar"
818 819
819 820 $ hg log --graph --template '{node|shortest} {files}'
820 821 @ bc05 bar.whole
821 822 |
822 823 o 4fd2 foo.whole
823 824 |
824 825 o f9ac foo.whole
825 826
826 827 $ hg fix -r 0:2
827 828 $ hg log --graph --template '{node|shortest} {files}'
828 829 o 3801 bar.whole
829 830 |
830 831 o 38cc
831 832 |
832 833 | @ bc05 bar.whole
833 834 | |
834 835 | x 4fd2 foo.whole
835 836 |/
836 837 o f9ac foo.whole
837 838
838 839
839 840 $ cd ..
840 841
841 842 It's also possible that the child needs absolutely no changes, but we still
842 843 need to replace it to update its parent. If we skipped replacing the child
843 844 because it had no file content changes, it would become an orphan for no good
844 845 reason.
845 846
846 847 $ hg init mustreplacechildevenifnop
847 848 $ cd mustreplacechildevenifnop
848 849
849 850 $ printf "Foo\n" > foo.whole
850 851 $ hg commit -Aqm "add a bad foo"
851 852 $ printf "FOO\n" > foo.whole
852 853 $ hg commit -m "add a good foo"
853 854 $ hg fix -r . -r '.^'
854 855 $ hg log --graph --template '{rev} {desc}'
855 856 o 3 add a good foo
856 857 |
857 858 o 2 add a bad foo
858 859
859 860 @ 1 add a good foo
860 861 |
861 862 x 0 add a bad foo
862 863
863 864
864 865 $ cd ..
865 866
866 867 Similar to the case above, the child revision may become empty as a result of
867 868 fixing its parent. We should still create an empty replacement child.
868 869 TODO: determine how this should interact with ui.allowemptycommit given that
869 870 the empty replacement could have children.
870 871
871 872 $ hg init mustreplacechildevenifempty
872 873 $ cd mustreplacechildevenifempty
873 874
874 875 $ printf "foo\n" > foo.whole
875 876 $ hg commit -Aqm "add foo"
876 877 $ printf "Foo\n" > foo.whole
877 878 $ hg commit -m "edit foo"
878 879 $ hg fix -r . -r '.^'
879 880 $ hg log --graph --template '{rev} {desc}\n' --stat
880 881 o 3 edit foo
881 882 |
882 883 o 2 add foo
883 884 foo.whole | 1 +
884 885 1 files changed, 1 insertions(+), 0 deletions(-)
885 886
886 887 @ 1 edit foo
887 888 | foo.whole | 2 +-
888 889 | 1 files changed, 1 insertions(+), 1 deletions(-)
889 890 |
890 891 x 0 add foo
891 892 foo.whole | 1 +
892 893 1 files changed, 1 insertions(+), 0 deletions(-)
893 894
894 895
895 896 $ cd ..
896 897
897 898 Fixing a secret commit should replace it with another secret commit.
898 899
899 900 $ hg init fixsecretcommit
900 901 $ cd fixsecretcommit
901 902
902 903 $ printf "foo\n" > foo.whole
903 904 $ hg commit -Aqm "add foo" --secret
904 905 $ hg fix -r .
905 906 $ hg log --template '{rev} {phase}\n'
906 907 1 secret
907 908 0 secret
908 909
909 910 $ cd ..
910 911
911 912 We should also preserve phase when fixing a draft commit while the user has
912 913 their default set to secret.
913 914
914 915 $ hg init respectphasesnewcommit
915 916 $ cd respectphasesnewcommit
916 917
917 918 $ printf "foo\n" > foo.whole
918 919 $ hg commit -Aqm "add foo"
919 920 $ hg --config phases.newcommit=secret fix -r .
920 921 $ hg log --template '{rev} {phase}\n'
921 922 1 draft
922 923 0 draft
923 924
924 925 $ cd ..
925 926
926 927 Debug output should show what fixer commands are being subprocessed, which is
927 928 useful for anyone trying to set up a new config.
928 929
929 930 $ hg init debugoutput
930 931 $ cd debugoutput
931 932
932 933 $ printf "foo\nbar\nbaz\n" > foo.changed
933 934 $ hg commit -Aqm "foo"
934 935 $ printf "Foo\nbar\nBaz\n" > foo.changed
935 936 $ hg --debug fix --working-dir
936 937 subprocess: * $TESTTMP/uppercase.py 1-1 3-3 (glob)
937 938
938 939 $ cd ..
939 940
940 941 Fixing an obsolete revision can cause divergence, so we abort unless the user
941 942 configures to allow it. This is not yet smart enough to know whether there is a
942 943 successor, but even then it is not likely intentional or idiomatic to fix an
943 944 obsolete revision.
944 945
945 946 $ hg init abortobsoleterev
946 947 $ cd abortobsoleterev
947 948
948 949 $ printf "foo\n" > foo.changed
949 950 $ hg commit -Aqm "foo"
950 951 $ hg debugobsolete `hg parents --template '{node}'`
951 952 obsoleted 1 changesets
952 953 $ hg --hidden fix -r 0
953 954 abort: fixing obsolete revision could cause divergence
954 955 [255]
955 956
956 957 $ hg --hidden fix -r 0 --config experimental.evolution.allowdivergence=true
957 958 $ hg cat -r tip foo.changed
958 959 FOO
959 960
960 961 $ cd ..
961 962
962 963 Test all of the available substitution values for fixer commands.
963 964
964 965 $ hg init substitution
965 966 $ cd substitution
966 967
967 968 $ mkdir foo
968 969 $ printf "hello\ngoodbye\n" > foo/bar
969 970 $ hg add
970 971 adding foo/bar
971 972 $ hg --config "fix.fail:command=printf '%s\n' '{rootpath}' '{basename}'" \
972 973 > --config "fix.fail:linerange='{first}' '{last}'" \
973 974 > --config "fix.fail:fileset=foo/bar" \
974 975 > fix --working-dir
975 976 $ cat foo/bar
976 977 foo/bar
977 978 bar
978 979 1
979 980 2
980 981
981 982 $ cd ..
982 983
983 984 The --base flag should allow picking the revisions to diff against for changed
984 985 files and incremental line formatting.
985 986
986 987 $ hg init baseflag
987 988 $ cd baseflag
988 989
989 990 $ printf "one\ntwo\n" > foo.changed
990 991 $ printf "bar\n" > bar.changed
991 992 $ hg commit -Aqm "first"
992 993 $ printf "one\nTwo\n" > foo.changed
993 994 $ hg commit -m "second"
994 995 $ hg fix -w --base .
995 996 $ hg status
996 997 $ hg fix -w --base null
997 998 $ cat foo.changed
998 999 ONE
999 1000 TWO
1000 1001 $ cat bar.changed
1001 1002 BAR
1002 1003
1003 1004 $ cd ..
1004 1005
1005 1006 If the user asks to fix the parent of another commit, they are asking to create
1006 1007 an orphan. We must respect experimental.evolution.allowunstable.
1007 1008
1008 1009 $ hg init allowunstable
1009 1010 $ cd allowunstable
1010 1011
1011 1012 $ printf "one\n" > foo.whole
1012 1013 $ hg commit -Aqm "first"
1013 1014 $ printf "two\n" > foo.whole
1014 1015 $ hg commit -m "second"
1015 1016 $ hg --config experimental.evolution.allowunstable=False fix -r '.^'
1016 1017 abort: can only fix a changeset together with all its descendants
1017 1018 [255]
1018 1019 $ hg fix -r '.^'
1019 1020 1 new orphan changesets
1020 1021 $ hg cat -r 2 foo.whole
1021 1022 ONE
1022 1023
1023 1024 $ cd ..
1024 1025
General Comments 0
You need to be logged in to leave comments. Login now