##// END OF EJS Templates
fix: correctly set wdirwritten given that the dict item is deleted...
Danny Hooper -
r38985:35bc4b6e default
parent child Browse files
Show More
@@ -1,603 +1,605
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 worker,
73 worker,
74 )
74 )
75
75
76 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
76 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
77 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
77 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
78 # be specifying the version(s) of Mercurial they are tested with, or
78 # be specifying the version(s) of Mercurial they are tested with, or
79 # leave the attribute unspecified.
79 # leave the attribute unspecified.
80 testedwith = 'ships-with-hg-core'
80 testedwith = 'ships-with-hg-core'
81
81
82 cmdtable = {}
82 cmdtable = {}
83 command = registrar.command(cmdtable)
83 command = registrar.command(cmdtable)
84
84
85 configtable = {}
85 configtable = {}
86 configitem = registrar.configitem(configtable)
86 configitem = registrar.configitem(configtable)
87
87
88 # Register the suboptions allowed for each configured fixer.
88 # Register the suboptions allowed for each configured fixer.
89 FIXER_ATTRS = ('command', 'linerange', 'fileset')
89 FIXER_ATTRS = ('command', 'linerange', 'fileset')
90
90
91 for key in FIXER_ATTRS:
91 for key in FIXER_ATTRS:
92 configitem('fix', '.*(:%s)?' % key, default=None, generic=True)
92 configitem('fix', '.*(:%s)?' % key, default=None, generic=True)
93
93
94 # A good default size allows most source code files to be fixed, but avoids
94 # A good default size allows most source code files to be fixed, but avoids
95 # letting fixer tools choke on huge inputs, which could be surprising to the
95 # letting fixer tools choke on huge inputs, which could be surprising to the
96 # user.
96 # user.
97 configitem('fix', 'maxfilesize', default='2MB')
97 configitem('fix', 'maxfilesize', default='2MB')
98
98
99 allopt = ('', 'all', False, _('fix all non-public non-obsolete revisions'))
99 allopt = ('', 'all', False, _('fix all non-public non-obsolete revisions'))
100 baseopt = ('', 'base', [], _('revisions to diff against (overrides automatic '
100 baseopt = ('', '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 revopt = ('r', 'rev', [], _('revisions to fix'), _('REV'))
103 revopt = ('r', 'rev', [], _('revisions to fix'), _('REV'))
104 wdiropt = ('w', 'working-dir', False, _('fix the working directory'))
104 wdiropt = ('w', 'working-dir', False, _('fix the working directory'))
105 wholeopt = ('', 'whole', False, _('always fix every line of a file'))
105 wholeopt = ('', 'whole', False, _('always fix every line of a file'))
106 usage = _('[OPTION]... [FILE]...')
106 usage = _('[OPTION]... [FILE]...')
107
107
108 @command('fix', [allopt, baseopt, revopt, wdiropt, wholeopt], usage)
108 @command('fix', [allopt, baseopt, revopt, wdiropt, wholeopt], usage)
109 def fix(ui, repo, *pats, **opts):
109 def fix(ui, repo, *pats, **opts):
110 """rewrite file content in changesets or working directory
110 """rewrite file content in changesets or working directory
111
111
112 Runs any configured tools to fix the content of files. Only affects files
112 Runs any configured tools to fix the content of files. Only affects files
113 with changes, unless file arguments are provided. Only affects changed lines
113 with changes, unless file arguments are provided. Only affects changed lines
114 of files, unless the --whole flag is used. Some tools may always affect the
114 of files, unless the --whole flag is used. Some tools may always affect the
115 whole file regardless of --whole.
115 whole file regardless of --whole.
116
116
117 If revisions are specified with --rev, those revisions will be checked, and
117 If revisions are specified with --rev, those revisions will be checked, and
118 they may be replaced with new revisions that have fixed file content. It is
118 they may be replaced with new revisions that have fixed file content. It is
119 desirable to specify all descendants of each specified revision, so that the
119 desirable to specify all descendants of each specified revision, so that the
120 fixes propagate to the descendants. If all descendants are fixed at the same
120 fixes propagate to the descendants. If all descendants are fixed at the same
121 time, no merging, rebasing, or evolution will be required.
121 time, no merging, rebasing, or evolution will be required.
122
122
123 If --working-dir is used, files with uncommitted changes in the working copy
123 If --working-dir is used, files with uncommitted changes in the working copy
124 will be fixed. If the checked-out revision is also fixed, the working
124 will be fixed. If the checked-out revision is also fixed, the working
125 directory will update to the replacement revision.
125 directory will update to the replacement revision.
126
126
127 When determining what lines of each file to fix at each revision, the whole
127 When determining what lines of each file to fix at each revision, the whole
128 set of revisions being fixed is considered, so that fixes to earlier
128 set of revisions being fixed is considered, so that fixes to earlier
129 revisions are not forgotten in later ones. The --base flag can be used to
129 revisions are not forgotten in later ones. The --base flag can be used to
130 override this default behavior, though it is not usually desirable to do so.
130 override this default behavior, though it is not usually desirable to do so.
131 """
131 """
132 opts = pycompat.byteskwargs(opts)
132 opts = pycompat.byteskwargs(opts)
133 if opts['all']:
133 if opts['all']:
134 if opts['rev']:
134 if opts['rev']:
135 raise error.Abort(_('cannot specify both "--rev" and "--all"'))
135 raise error.Abort(_('cannot specify both "--rev" and "--all"'))
136 opts['rev'] = ['not public() and not obsolete()']
136 opts['rev'] = ['not public() and not obsolete()']
137 opts['working_dir'] = True
137 opts['working_dir'] = True
138 with repo.wlock(), repo.lock(), repo.transaction('fix'):
138 with repo.wlock(), repo.lock(), repo.transaction('fix'):
139 revstofix = getrevstofix(ui, repo, opts)
139 revstofix = getrevstofix(ui, repo, opts)
140 basectxs = getbasectxs(repo, opts, revstofix)
140 basectxs = getbasectxs(repo, opts, revstofix)
141 workqueue, numitems = getworkqueue(ui, repo, pats, opts, revstofix,
141 workqueue, numitems = getworkqueue(ui, repo, pats, opts, revstofix,
142 basectxs)
142 basectxs)
143 fixers = getfixers(ui)
143 fixers = getfixers(ui)
144
144
145 # There are no data dependencies between the workers fixing each file
145 # There are no data dependencies between the workers fixing each file
146 # revision, so we can use all available parallelism.
146 # revision, so we can use all available parallelism.
147 def getfixes(items):
147 def getfixes(items):
148 for rev, path in items:
148 for rev, path in items:
149 ctx = repo[rev]
149 ctx = repo[rev]
150 olddata = ctx[path].data()
150 olddata = ctx[path].data()
151 newdata = fixfile(ui, opts, fixers, ctx, path, basectxs[rev])
151 newdata = fixfile(ui, opts, fixers, ctx, path, basectxs[rev])
152 # Don't waste memory/time passing unchanged content back, but
152 # Don't waste memory/time passing unchanged content back, but
153 # produce one result per item either way.
153 # produce one result per item either way.
154 yield (rev, path, newdata if newdata != olddata else None)
154 yield (rev, path, newdata if newdata != olddata else None)
155 results = worker.worker(ui, 1.0, getfixes, tuple(), workqueue)
155 results = worker.worker(ui, 1.0, getfixes, tuple(), workqueue)
156
156
157 # We have to hold on to the data for each successor revision in memory
157 # We have to hold on to the data for each successor revision in memory
158 # until all its parents are committed. We ensure this by committing and
158 # until all its parents are committed. We ensure this by committing and
159 # freeing memory for the revisions in some topological order. This
159 # freeing memory for the revisions in some topological order. This
160 # leaves a little bit of memory efficiency on the table, but also makes
160 # leaves a little bit of memory efficiency on the table, but also makes
161 # the tests deterministic. It might also be considered a feature since
161 # the tests deterministic. It might also be considered a feature since
162 # it makes the results more easily reproducible.
162 # it makes the results more easily reproducible.
163 filedata = collections.defaultdict(dict)
163 filedata = collections.defaultdict(dict)
164 replacements = {}
164 replacements = {}
165 wdirwritten = False
165 commitorder = sorted(revstofix, reverse=True)
166 commitorder = sorted(revstofix, reverse=True)
166 with ui.makeprogress(topic=_('fixing'), unit=_('files'),
167 with ui.makeprogress(topic=_('fixing'), unit=_('files'),
167 total=sum(numitems.values())) as progress:
168 total=sum(numitems.values())) as progress:
168 for rev, path, newdata in results:
169 for rev, path, newdata in results:
169 progress.increment(item=path)
170 progress.increment(item=path)
170 if newdata is not None:
171 if newdata is not None:
171 filedata[rev][path] = newdata
172 filedata[rev][path] = newdata
172 numitems[rev] -= 1
173 numitems[rev] -= 1
173 # Apply the fixes for this and any other revisions that are
174 # Apply the fixes for this and any other revisions that are
174 # ready and sitting at the front of the queue. Using a loop here
175 # ready and sitting at the front of the queue. Using a loop here
175 # prevents the queue from being blocked by the first revision to
176 # prevents the queue from being blocked by the first revision to
176 # be ready out of order.
177 # be ready out of order.
177 while commitorder and not numitems[commitorder[-1]]:
178 while commitorder and not numitems[commitorder[-1]]:
178 rev = commitorder.pop()
179 rev = commitorder.pop()
179 ctx = repo[rev]
180 ctx = repo[rev]
180 if rev == wdirrev:
181 if rev == wdirrev:
181 writeworkingdir(repo, ctx, filedata[rev], replacements)
182 writeworkingdir(repo, ctx, filedata[rev], replacements)
183 wdirwritten = bool(filedata[rev])
182 else:
184 else:
183 replacerev(ui, repo, ctx, filedata[rev], replacements)
185 replacerev(ui, repo, ctx, filedata[rev], replacements)
184 del filedata[rev]
186 del filedata[rev]
185
187
186 cleanup(repo, replacements, bool(filedata[wdirrev]))
188 cleanup(repo, replacements, wdirwritten)
187
189
188 def cleanup(repo, replacements, wdirwritten):
190 def cleanup(repo, replacements, wdirwritten):
189 """Calls scmutil.cleanupnodes() with the given replacements.
191 """Calls scmutil.cleanupnodes() with the given replacements.
190
192
191 "replacements" is a dict from nodeid to nodeid, with one key and one value
193 "replacements" is a dict from nodeid to nodeid, with one key and one value
192 for every revision that was affected by fixing. This is slightly different
194 for every revision that was affected by fixing. This is slightly different
193 from cleanupnodes().
195 from cleanupnodes().
194
196
195 "wdirwritten" is a bool which tells whether the working copy was affected by
197 "wdirwritten" is a bool which tells whether the working copy was affected by
196 fixing, since it has no entry in "replacements".
198 fixing, since it has no entry in "replacements".
197
199
198 Useful as a hook point for extending "hg fix" with output summarizing the
200 Useful as a hook point for extending "hg fix" with output summarizing the
199 effects of the command, though we choose not to output anything here.
201 effects of the command, though we choose not to output anything here.
200 """
202 """
201 replacements = {prec: [succ] for prec, succ in replacements.iteritems()}
203 replacements = {prec: [succ] for prec, succ in replacements.iteritems()}
202 scmutil.cleanupnodes(repo, replacements, 'fix', fixphase=True)
204 scmutil.cleanupnodes(repo, replacements, 'fix', fixphase=True)
203
205
204 def getworkqueue(ui, repo, pats, opts, revstofix, basectxs):
206 def getworkqueue(ui, repo, pats, opts, revstofix, basectxs):
205 """"Constructs the list of files to be fixed at specific revisions
207 """"Constructs the list of files to be fixed at specific revisions
206
208
207 It is up to the caller how to consume the work items, and the only
209 It is up to the caller how to consume the work items, and the only
208 dependence between them is that replacement revisions must be committed in
210 dependence between them is that replacement revisions must be committed in
209 topological order. Each work item represents a file in the working copy or
211 topological order. Each work item represents a file in the working copy or
210 in some revision that should be fixed and written back to the working copy
212 in some revision that should be fixed and written back to the working copy
211 or into a replacement revision.
213 or into a replacement revision.
212
214
213 Work items for the same revision are grouped together, so that a worker
215 Work items for the same revision are grouped together, so that a worker
214 pool starting with the first N items in parallel is likely to finish the
216 pool starting with the first N items in parallel is likely to finish the
215 first revision's work before other revisions. This can allow us to write
217 first revision's work before other revisions. This can allow us to write
216 the result to disk and reduce memory footprint. At time of writing, the
218 the result to disk and reduce memory footprint. At time of writing, the
217 partition strategy in worker.py seems favorable to this. We also sort the
219 partition strategy in worker.py seems favorable to this. We also sort the
218 items by ascending revision number to match the order in which we commit
220 items by ascending revision number to match the order in which we commit
219 the fixes later.
221 the fixes later.
220 """
222 """
221 workqueue = []
223 workqueue = []
222 numitems = collections.defaultdict(int)
224 numitems = collections.defaultdict(int)
223 maxfilesize = ui.configbytes('fix', 'maxfilesize')
225 maxfilesize = ui.configbytes('fix', 'maxfilesize')
224 for rev in sorted(revstofix):
226 for rev in sorted(revstofix):
225 fixctx = repo[rev]
227 fixctx = repo[rev]
226 match = scmutil.match(fixctx, pats, opts)
228 match = scmutil.match(fixctx, pats, opts)
227 for path in pathstofix(ui, repo, pats, opts, match, basectxs[rev],
229 for path in pathstofix(ui, repo, pats, opts, match, basectxs[rev],
228 fixctx):
230 fixctx):
229 if path not in fixctx:
231 if path not in fixctx:
230 continue
232 continue
231 fctx = fixctx[path]
233 fctx = fixctx[path]
232 if fctx.islink():
234 if fctx.islink():
233 continue
235 continue
234 if fctx.size() > maxfilesize:
236 if fctx.size() > maxfilesize:
235 ui.warn(_('ignoring file larger than %s: %s\n') %
237 ui.warn(_('ignoring file larger than %s: %s\n') %
236 (util.bytecount(maxfilesize), path))
238 (util.bytecount(maxfilesize), path))
237 continue
239 continue
238 workqueue.append((rev, path))
240 workqueue.append((rev, path))
239 numitems[rev] += 1
241 numitems[rev] += 1
240 return workqueue, numitems
242 return workqueue, numitems
241
243
242 def getrevstofix(ui, repo, opts):
244 def getrevstofix(ui, repo, opts):
243 """Returns the set of revision numbers that should be fixed"""
245 """Returns the set of revision numbers that should be fixed"""
244 revs = set(scmutil.revrange(repo, opts['rev']))
246 revs = set(scmutil.revrange(repo, opts['rev']))
245 for rev in revs:
247 for rev in revs:
246 checkfixablectx(ui, repo, repo[rev])
248 checkfixablectx(ui, repo, repo[rev])
247 if revs:
249 if revs:
248 cmdutil.checkunfinished(repo)
250 cmdutil.checkunfinished(repo)
249 checknodescendants(repo, revs)
251 checknodescendants(repo, revs)
250 if opts.get('working_dir'):
252 if opts.get('working_dir'):
251 revs.add(wdirrev)
253 revs.add(wdirrev)
252 if list(merge.mergestate.read(repo).unresolved()):
254 if list(merge.mergestate.read(repo).unresolved()):
253 raise error.Abort('unresolved conflicts', hint="use 'hg resolve'")
255 raise error.Abort('unresolved conflicts', hint="use 'hg resolve'")
254 if not revs:
256 if not revs:
255 raise error.Abort(
257 raise error.Abort(
256 'no changesets specified', hint='use --rev or --working-dir')
258 'no changesets specified', hint='use --rev or --working-dir')
257 return revs
259 return revs
258
260
259 def checknodescendants(repo, revs):
261 def checknodescendants(repo, revs):
260 if (not obsolete.isenabled(repo, obsolete.allowunstableopt) and
262 if (not obsolete.isenabled(repo, obsolete.allowunstableopt) and
261 repo.revs('(%ld::) - (%ld)', revs, revs)):
263 repo.revs('(%ld::) - (%ld)', revs, revs)):
262 raise error.Abort(_('can only fix a changeset together '
264 raise error.Abort(_('can only fix a changeset together '
263 'with all its descendants'))
265 'with all its descendants'))
264
266
265 def checkfixablectx(ui, repo, ctx):
267 def checkfixablectx(ui, repo, ctx):
266 """Aborts if the revision shouldn't be replaced with a fixed one."""
268 """Aborts if the revision shouldn't be replaced with a fixed one."""
267 if not ctx.mutable():
269 if not ctx.mutable():
268 raise error.Abort('can\'t fix immutable changeset %s' %
270 raise error.Abort('can\'t fix immutable changeset %s' %
269 (scmutil.formatchangeid(ctx),))
271 (scmutil.formatchangeid(ctx),))
270 if ctx.obsolete():
272 if ctx.obsolete():
271 # It would be better to actually check if the revision has a successor.
273 # It would be better to actually check if the revision has a successor.
272 allowdivergence = ui.configbool('experimental',
274 allowdivergence = ui.configbool('experimental',
273 'evolution.allowdivergence')
275 'evolution.allowdivergence')
274 if not allowdivergence:
276 if not allowdivergence:
275 raise error.Abort('fixing obsolete revision could cause divergence')
277 raise error.Abort('fixing obsolete revision could cause divergence')
276
278
277 def pathstofix(ui, repo, pats, opts, match, basectxs, fixctx):
279 def pathstofix(ui, repo, pats, opts, match, basectxs, fixctx):
278 """Returns the set of files that should be fixed in a context
280 """Returns the set of files that should be fixed in a context
279
281
280 The result depends on the base contexts; we include any file that has
282 The result depends on the base contexts; we include any file that has
281 changed relative to any of the base contexts. Base contexts should be
283 changed relative to any of the base contexts. Base contexts should be
282 ancestors of the context being fixed.
284 ancestors of the context being fixed.
283 """
285 """
284 files = set()
286 files = set()
285 for basectx in basectxs:
287 for basectx in basectxs:
286 stat = basectx.status(fixctx, match=match, listclean=bool(pats),
288 stat = basectx.status(fixctx, match=match, listclean=bool(pats),
287 listunknown=bool(pats))
289 listunknown=bool(pats))
288 files.update(
290 files.update(
289 set(itertools.chain(stat.added, stat.modified, stat.clean,
291 set(itertools.chain(stat.added, stat.modified, stat.clean,
290 stat.unknown)))
292 stat.unknown)))
291 return files
293 return files
292
294
293 def lineranges(opts, path, basectxs, fixctx, content2):
295 def lineranges(opts, path, basectxs, fixctx, content2):
294 """Returns the set of line ranges that should be fixed in a file
296 """Returns the set of line ranges that should be fixed in a file
295
297
296 Of the form [(10, 20), (30, 40)].
298 Of the form [(10, 20), (30, 40)].
297
299
298 This depends on the given base contexts; we must consider lines that have
300 This depends on the given base contexts; we must consider lines that have
299 changed versus any of the base contexts, and whether the file has been
301 changed versus any of the base contexts, and whether the file has been
300 renamed versus any of them.
302 renamed versus any of them.
301
303
302 Another way to understand this is that we exclude line ranges that are
304 Another way to understand this is that we exclude line ranges that are
303 common to the file in all base contexts.
305 common to the file in all base contexts.
304 """
306 """
305 if opts.get('whole'):
307 if opts.get('whole'):
306 # Return a range containing all lines. Rely on the diff implementation's
308 # Return a range containing all lines. Rely on the diff implementation's
307 # idea of how many lines are in the file, instead of reimplementing it.
309 # idea of how many lines are in the file, instead of reimplementing it.
308 return difflineranges('', content2)
310 return difflineranges('', content2)
309
311
310 rangeslist = []
312 rangeslist = []
311 for basectx in basectxs:
313 for basectx in basectxs:
312 basepath = copies.pathcopies(basectx, fixctx).get(path, path)
314 basepath = copies.pathcopies(basectx, fixctx).get(path, path)
313 if basepath in basectx:
315 if basepath in basectx:
314 content1 = basectx[basepath].data()
316 content1 = basectx[basepath].data()
315 else:
317 else:
316 content1 = ''
318 content1 = ''
317 rangeslist.extend(difflineranges(content1, content2))
319 rangeslist.extend(difflineranges(content1, content2))
318 return unionranges(rangeslist)
320 return unionranges(rangeslist)
319
321
320 def unionranges(rangeslist):
322 def unionranges(rangeslist):
321 """Return the union of some closed intervals
323 """Return the union of some closed intervals
322
324
323 >>> unionranges([])
325 >>> unionranges([])
324 []
326 []
325 >>> unionranges([(1, 100)])
327 >>> unionranges([(1, 100)])
326 [(1, 100)]
328 [(1, 100)]
327 >>> unionranges([(1, 100), (1, 100)])
329 >>> unionranges([(1, 100), (1, 100)])
328 [(1, 100)]
330 [(1, 100)]
329 >>> unionranges([(1, 100), (2, 100)])
331 >>> unionranges([(1, 100), (2, 100)])
330 [(1, 100)]
332 [(1, 100)]
331 >>> unionranges([(1, 99), (1, 100)])
333 >>> unionranges([(1, 99), (1, 100)])
332 [(1, 100)]
334 [(1, 100)]
333 >>> unionranges([(1, 100), (40, 60)])
335 >>> unionranges([(1, 100), (40, 60)])
334 [(1, 100)]
336 [(1, 100)]
335 >>> unionranges([(1, 49), (50, 100)])
337 >>> unionranges([(1, 49), (50, 100)])
336 [(1, 100)]
338 [(1, 100)]
337 >>> unionranges([(1, 48), (50, 100)])
339 >>> unionranges([(1, 48), (50, 100)])
338 [(1, 48), (50, 100)]
340 [(1, 48), (50, 100)]
339 >>> unionranges([(1, 2), (3, 4), (5, 6)])
341 >>> unionranges([(1, 2), (3, 4), (5, 6)])
340 [(1, 6)]
342 [(1, 6)]
341 """
343 """
342 rangeslist = sorted(set(rangeslist))
344 rangeslist = sorted(set(rangeslist))
343 unioned = []
345 unioned = []
344 if rangeslist:
346 if rangeslist:
345 unioned, rangeslist = [rangeslist[0]], rangeslist[1:]
347 unioned, rangeslist = [rangeslist[0]], rangeslist[1:]
346 for a, b in rangeslist:
348 for a, b in rangeslist:
347 c, d = unioned[-1]
349 c, d = unioned[-1]
348 if a > d + 1:
350 if a > d + 1:
349 unioned.append((a, b))
351 unioned.append((a, b))
350 else:
352 else:
351 unioned[-1] = (c, max(b, d))
353 unioned[-1] = (c, max(b, d))
352 return unioned
354 return unioned
353
355
354 def difflineranges(content1, content2):
356 def difflineranges(content1, content2):
355 """Return list of line number ranges in content2 that differ from content1.
357 """Return list of line number ranges in content2 that differ from content1.
356
358
357 Line numbers are 1-based. The numbers are the first and last line contained
359 Line numbers are 1-based. The numbers are the first and last line contained
358 in the range. Single-line ranges have the same line number for the first and
360 in the range. Single-line ranges have the same line number for the first and
359 last line. Excludes any empty ranges that result from lines that are only
361 last line. Excludes any empty ranges that result from lines that are only
360 present in content1. Relies on mdiff's idea of where the line endings are in
362 present in content1. Relies on mdiff's idea of where the line endings are in
361 the string.
363 the string.
362
364
363 >>> from mercurial import pycompat
365 >>> from mercurial import pycompat
364 >>> lines = lambda s: b'\\n'.join([c for c in pycompat.iterbytestr(s)])
366 >>> lines = lambda s: b'\\n'.join([c for c in pycompat.iterbytestr(s)])
365 >>> difflineranges2 = lambda a, b: difflineranges(lines(a), lines(b))
367 >>> difflineranges2 = lambda a, b: difflineranges(lines(a), lines(b))
366 >>> difflineranges2(b'', b'')
368 >>> difflineranges2(b'', b'')
367 []
369 []
368 >>> difflineranges2(b'a', b'')
370 >>> difflineranges2(b'a', b'')
369 []
371 []
370 >>> difflineranges2(b'', b'A')
372 >>> difflineranges2(b'', b'A')
371 [(1, 1)]
373 [(1, 1)]
372 >>> difflineranges2(b'a', b'a')
374 >>> difflineranges2(b'a', b'a')
373 []
375 []
374 >>> difflineranges2(b'a', b'A')
376 >>> difflineranges2(b'a', b'A')
375 [(1, 1)]
377 [(1, 1)]
376 >>> difflineranges2(b'ab', b'')
378 >>> difflineranges2(b'ab', b'')
377 []
379 []
378 >>> difflineranges2(b'', b'AB')
380 >>> difflineranges2(b'', b'AB')
379 [(1, 2)]
381 [(1, 2)]
380 >>> difflineranges2(b'abc', b'ac')
382 >>> difflineranges2(b'abc', b'ac')
381 []
383 []
382 >>> difflineranges2(b'ab', b'aCb')
384 >>> difflineranges2(b'ab', b'aCb')
383 [(2, 2)]
385 [(2, 2)]
384 >>> difflineranges2(b'abc', b'aBc')
386 >>> difflineranges2(b'abc', b'aBc')
385 [(2, 2)]
387 [(2, 2)]
386 >>> difflineranges2(b'ab', b'AB')
388 >>> difflineranges2(b'ab', b'AB')
387 [(1, 2)]
389 [(1, 2)]
388 >>> difflineranges2(b'abcde', b'aBcDe')
390 >>> difflineranges2(b'abcde', b'aBcDe')
389 [(2, 2), (4, 4)]
391 [(2, 2), (4, 4)]
390 >>> difflineranges2(b'abcde', b'aBCDe')
392 >>> difflineranges2(b'abcde', b'aBCDe')
391 [(2, 4)]
393 [(2, 4)]
392 """
394 """
393 ranges = []
395 ranges = []
394 for lines, kind in mdiff.allblocks(content1, content2):
396 for lines, kind in mdiff.allblocks(content1, content2):
395 firstline, lastline = lines[2:4]
397 firstline, lastline = lines[2:4]
396 if kind == '!' and firstline != lastline:
398 if kind == '!' and firstline != lastline:
397 ranges.append((firstline + 1, lastline))
399 ranges.append((firstline + 1, lastline))
398 return ranges
400 return ranges
399
401
400 def getbasectxs(repo, opts, revstofix):
402 def getbasectxs(repo, opts, revstofix):
401 """Returns a map of the base contexts for each revision
403 """Returns a map of the base contexts for each revision
402
404
403 The base contexts determine which lines are considered modified when we
405 The base contexts determine which lines are considered modified when we
404 attempt to fix just the modified lines in a file. It also determines which
406 attempt to fix just the modified lines in a file. It also determines which
405 files we attempt to fix, so it is important to compute this even when
407 files we attempt to fix, so it is important to compute this even when
406 --whole is used.
408 --whole is used.
407 """
409 """
408 # The --base flag overrides the usual logic, and we give every revision
410 # The --base flag overrides the usual logic, and we give every revision
409 # exactly the set of baserevs that the user specified.
411 # exactly the set of baserevs that the user specified.
410 if opts.get('base'):
412 if opts.get('base'):
411 baserevs = set(scmutil.revrange(repo, opts.get('base')))
413 baserevs = set(scmutil.revrange(repo, opts.get('base')))
412 if not baserevs:
414 if not baserevs:
413 baserevs = {nullrev}
415 baserevs = {nullrev}
414 basectxs = {repo[rev] for rev in baserevs}
416 basectxs = {repo[rev] for rev in baserevs}
415 return {rev: basectxs for rev in revstofix}
417 return {rev: basectxs for rev in revstofix}
416
418
417 # Proceed in topological order so that we can easily determine each
419 # Proceed in topological order so that we can easily determine each
418 # revision's baserevs by looking at its parents and their baserevs.
420 # revision's baserevs by looking at its parents and their baserevs.
419 basectxs = collections.defaultdict(set)
421 basectxs = collections.defaultdict(set)
420 for rev in sorted(revstofix):
422 for rev in sorted(revstofix):
421 ctx = repo[rev]
423 ctx = repo[rev]
422 for pctx in ctx.parents():
424 for pctx in ctx.parents():
423 if pctx.rev() in basectxs:
425 if pctx.rev() in basectxs:
424 basectxs[rev].update(basectxs[pctx.rev()])
426 basectxs[rev].update(basectxs[pctx.rev()])
425 else:
427 else:
426 basectxs[rev].add(pctx)
428 basectxs[rev].add(pctx)
427 return basectxs
429 return basectxs
428
430
429 def fixfile(ui, opts, fixers, fixctx, path, basectxs):
431 def fixfile(ui, opts, fixers, fixctx, path, basectxs):
430 """Run any configured fixers that should affect the file in this context
432 """Run any configured fixers that should affect the file in this context
431
433
432 Returns the file content that results from applying the fixers in some order
434 Returns the file content that results from applying the fixers in some order
433 starting with the file's content in the fixctx. Fixers that support line
435 starting with the file's content in the fixctx. Fixers that support line
434 ranges will affect lines that have changed relative to any of the basectxs
436 ranges will affect lines that have changed relative to any of the basectxs
435 (i.e. they will only avoid lines that are common to all basectxs).
437 (i.e. they will only avoid lines that are common to all basectxs).
436 """
438 """
437 newdata = fixctx[path].data()
439 newdata = fixctx[path].data()
438 for fixername, fixer in fixers.iteritems():
440 for fixername, fixer in fixers.iteritems():
439 if fixer.affects(opts, fixctx, path):
441 if fixer.affects(opts, fixctx, path):
440 rangesfn = lambda: lineranges(opts, path, basectxs, fixctx, newdata)
442 rangesfn = lambda: lineranges(opts, path, basectxs, fixctx, newdata)
441 command = fixer.command(ui, path, rangesfn)
443 command = fixer.command(ui, path, rangesfn)
442 if command is None:
444 if command is None:
443 continue
445 continue
444 ui.debug('subprocess: %s\n' % (command,))
446 ui.debug('subprocess: %s\n' % (command,))
445 proc = subprocess.Popen(
447 proc = subprocess.Popen(
446 command,
448 command,
447 shell=True,
449 shell=True,
448 cwd='/',
450 cwd='/',
449 stdin=subprocess.PIPE,
451 stdin=subprocess.PIPE,
450 stdout=subprocess.PIPE,
452 stdout=subprocess.PIPE,
451 stderr=subprocess.PIPE)
453 stderr=subprocess.PIPE)
452 newerdata, stderr = proc.communicate(newdata)
454 newerdata, stderr = proc.communicate(newdata)
453 if stderr:
455 if stderr:
454 showstderr(ui, fixctx.rev(), fixername, stderr)
456 showstderr(ui, fixctx.rev(), fixername, stderr)
455 else:
457 else:
456 newdata = newerdata
458 newdata = newerdata
457 return newdata
459 return newdata
458
460
459 def showstderr(ui, rev, fixername, stderr):
461 def showstderr(ui, rev, fixername, stderr):
460 """Writes the lines of the stderr string as warnings on the ui
462 """Writes the lines of the stderr string as warnings on the ui
461
463
462 Uses the revision number and fixername to give more context to each line of
464 Uses the revision number and fixername to give more context to each line of
463 the error message. Doesn't include file names, since those take up a lot of
465 the error message. Doesn't include file names, since those take up a lot of
464 space and would tend to be included in the error message if they were
466 space and would tend to be included in the error message if they were
465 relevant.
467 relevant.
466 """
468 """
467 for line in re.split('[\r\n]+', stderr):
469 for line in re.split('[\r\n]+', stderr):
468 if line:
470 if line:
469 ui.warn(('['))
471 ui.warn(('['))
470 if rev is None:
472 if rev is None:
471 ui.warn(_('wdir'), label='evolve.rev')
473 ui.warn(_('wdir'), label='evolve.rev')
472 else:
474 else:
473 ui.warn((str(rev)), label='evolve.rev')
475 ui.warn((str(rev)), label='evolve.rev')
474 ui.warn(('] %s: %s\n') % (fixername, line))
476 ui.warn(('] %s: %s\n') % (fixername, line))
475
477
476 def writeworkingdir(repo, ctx, filedata, replacements):
478 def writeworkingdir(repo, ctx, filedata, replacements):
477 """Write new content to the working copy and check out the new p1 if any
479 """Write new content to the working copy and check out the new p1 if any
478
480
479 We check out a new revision if and only if we fixed something in both the
481 We check out a new revision if and only if we fixed something in both the
480 working directory and its parent revision. This avoids the need for a full
482 working directory and its parent revision. This avoids the need for a full
481 update/merge, and means that the working directory simply isn't affected
483 update/merge, and means that the working directory simply isn't affected
482 unless the --working-dir flag is given.
484 unless the --working-dir flag is given.
483
485
484 Directly updates the dirstate for the affected files.
486 Directly updates the dirstate for the affected files.
485 """
487 """
486 for path, data in filedata.iteritems():
488 for path, data in filedata.iteritems():
487 fctx = ctx[path]
489 fctx = ctx[path]
488 fctx.write(data, fctx.flags())
490 fctx.write(data, fctx.flags())
489 if repo.dirstate[path] == 'n':
491 if repo.dirstate[path] == 'n':
490 repo.dirstate.normallookup(path)
492 repo.dirstate.normallookup(path)
491
493
492 oldparentnodes = repo.dirstate.parents()
494 oldparentnodes = repo.dirstate.parents()
493 newparentnodes = [replacements.get(n, n) for n in oldparentnodes]
495 newparentnodes = [replacements.get(n, n) for n in oldparentnodes]
494 if newparentnodes != oldparentnodes:
496 if newparentnodes != oldparentnodes:
495 repo.setparents(*newparentnodes)
497 repo.setparents(*newparentnodes)
496
498
497 def replacerev(ui, repo, ctx, filedata, replacements):
499 def replacerev(ui, repo, ctx, filedata, replacements):
498 """Commit a new revision like the given one, but with file content changes
500 """Commit a new revision like the given one, but with file content changes
499
501
500 "ctx" is the original revision to be replaced by a modified one.
502 "ctx" is the original revision to be replaced by a modified one.
501
503
502 "filedata" is a dict that maps paths to their new file content. All other
504 "filedata" is a dict that maps paths to their new file content. All other
503 paths will be recreated from the original revision without changes.
505 paths will be recreated from the original revision without changes.
504 "filedata" may contain paths that didn't exist in the original revision;
506 "filedata" may contain paths that didn't exist in the original revision;
505 they will be added.
507 they will be added.
506
508
507 "replacements" is a dict that maps a single node to a single node, and it is
509 "replacements" is a dict that maps a single node to a single node, and it is
508 updated to indicate the original revision is replaced by the newly created
510 updated to indicate the original revision is replaced by the newly created
509 one. No entry is added if the replacement's node already exists.
511 one. No entry is added if the replacement's node already exists.
510
512
511 The new revision has the same parents as the old one, unless those parents
513 The new revision has the same parents as the old one, unless those parents
512 have already been replaced, in which case those replacements are the parents
514 have already been replaced, in which case those replacements are the parents
513 of this new revision. Thus, if revisions are replaced in topological order,
515 of this new revision. Thus, if revisions are replaced in topological order,
514 there is no need to rebase them into the original topology later.
516 there is no need to rebase them into the original topology later.
515 """
517 """
516
518
517 p1rev, p2rev = repo.changelog.parentrevs(ctx.rev())
519 p1rev, p2rev = repo.changelog.parentrevs(ctx.rev())
518 p1ctx, p2ctx = repo[p1rev], repo[p2rev]
520 p1ctx, p2ctx = repo[p1rev], repo[p2rev]
519 newp1node = replacements.get(p1ctx.node(), p1ctx.node())
521 newp1node = replacements.get(p1ctx.node(), p1ctx.node())
520 newp2node = replacements.get(p2ctx.node(), p2ctx.node())
522 newp2node = replacements.get(p2ctx.node(), p2ctx.node())
521
523
522 def filectxfn(repo, memctx, path):
524 def filectxfn(repo, memctx, path):
523 if path not in ctx:
525 if path not in ctx:
524 return None
526 return None
525 fctx = ctx[path]
527 fctx = ctx[path]
526 copied = fctx.renamed()
528 copied = fctx.renamed()
527 if copied:
529 if copied:
528 copied = copied[0]
530 copied = copied[0]
529 return context.memfilectx(
531 return context.memfilectx(
530 repo,
532 repo,
531 memctx,
533 memctx,
532 path=fctx.path(),
534 path=fctx.path(),
533 data=filedata.get(path, fctx.data()),
535 data=filedata.get(path, fctx.data()),
534 islink=fctx.islink(),
536 islink=fctx.islink(),
535 isexec=fctx.isexec(),
537 isexec=fctx.isexec(),
536 copied=copied)
538 copied=copied)
537
539
538 memctx = context.memctx(
540 memctx = context.memctx(
539 repo,
541 repo,
540 parents=(newp1node, newp2node),
542 parents=(newp1node, newp2node),
541 text=ctx.description(),
543 text=ctx.description(),
542 files=set(ctx.files()) | set(filedata.keys()),
544 files=set(ctx.files()) | set(filedata.keys()),
543 filectxfn=filectxfn,
545 filectxfn=filectxfn,
544 user=ctx.user(),
546 user=ctx.user(),
545 date=ctx.date(),
547 date=ctx.date(),
546 extra=ctx.extra(),
548 extra=ctx.extra(),
547 branch=ctx.branch(),
549 branch=ctx.branch(),
548 editor=None)
550 editor=None)
549 sucnode = memctx.commit()
551 sucnode = memctx.commit()
550 prenode = ctx.node()
552 prenode = ctx.node()
551 if prenode == sucnode:
553 if prenode == sucnode:
552 ui.debug('node %s already existed\n' % (ctx.hex()))
554 ui.debug('node %s already existed\n' % (ctx.hex()))
553 else:
555 else:
554 replacements[ctx.node()] = sucnode
556 replacements[ctx.node()] = sucnode
555
557
556 def getfixers(ui):
558 def getfixers(ui):
557 """Returns a map of configured fixer tools indexed by their names
559 """Returns a map of configured fixer tools indexed by their names
558
560
559 Each value is a Fixer object with methods that implement the behavior of the
561 Each value is a Fixer object with methods that implement the behavior of the
560 fixer's config suboptions. Does not validate the config values.
562 fixer's config suboptions. Does not validate the config values.
561 """
563 """
562 result = {}
564 result = {}
563 for name in fixernames(ui):
565 for name in fixernames(ui):
564 result[name] = Fixer()
566 result[name] = Fixer()
565 attrs = ui.configsuboptions('fix', name)[1]
567 attrs = ui.configsuboptions('fix', name)[1]
566 for key in FIXER_ATTRS:
568 for key in FIXER_ATTRS:
567 setattr(result[name], pycompat.sysstr('_' + key),
569 setattr(result[name], pycompat.sysstr('_' + key),
568 attrs.get(key, ''))
570 attrs.get(key, ''))
569 return result
571 return result
570
572
571 def fixernames(ui):
573 def fixernames(ui):
572 """Returns the names of [fix] config options that have suboptions"""
574 """Returns the names of [fix] config options that have suboptions"""
573 names = set()
575 names = set()
574 for k, v in ui.configitems('fix'):
576 for k, v in ui.configitems('fix'):
575 if ':' in k:
577 if ':' in k:
576 names.add(k.split(':', 1)[0])
578 names.add(k.split(':', 1)[0])
577 return names
579 return names
578
580
579 class Fixer(object):
581 class Fixer(object):
580 """Wraps the raw config values for a fixer with methods"""
582 """Wraps the raw config values for a fixer with methods"""
581
583
582 def affects(self, opts, fixctx, path):
584 def affects(self, opts, fixctx, path):
583 """Should this fixer run on the file at the given path and context?"""
585 """Should this fixer run on the file at the given path and context?"""
584 return scmutil.match(fixctx, [self._fileset], opts)(path)
586 return scmutil.match(fixctx, [self._fileset], opts)(path)
585
587
586 def command(self, ui, path, rangesfn):
588 def command(self, ui, path, rangesfn):
587 """A shell command to use to invoke this fixer on the given file/lines
589 """A shell command to use to invoke this fixer on the given file/lines
588
590
589 May return None if there is no appropriate command to run for the given
591 May return None if there is no appropriate command to run for the given
590 parameters.
592 parameters.
591 """
593 """
592 expand = cmdutil.rendercommandtemplate
594 expand = cmdutil.rendercommandtemplate
593 parts = [expand(ui, self._command,
595 parts = [expand(ui, self._command,
594 {'rootpath': path, 'basename': os.path.basename(path)})]
596 {'rootpath': path, 'basename': os.path.basename(path)})]
595 if self._linerange:
597 if self._linerange:
596 ranges = rangesfn()
598 ranges = rangesfn()
597 if not ranges:
599 if not ranges:
598 # No line ranges to fix, so don't run the fixer.
600 # No line ranges to fix, so don't run the fixer.
599 return None
601 return None
600 for first, last in ranges:
602 for first, last in ranges:
601 parts.append(expand(ui, self._linerange,
603 parts.append(expand(ui, self._linerange,
602 {'first': first, 'last': last}))
604 {'first': first, 'last': last}))
603 return ' '.join(parts)
605 return ' '.join(parts)
General Comments 0
You need to be logged in to leave comments. Login now