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