##// END OF EJS Templates
fix: don't include obsolete descendants with -s...
Martin von Zweigbergk -
r46400:7f07e634 default draft
parent child Browse files
Show More
@@ -1,937 +1,937 b''
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:pattern=set:**.cpp or **.hpp
18 clang-format:pattern=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. Any output on standard error
22 fixed file content is expected on standard output. Any output on standard error
23 will be displayed as a warning. If the exit status is not zero, the file will
23 will be displayed as a warning. If the exit status is not zero, the file will
24 not be affected. A placeholder warning is displayed if there is a non-zero exit
24 not be affected. A placeholder warning is displayed if there is a non-zero exit
25 status but no standard error output. Some values may be substituted into the
25 status but no standard error output. Some values may be substituted into the
26 command::
26 command::
27
27
28 {rootpath} The path of the file being fixed, relative to the repo root
28 {rootpath} The path of the file being fixed, relative to the repo root
29 {basename} The name of the file being fixed, without the directory path
29 {basename} The name of the file being fixed, without the directory path
30
30
31 If the :linerange suboption is set, the tool will only be run if there are
31 If the :linerange suboption is set, the tool will only be run if there are
32 changed lines in a file. The value of this suboption is appended to the shell
32 changed lines in a file. The value of this suboption is appended to the shell
33 command once for every range of changed lines in the file. Some values may be
33 command once for every range of changed lines in the file. Some values may be
34 substituted into the command::
34 substituted into the command::
35
35
36 {first} The 1-based line number of the first line in the modified range
36 {first} The 1-based line number of the first line in the modified range
37 {last} The 1-based line number of the last line in the modified range
37 {last} The 1-based line number of the last line in the modified range
38
38
39 Deleted sections of a file will be ignored by :linerange, because there is no
39 Deleted sections of a file will be ignored by :linerange, because there is no
40 corresponding line range in the version being fixed.
40 corresponding line range in the version being fixed.
41
41
42 By default, tools that set :linerange will only be executed if there is at least
42 By default, tools that set :linerange will only be executed if there is at least
43 one changed line range. This is meant to prevent accidents like running a code
43 one changed line range. This is meant to prevent accidents like running a code
44 formatter in such a way that it unexpectedly reformats the whole file. If such a
44 formatter in such a way that it unexpectedly reformats the whole file. If such a
45 tool needs to operate on unchanged files, it should set the :skipclean suboption
45 tool needs to operate on unchanged files, it should set the :skipclean suboption
46 to false.
46 to false.
47
47
48 The :pattern suboption determines which files will be passed through each
48 The :pattern suboption determines which files will be passed through each
49 configured tool. See :hg:`help patterns` for possible values. However, all
49 configured tool. See :hg:`help patterns` for possible values. However, all
50 patterns are relative to the repo root, even if that text says they are relative
50 patterns are relative to the repo root, even if that text says they are relative
51 to the current working directory. If there are file arguments to :hg:`fix`, the
51 to the current working directory. If there are file arguments to :hg:`fix`, the
52 intersection of these patterns is used.
52 intersection of these patterns is used.
53
53
54 There is also a configurable limit for the maximum size of file that will be
54 There is also a configurable limit for the maximum size of file that will be
55 processed by :hg:`fix`::
55 processed by :hg:`fix`::
56
56
57 [fix]
57 [fix]
58 maxfilesize = 2MB
58 maxfilesize = 2MB
59
59
60 Normally, execution of configured tools will continue after a failure (indicated
60 Normally, execution of configured tools will continue after a failure (indicated
61 by a non-zero exit status). It can also be configured to abort after the first
61 by a non-zero exit status). It can also be configured to abort after the first
62 such failure, so that no files will be affected if any tool fails. This abort
62 such failure, so that no files will be affected if any tool fails. This abort
63 will also cause :hg:`fix` to exit with a non-zero status::
63 will also cause :hg:`fix` to exit with a non-zero status::
64
64
65 [fix]
65 [fix]
66 failure = abort
66 failure = abort
67
67
68 When multiple tools are configured to affect a file, they execute in an order
68 When multiple tools are configured to affect a file, they execute in an order
69 defined by the :priority suboption. The priority suboption has a default value
69 defined by the :priority suboption. The priority suboption has a default value
70 of zero for each tool. Tools are executed in order of descending priority. The
70 of zero for each tool. Tools are executed in order of descending priority. The
71 execution order of tools with equal priority is unspecified. For example, you
71 execution order of tools with equal priority is unspecified. For example, you
72 could use the 'sort' and 'head' utilities to keep only the 10 smallest numbers
72 could use the 'sort' and 'head' utilities to keep only the 10 smallest numbers
73 in a text file by ensuring that 'sort' runs before 'head'::
73 in a text file by ensuring that 'sort' runs before 'head'::
74
74
75 [fix]
75 [fix]
76 sort:command = sort -n
76 sort:command = sort -n
77 head:command = head -n 10
77 head:command = head -n 10
78 sort:pattern = numbers.txt
78 sort:pattern = numbers.txt
79 head:pattern = numbers.txt
79 head:pattern = numbers.txt
80 sort:priority = 2
80 sort:priority = 2
81 head:priority = 1
81 head:priority = 1
82
82
83 To account for changes made by each tool, the line numbers used for incremental
83 To account for changes made by each tool, the line numbers used for incremental
84 formatting are recomputed before executing the next tool. So, each tool may see
84 formatting are recomputed before executing the next tool. So, each tool may see
85 different values for the arguments added by the :linerange suboption.
85 different values for the arguments added by the :linerange suboption.
86
86
87 Each fixer tool is allowed to return some metadata in addition to the fixed file
87 Each fixer tool is allowed to return some metadata in addition to the fixed file
88 content. The metadata must be placed before the file content on stdout,
88 content. The metadata must be placed before the file content on stdout,
89 separated from the file content by a zero byte. The metadata is parsed as a JSON
89 separated from the file content by a zero byte. The metadata is parsed as a JSON
90 value (so, it should be UTF-8 encoded and contain no zero bytes). A fixer tool
90 value (so, it should be UTF-8 encoded and contain no zero bytes). A fixer tool
91 is expected to produce this metadata encoding if and only if the :metadata
91 is expected to produce this metadata encoding if and only if the :metadata
92 suboption is true::
92 suboption is true::
93
93
94 [fix]
94 [fix]
95 tool:command = tool --prepend-json-metadata
95 tool:command = tool --prepend-json-metadata
96 tool:metadata = true
96 tool:metadata = true
97
97
98 The metadata values are passed to hooks, which can be used to print summaries or
98 The metadata values are passed to hooks, which can be used to print summaries or
99 perform other post-fixing work. The supported hooks are::
99 perform other post-fixing work. The supported hooks are::
100
100
101 "postfixfile"
101 "postfixfile"
102 Run once for each file in each revision where any fixer tools made changes
102 Run once for each file in each revision where any fixer tools made changes
103 to the file content. Provides "$HG_REV" and "$HG_PATH" to identify the file,
103 to the file content. Provides "$HG_REV" and "$HG_PATH" to identify the file,
104 and "$HG_METADATA" with a map of fixer names to metadata values from fixer
104 and "$HG_METADATA" with a map of fixer names to metadata values from fixer
105 tools that affected the file. Fixer tools that didn't affect the file have a
105 tools that affected the file. Fixer tools that didn't affect the file have a
106 value of None. Only fixer tools that executed are present in the metadata.
106 value of None. Only fixer tools that executed are present in the metadata.
107
107
108 "postfix"
108 "postfix"
109 Run once after all files and revisions have been handled. Provides
109 Run once after all files and revisions have been handled. Provides
110 "$HG_REPLACEMENTS" with information about what revisions were created and
110 "$HG_REPLACEMENTS" with information about what revisions were created and
111 made obsolete. Provides a boolean "$HG_WDIRWRITTEN" to indicate whether any
111 made obsolete. Provides a boolean "$HG_WDIRWRITTEN" to indicate whether any
112 files in the working copy were updated. Provides a list "$HG_METADATA"
112 files in the working copy were updated. Provides a list "$HG_METADATA"
113 mapping fixer tool names to lists of metadata values returned from
113 mapping fixer tool names to lists of metadata values returned from
114 executions that modified a file. This aggregates the same metadata
114 executions that modified a file. This aggregates the same metadata
115 previously passed to the "postfixfile" hook.
115 previously passed to the "postfixfile" hook.
116
116
117 Fixer tools are run in the repository's root directory. This allows them to read
117 Fixer tools are run in the repository's root directory. This allows them to read
118 configuration files from the working copy, or even write to the working copy.
118 configuration files from the working copy, or even write to the working copy.
119 The working copy is not updated to match the revision being fixed. In fact,
119 The working copy is not updated to match the revision being fixed. In fact,
120 several revisions may be fixed in parallel. Writes to the working copy are not
120 several revisions may be fixed in parallel. Writes to the working copy are not
121 amended into the revision being fixed; fixer tools should always write fixed
121 amended into the revision being fixed; fixer tools should always write fixed
122 file content back to stdout as documented above.
122 file content back to stdout as documented above.
123 """
123 """
124
124
125 from __future__ import absolute_import
125 from __future__ import absolute_import
126
126
127 import collections
127 import collections
128 import itertools
128 import itertools
129 import os
129 import os
130 import re
130 import re
131 import subprocess
131 import subprocess
132
132
133 from mercurial.i18n import _
133 from mercurial.i18n import _
134 from mercurial.node import nullrev
134 from mercurial.node import nullrev
135 from mercurial.node import wdirrev
135 from mercurial.node import wdirrev
136
136
137 from mercurial.utils import procutil
137 from mercurial.utils import procutil
138
138
139 from mercurial import (
139 from mercurial import (
140 cmdutil,
140 cmdutil,
141 context,
141 context,
142 copies,
142 copies,
143 error,
143 error,
144 match as matchmod,
144 match as matchmod,
145 mdiff,
145 mdiff,
146 merge,
146 merge,
147 mergestate as mergestatemod,
147 mergestate as mergestatemod,
148 pycompat,
148 pycompat,
149 registrar,
149 registrar,
150 rewriteutil,
150 rewriteutil,
151 scmutil,
151 scmutil,
152 util,
152 util,
153 worker,
153 worker,
154 )
154 )
155
155
156 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
156 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
157 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
157 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
158 # be specifying the version(s) of Mercurial they are tested with, or
158 # be specifying the version(s) of Mercurial they are tested with, or
159 # leave the attribute unspecified.
159 # leave the attribute unspecified.
160 testedwith = b'ships-with-hg-core'
160 testedwith = b'ships-with-hg-core'
161
161
162 cmdtable = {}
162 cmdtable = {}
163 command = registrar.command(cmdtable)
163 command = registrar.command(cmdtable)
164
164
165 configtable = {}
165 configtable = {}
166 configitem = registrar.configitem(configtable)
166 configitem = registrar.configitem(configtable)
167
167
168 # Register the suboptions allowed for each configured fixer, and default values.
168 # Register the suboptions allowed for each configured fixer, and default values.
169 FIXER_ATTRS = {
169 FIXER_ATTRS = {
170 b'command': None,
170 b'command': None,
171 b'linerange': None,
171 b'linerange': None,
172 b'pattern': None,
172 b'pattern': None,
173 b'priority': 0,
173 b'priority': 0,
174 b'metadata': False,
174 b'metadata': False,
175 b'skipclean': True,
175 b'skipclean': True,
176 b'enabled': True,
176 b'enabled': True,
177 }
177 }
178
178
179 for key, default in FIXER_ATTRS.items():
179 for key, default in FIXER_ATTRS.items():
180 configitem(b'fix', b'.*:%s$' % key, default=default, generic=True)
180 configitem(b'fix', b'.*:%s$' % key, default=default, generic=True)
181
181
182 # A good default size allows most source code files to be fixed, but avoids
182 # A good default size allows most source code files to be fixed, but avoids
183 # letting fixer tools choke on huge inputs, which could be surprising to the
183 # letting fixer tools choke on huge inputs, which could be surprising to the
184 # user.
184 # user.
185 configitem(b'fix', b'maxfilesize', default=b'2MB')
185 configitem(b'fix', b'maxfilesize', default=b'2MB')
186
186
187 # Allow fix commands to exit non-zero if an executed fixer tool exits non-zero.
187 # Allow fix commands to exit non-zero if an executed fixer tool exits non-zero.
188 # This helps users do shell scripts that stop when a fixer tool signals a
188 # This helps users do shell scripts that stop when a fixer tool signals a
189 # problem.
189 # problem.
190 configitem(b'fix', b'failure', default=b'continue')
190 configitem(b'fix', b'failure', default=b'continue')
191
191
192
192
193 def checktoolfailureaction(ui, message, hint=None):
193 def checktoolfailureaction(ui, message, hint=None):
194 """Abort with 'message' if fix.failure=abort"""
194 """Abort with 'message' if fix.failure=abort"""
195 action = ui.config(b'fix', b'failure')
195 action = ui.config(b'fix', b'failure')
196 if action not in (b'continue', b'abort'):
196 if action not in (b'continue', b'abort'):
197 raise error.Abort(
197 raise error.Abort(
198 _(b'unknown fix.failure action: %s') % (action,),
198 _(b'unknown fix.failure action: %s') % (action,),
199 hint=_(b'use "continue" or "abort"'),
199 hint=_(b'use "continue" or "abort"'),
200 )
200 )
201 if action == b'abort':
201 if action == b'abort':
202 raise error.Abort(message, hint=hint)
202 raise error.Abort(message, hint=hint)
203
203
204
204
205 allopt = (b'', b'all', False, _(b'fix all non-public non-obsolete revisions'))
205 allopt = (b'', b'all', False, _(b'fix all non-public non-obsolete revisions'))
206 baseopt = (
206 baseopt = (
207 b'',
207 b'',
208 b'base',
208 b'base',
209 [],
209 [],
210 _(
210 _(
211 b'revisions to diff against (overrides automatic '
211 b'revisions to diff against (overrides automatic '
212 b'selection, and applies to every revision being '
212 b'selection, and applies to every revision being '
213 b'fixed)'
213 b'fixed)'
214 ),
214 ),
215 _(b'REV'),
215 _(b'REV'),
216 )
216 )
217 revopt = (b'r', b'rev', [], _(b'revisions to fix (ADVANCED)'), _(b'REV'))
217 revopt = (b'r', b'rev', [], _(b'revisions to fix (ADVANCED)'), _(b'REV'))
218 sourceopt = (
218 sourceopt = (
219 b's',
219 b's',
220 b'source',
220 b'source',
221 [],
221 [],
222 _(b'fix the specified revisions and their descendants'),
222 _(b'fix the specified revisions and their descendants'),
223 _(b'REV'),
223 _(b'REV'),
224 )
224 )
225 wdiropt = (b'w', b'working-dir', False, _(b'fix the working directory'))
225 wdiropt = (b'w', b'working-dir', False, _(b'fix the working directory'))
226 wholeopt = (b'', b'whole', False, _(b'always fix every line of a file'))
226 wholeopt = (b'', b'whole', False, _(b'always fix every line of a file'))
227 usage = _(b'[OPTION]... [FILE]...')
227 usage = _(b'[OPTION]... [FILE]...')
228
228
229
229
230 @command(
230 @command(
231 b'fix',
231 b'fix',
232 [allopt, baseopt, revopt, sourceopt, wdiropt, wholeopt],
232 [allopt, baseopt, revopt, sourceopt, wdiropt, wholeopt],
233 usage,
233 usage,
234 helpcategory=command.CATEGORY_FILE_CONTENTS,
234 helpcategory=command.CATEGORY_FILE_CONTENTS,
235 )
235 )
236 def fix(ui, repo, *pats, **opts):
236 def fix(ui, repo, *pats, **opts):
237 """rewrite file content in changesets or working directory
237 """rewrite file content in changesets or working directory
238
238
239 Runs any configured tools to fix the content of files. Only affects files
239 Runs any configured tools to fix the content of files. Only affects files
240 with changes, unless file arguments are provided. Only affects changed lines
240 with changes, unless file arguments are provided. Only affects changed lines
241 of files, unless the --whole flag is used. Some tools may always affect the
241 of files, unless the --whole flag is used. Some tools may always affect the
242 whole file regardless of --whole.
242 whole file regardless of --whole.
243
243
244 If --working-dir is used, files with uncommitted changes in the working copy
244 If --working-dir is used, files with uncommitted changes in the working copy
245 will be fixed. Note that no backup are made.
245 will be fixed. Note that no backup are made.
246
246
247 If revisions are specified with --source, those revisions and their
247 If revisions are specified with --source, those revisions and their
248 descendants will be checked, and they may be replaced with new revisions
248 descendants will be checked, and they may be replaced with new revisions
249 that have fixed file content. By automatically including the descendants,
249 that have fixed file content. By automatically including the descendants,
250 no merging, rebasing, or evolution will be required. If an ancestor of the
250 no merging, rebasing, or evolution will be required. If an ancestor of the
251 working copy is included, then the working copy itself will also be fixed,
251 working copy is included, then the working copy itself will also be fixed,
252 and the working copy will be updated to the fixed parent.
252 and the working copy will be updated to the fixed parent.
253
253
254 When determining what lines of each file to fix at each revision, the whole
254 When determining what lines of each file to fix at each revision, the whole
255 set of revisions being fixed is considered, so that fixes to earlier
255 set of revisions being fixed is considered, so that fixes to earlier
256 revisions are not forgotten in later ones. The --base flag can be used to
256 revisions are not forgotten in later ones. The --base flag can be used to
257 override this default behavior, though it is not usually desirable to do so.
257 override this default behavior, though it is not usually desirable to do so.
258 """
258 """
259 opts = pycompat.byteskwargs(opts)
259 opts = pycompat.byteskwargs(opts)
260 cmdutil.check_at_most_one_arg(opts, b'all', b'source', b'rev')
260 cmdutil.check_at_most_one_arg(opts, b'all', b'source', b'rev')
261 cmdutil.check_incompatible_arguments(
261 cmdutil.check_incompatible_arguments(
262 opts, b'working_dir', [b'all', b'source']
262 opts, b'working_dir', [b'all', b'source']
263 )
263 )
264
264
265 with repo.wlock(), repo.lock(), repo.transaction(b'fix'):
265 with repo.wlock(), repo.lock(), repo.transaction(b'fix'):
266 revstofix = getrevstofix(ui, repo, opts)
266 revstofix = getrevstofix(ui, repo, opts)
267 basectxs = getbasectxs(repo, opts, revstofix)
267 basectxs = getbasectxs(repo, opts, revstofix)
268 workqueue, numitems = getworkqueue(
268 workqueue, numitems = getworkqueue(
269 ui, repo, pats, opts, revstofix, basectxs
269 ui, repo, pats, opts, revstofix, basectxs
270 )
270 )
271 basepaths = getbasepaths(repo, opts, workqueue, basectxs)
271 basepaths = getbasepaths(repo, opts, workqueue, basectxs)
272 fixers = getfixers(ui)
272 fixers = getfixers(ui)
273
273
274 # Rather than letting each worker independently fetch the files
274 # Rather than letting each worker independently fetch the files
275 # (which also would add complications for shared/keepalive
275 # (which also would add complications for shared/keepalive
276 # connections), prefetch them all first.
276 # connections), prefetch them all first.
277 _prefetchfiles(repo, workqueue, basepaths)
277 _prefetchfiles(repo, workqueue, basepaths)
278
278
279 # There are no data dependencies between the workers fixing each file
279 # There are no data dependencies between the workers fixing each file
280 # revision, so we can use all available parallelism.
280 # revision, so we can use all available parallelism.
281 def getfixes(items):
281 def getfixes(items):
282 for rev, path in items:
282 for rev, path in items:
283 ctx = repo[rev]
283 ctx = repo[rev]
284 olddata = ctx[path].data()
284 olddata = ctx[path].data()
285 metadata, newdata = fixfile(
285 metadata, newdata = fixfile(
286 ui, repo, opts, fixers, ctx, path, basepaths, basectxs[rev]
286 ui, repo, opts, fixers, ctx, path, basepaths, basectxs[rev]
287 )
287 )
288 # Don't waste memory/time passing unchanged content back, but
288 # Don't waste memory/time passing unchanged content back, but
289 # produce one result per item either way.
289 # produce one result per item either way.
290 yield (
290 yield (
291 rev,
291 rev,
292 path,
292 path,
293 metadata,
293 metadata,
294 newdata if newdata != olddata else None,
294 newdata if newdata != olddata else None,
295 )
295 )
296
296
297 results = worker.worker(
297 results = worker.worker(
298 ui, 1.0, getfixes, tuple(), workqueue, threadsafe=False
298 ui, 1.0, getfixes, tuple(), workqueue, threadsafe=False
299 )
299 )
300
300
301 # We have to hold on to the data for each successor revision in memory
301 # We have to hold on to the data for each successor revision in memory
302 # until all its parents are committed. We ensure this by committing and
302 # until all its parents are committed. We ensure this by committing and
303 # freeing memory for the revisions in some topological order. This
303 # freeing memory for the revisions in some topological order. This
304 # leaves a little bit of memory efficiency on the table, but also makes
304 # leaves a little bit of memory efficiency on the table, but also makes
305 # the tests deterministic. It might also be considered a feature since
305 # the tests deterministic. It might also be considered a feature since
306 # it makes the results more easily reproducible.
306 # it makes the results more easily reproducible.
307 filedata = collections.defaultdict(dict)
307 filedata = collections.defaultdict(dict)
308 aggregatemetadata = collections.defaultdict(list)
308 aggregatemetadata = collections.defaultdict(list)
309 replacements = {}
309 replacements = {}
310 wdirwritten = False
310 wdirwritten = False
311 commitorder = sorted(revstofix, reverse=True)
311 commitorder = sorted(revstofix, reverse=True)
312 with ui.makeprogress(
312 with ui.makeprogress(
313 topic=_(b'fixing'), unit=_(b'files'), total=sum(numitems.values())
313 topic=_(b'fixing'), unit=_(b'files'), total=sum(numitems.values())
314 ) as progress:
314 ) as progress:
315 for rev, path, filerevmetadata, newdata in results:
315 for rev, path, filerevmetadata, newdata in results:
316 progress.increment(item=path)
316 progress.increment(item=path)
317 for fixername, fixermetadata in filerevmetadata.items():
317 for fixername, fixermetadata in filerevmetadata.items():
318 aggregatemetadata[fixername].append(fixermetadata)
318 aggregatemetadata[fixername].append(fixermetadata)
319 if newdata is not None:
319 if newdata is not None:
320 filedata[rev][path] = newdata
320 filedata[rev][path] = newdata
321 hookargs = {
321 hookargs = {
322 b'rev': rev,
322 b'rev': rev,
323 b'path': path,
323 b'path': path,
324 b'metadata': filerevmetadata,
324 b'metadata': filerevmetadata,
325 }
325 }
326 repo.hook(
326 repo.hook(
327 b'postfixfile',
327 b'postfixfile',
328 throw=False,
328 throw=False,
329 **pycompat.strkwargs(hookargs)
329 **pycompat.strkwargs(hookargs)
330 )
330 )
331 numitems[rev] -= 1
331 numitems[rev] -= 1
332 # Apply the fixes for this and any other revisions that are
332 # Apply the fixes for this and any other revisions that are
333 # ready and sitting at the front of the queue. Using a loop here
333 # ready and sitting at the front of the queue. Using a loop here
334 # prevents the queue from being blocked by the first revision to
334 # prevents the queue from being blocked by the first revision to
335 # be ready out of order.
335 # be ready out of order.
336 while commitorder and not numitems[commitorder[-1]]:
336 while commitorder and not numitems[commitorder[-1]]:
337 rev = commitorder.pop()
337 rev = commitorder.pop()
338 ctx = repo[rev]
338 ctx = repo[rev]
339 if rev == wdirrev:
339 if rev == wdirrev:
340 writeworkingdir(repo, ctx, filedata[rev], replacements)
340 writeworkingdir(repo, ctx, filedata[rev], replacements)
341 wdirwritten = bool(filedata[rev])
341 wdirwritten = bool(filedata[rev])
342 else:
342 else:
343 replacerev(ui, repo, ctx, filedata[rev], replacements)
343 replacerev(ui, repo, ctx, filedata[rev], replacements)
344 del filedata[rev]
344 del filedata[rev]
345
345
346 cleanup(repo, replacements, wdirwritten)
346 cleanup(repo, replacements, wdirwritten)
347 hookargs = {
347 hookargs = {
348 b'replacements': replacements,
348 b'replacements': replacements,
349 b'wdirwritten': wdirwritten,
349 b'wdirwritten': wdirwritten,
350 b'metadata': aggregatemetadata,
350 b'metadata': aggregatemetadata,
351 }
351 }
352 repo.hook(b'postfix', throw=True, **pycompat.strkwargs(hookargs))
352 repo.hook(b'postfix', throw=True, **pycompat.strkwargs(hookargs))
353
353
354
354
355 def cleanup(repo, replacements, wdirwritten):
355 def cleanup(repo, replacements, wdirwritten):
356 """Calls scmutil.cleanupnodes() with the given replacements.
356 """Calls scmutil.cleanupnodes() with the given replacements.
357
357
358 "replacements" is a dict from nodeid to nodeid, with one key and one value
358 "replacements" is a dict from nodeid to nodeid, with one key and one value
359 for every revision that was affected by fixing. This is slightly different
359 for every revision that was affected by fixing. This is slightly different
360 from cleanupnodes().
360 from cleanupnodes().
361
361
362 "wdirwritten" is a bool which tells whether the working copy was affected by
362 "wdirwritten" is a bool which tells whether the working copy was affected by
363 fixing, since it has no entry in "replacements".
363 fixing, since it has no entry in "replacements".
364
364
365 Useful as a hook point for extending "hg fix" with output summarizing the
365 Useful as a hook point for extending "hg fix" with output summarizing the
366 effects of the command, though we choose not to output anything here.
366 effects of the command, though we choose not to output anything here.
367 """
367 """
368 replacements = {
368 replacements = {
369 prec: [succ] for prec, succ in pycompat.iteritems(replacements)
369 prec: [succ] for prec, succ in pycompat.iteritems(replacements)
370 }
370 }
371 scmutil.cleanupnodes(repo, replacements, b'fix', fixphase=True)
371 scmutil.cleanupnodes(repo, replacements, b'fix', fixphase=True)
372
372
373
373
374 def getworkqueue(ui, repo, pats, opts, revstofix, basectxs):
374 def getworkqueue(ui, repo, pats, opts, revstofix, basectxs):
375 """"Constructs the list of files to be fixed at specific revisions
375 """"Constructs the list of files to be fixed at specific revisions
376
376
377 It is up to the caller how to consume the work items, and the only
377 It is up to the caller how to consume the work items, and the only
378 dependence between them is that replacement revisions must be committed in
378 dependence between them is that replacement revisions must be committed in
379 topological order. Each work item represents a file in the working copy or
379 topological order. Each work item represents a file in the working copy or
380 in some revision that should be fixed and written back to the working copy
380 in some revision that should be fixed and written back to the working copy
381 or into a replacement revision.
381 or into a replacement revision.
382
382
383 Work items for the same revision are grouped together, so that a worker
383 Work items for the same revision are grouped together, so that a worker
384 pool starting with the first N items in parallel is likely to finish the
384 pool starting with the first N items in parallel is likely to finish the
385 first revision's work before other revisions. This can allow us to write
385 first revision's work before other revisions. This can allow us to write
386 the result to disk and reduce memory footprint. At time of writing, the
386 the result to disk and reduce memory footprint. At time of writing, the
387 partition strategy in worker.py seems favorable to this. We also sort the
387 partition strategy in worker.py seems favorable to this. We also sort the
388 items by ascending revision number to match the order in which we commit
388 items by ascending revision number to match the order in which we commit
389 the fixes later.
389 the fixes later.
390 """
390 """
391 workqueue = []
391 workqueue = []
392 numitems = collections.defaultdict(int)
392 numitems = collections.defaultdict(int)
393 maxfilesize = ui.configbytes(b'fix', b'maxfilesize')
393 maxfilesize = ui.configbytes(b'fix', b'maxfilesize')
394 for rev in sorted(revstofix):
394 for rev in sorted(revstofix):
395 fixctx = repo[rev]
395 fixctx = repo[rev]
396 match = scmutil.match(fixctx, pats, opts)
396 match = scmutil.match(fixctx, pats, opts)
397 for path in sorted(
397 for path in sorted(
398 pathstofix(ui, repo, pats, opts, match, basectxs[rev], fixctx)
398 pathstofix(ui, repo, pats, opts, match, basectxs[rev], fixctx)
399 ):
399 ):
400 fctx = fixctx[path]
400 fctx = fixctx[path]
401 if fctx.islink():
401 if fctx.islink():
402 continue
402 continue
403 if fctx.size() > maxfilesize:
403 if fctx.size() > maxfilesize:
404 ui.warn(
404 ui.warn(
405 _(b'ignoring file larger than %s: %s\n')
405 _(b'ignoring file larger than %s: %s\n')
406 % (util.bytecount(maxfilesize), path)
406 % (util.bytecount(maxfilesize), path)
407 )
407 )
408 continue
408 continue
409 workqueue.append((rev, path))
409 workqueue.append((rev, path))
410 numitems[rev] += 1
410 numitems[rev] += 1
411 return workqueue, numitems
411 return workqueue, numitems
412
412
413
413
414 def getrevstofix(ui, repo, opts):
414 def getrevstofix(ui, repo, opts):
415 """Returns the set of revision numbers that should be fixed"""
415 """Returns the set of revision numbers that should be fixed"""
416 if opts[b'all']:
416 if opts[b'all']:
417 revs = repo.revs(b'(not public() and not obsolete()) or wdir()')
417 revs = repo.revs(b'(not public() and not obsolete()) or wdir()')
418 elif opts[b'source']:
418 elif opts[b'source']:
419 source_revs = scmutil.revrange(repo, opts[b'source'])
419 source_revs = scmutil.revrange(repo, opts[b'source'])
420 revs = set(repo.revs(b'%ld::', source_revs))
420 revs = set(repo.revs(b'(%ld::) - obsolete()', source_revs))
421 if wdirrev in source_revs:
421 if wdirrev in source_revs:
422 # `wdir()::` is currently empty, so manually add wdir
422 # `wdir()::` is currently empty, so manually add wdir
423 revs.add(wdirrev)
423 revs.add(wdirrev)
424 if repo[b'.'].rev() in revs:
424 if repo[b'.'].rev() in revs:
425 revs.add(wdirrev)
425 revs.add(wdirrev)
426 else:
426 else:
427 revs = set(scmutil.revrange(repo, opts[b'rev']))
427 revs = set(scmutil.revrange(repo, opts[b'rev']))
428 if opts.get(b'working_dir'):
428 if opts.get(b'working_dir'):
429 revs.add(wdirrev)
429 revs.add(wdirrev)
430 for rev in revs:
430 for rev in revs:
431 checkfixablectx(ui, repo, repo[rev])
431 checkfixablectx(ui, repo, repo[rev])
432 # Allow fixing only wdir() even if there's an unfinished operation
432 # Allow fixing only wdir() even if there's an unfinished operation
433 if not (len(revs) == 1 and wdirrev in revs):
433 if not (len(revs) == 1 and wdirrev in revs):
434 cmdutil.checkunfinished(repo)
434 cmdutil.checkunfinished(repo)
435 rewriteutil.precheck(repo, revs, b'fix')
435 rewriteutil.precheck(repo, revs, b'fix')
436 if wdirrev in revs and list(
436 if wdirrev in revs and list(
437 mergestatemod.mergestate.read(repo).unresolved()
437 mergestatemod.mergestate.read(repo).unresolved()
438 ):
438 ):
439 raise error.Abort(b'unresolved conflicts', hint=b"use 'hg resolve'")
439 raise error.Abort(b'unresolved conflicts', hint=b"use 'hg resolve'")
440 if not revs:
440 if not revs:
441 raise error.Abort(
441 raise error.Abort(
442 b'no changesets specified', hint=b'use --source or --working-dir'
442 b'no changesets specified', hint=b'use --source or --working-dir'
443 )
443 )
444 return revs
444 return revs
445
445
446
446
447 def checkfixablectx(ui, repo, ctx):
447 def checkfixablectx(ui, repo, ctx):
448 """Aborts if the revision shouldn't be replaced with a fixed one."""
448 """Aborts if the revision shouldn't be replaced with a fixed one."""
449 if ctx.obsolete():
449 if ctx.obsolete():
450 # It would be better to actually check if the revision has a successor.
450 # It would be better to actually check if the revision has a successor.
451 allowdivergence = ui.configbool(
451 allowdivergence = ui.configbool(
452 b'experimental', b'evolution.allowdivergence'
452 b'experimental', b'evolution.allowdivergence'
453 )
453 )
454 if not allowdivergence:
454 if not allowdivergence:
455 raise error.Abort(
455 raise error.Abort(
456 b'fixing obsolete revision could cause divergence'
456 b'fixing obsolete revision could cause divergence'
457 )
457 )
458
458
459
459
460 def pathstofix(ui, repo, pats, opts, match, basectxs, fixctx):
460 def pathstofix(ui, repo, pats, opts, match, basectxs, fixctx):
461 """Returns the set of files that should be fixed in a context
461 """Returns the set of files that should be fixed in a context
462
462
463 The result depends on the base contexts; we include any file that has
463 The result depends on the base contexts; we include any file that has
464 changed relative to any of the base contexts. Base contexts should be
464 changed relative to any of the base contexts. Base contexts should be
465 ancestors of the context being fixed.
465 ancestors of the context being fixed.
466 """
466 """
467 files = set()
467 files = set()
468 for basectx in basectxs:
468 for basectx in basectxs:
469 stat = basectx.status(
469 stat = basectx.status(
470 fixctx, match=match, listclean=bool(pats), listunknown=bool(pats)
470 fixctx, match=match, listclean=bool(pats), listunknown=bool(pats)
471 )
471 )
472 files.update(
472 files.update(
473 set(
473 set(
474 itertools.chain(
474 itertools.chain(
475 stat.added, stat.modified, stat.clean, stat.unknown
475 stat.added, stat.modified, stat.clean, stat.unknown
476 )
476 )
477 )
477 )
478 )
478 )
479 return files
479 return files
480
480
481
481
482 def lineranges(opts, path, basepaths, basectxs, fixctx, content2):
482 def lineranges(opts, path, basepaths, basectxs, fixctx, content2):
483 """Returns the set of line ranges that should be fixed in a file
483 """Returns the set of line ranges that should be fixed in a file
484
484
485 Of the form [(10, 20), (30, 40)].
485 Of the form [(10, 20), (30, 40)].
486
486
487 This depends on the given base contexts; we must consider lines that have
487 This depends on the given base contexts; we must consider lines that have
488 changed versus any of the base contexts, and whether the file has been
488 changed versus any of the base contexts, and whether the file has been
489 renamed versus any of them.
489 renamed versus any of them.
490
490
491 Another way to understand this is that we exclude line ranges that are
491 Another way to understand this is that we exclude line ranges that are
492 common to the file in all base contexts.
492 common to the file in all base contexts.
493 """
493 """
494 if opts.get(b'whole'):
494 if opts.get(b'whole'):
495 # Return a range containing all lines. Rely on the diff implementation's
495 # Return a range containing all lines. Rely on the diff implementation's
496 # idea of how many lines are in the file, instead of reimplementing it.
496 # idea of how many lines are in the file, instead of reimplementing it.
497 return difflineranges(b'', content2)
497 return difflineranges(b'', content2)
498
498
499 rangeslist = []
499 rangeslist = []
500 for basectx in basectxs:
500 for basectx in basectxs:
501 basepath = basepaths.get((basectx.rev(), fixctx.rev(), path), path)
501 basepath = basepaths.get((basectx.rev(), fixctx.rev(), path), path)
502
502
503 if basepath in basectx:
503 if basepath in basectx:
504 content1 = basectx[basepath].data()
504 content1 = basectx[basepath].data()
505 else:
505 else:
506 content1 = b''
506 content1 = b''
507 rangeslist.extend(difflineranges(content1, content2))
507 rangeslist.extend(difflineranges(content1, content2))
508 return unionranges(rangeslist)
508 return unionranges(rangeslist)
509
509
510
510
511 def getbasepaths(repo, opts, workqueue, basectxs):
511 def getbasepaths(repo, opts, workqueue, basectxs):
512 if opts.get(b'whole'):
512 if opts.get(b'whole'):
513 # Base paths will never be fetched for line range determination.
513 # Base paths will never be fetched for line range determination.
514 return {}
514 return {}
515
515
516 basepaths = {}
516 basepaths = {}
517 for rev, path in workqueue:
517 for rev, path in workqueue:
518 fixctx = repo[rev]
518 fixctx = repo[rev]
519 for basectx in basectxs[rev]:
519 for basectx in basectxs[rev]:
520 basepath = copies.pathcopies(basectx, fixctx).get(path, path)
520 basepath = copies.pathcopies(basectx, fixctx).get(path, path)
521 if basepath in basectx:
521 if basepath in basectx:
522 basepaths[(basectx.rev(), fixctx.rev(), path)] = basepath
522 basepaths[(basectx.rev(), fixctx.rev(), path)] = basepath
523 return basepaths
523 return basepaths
524
524
525
525
526 def unionranges(rangeslist):
526 def unionranges(rangeslist):
527 """Return the union of some closed intervals
527 """Return the union of some closed intervals
528
528
529 >>> unionranges([])
529 >>> unionranges([])
530 []
530 []
531 >>> unionranges([(1, 100)])
531 >>> unionranges([(1, 100)])
532 [(1, 100)]
532 [(1, 100)]
533 >>> unionranges([(1, 100), (1, 100)])
533 >>> unionranges([(1, 100), (1, 100)])
534 [(1, 100)]
534 [(1, 100)]
535 >>> unionranges([(1, 100), (2, 100)])
535 >>> unionranges([(1, 100), (2, 100)])
536 [(1, 100)]
536 [(1, 100)]
537 >>> unionranges([(1, 99), (1, 100)])
537 >>> unionranges([(1, 99), (1, 100)])
538 [(1, 100)]
538 [(1, 100)]
539 >>> unionranges([(1, 100), (40, 60)])
539 >>> unionranges([(1, 100), (40, 60)])
540 [(1, 100)]
540 [(1, 100)]
541 >>> unionranges([(1, 49), (50, 100)])
541 >>> unionranges([(1, 49), (50, 100)])
542 [(1, 100)]
542 [(1, 100)]
543 >>> unionranges([(1, 48), (50, 100)])
543 >>> unionranges([(1, 48), (50, 100)])
544 [(1, 48), (50, 100)]
544 [(1, 48), (50, 100)]
545 >>> unionranges([(1, 2), (3, 4), (5, 6)])
545 >>> unionranges([(1, 2), (3, 4), (5, 6)])
546 [(1, 6)]
546 [(1, 6)]
547 """
547 """
548 rangeslist = sorted(set(rangeslist))
548 rangeslist = sorted(set(rangeslist))
549 unioned = []
549 unioned = []
550 if rangeslist:
550 if rangeslist:
551 unioned, rangeslist = [rangeslist[0]], rangeslist[1:]
551 unioned, rangeslist = [rangeslist[0]], rangeslist[1:]
552 for a, b in rangeslist:
552 for a, b in rangeslist:
553 c, d = unioned[-1]
553 c, d = unioned[-1]
554 if a > d + 1:
554 if a > d + 1:
555 unioned.append((a, b))
555 unioned.append((a, b))
556 else:
556 else:
557 unioned[-1] = (c, max(b, d))
557 unioned[-1] = (c, max(b, d))
558 return unioned
558 return unioned
559
559
560
560
561 def difflineranges(content1, content2):
561 def difflineranges(content1, content2):
562 """Return list of line number ranges in content2 that differ from content1.
562 """Return list of line number ranges in content2 that differ from content1.
563
563
564 Line numbers are 1-based. The numbers are the first and last line contained
564 Line numbers are 1-based. The numbers are the first and last line contained
565 in the range. Single-line ranges have the same line number for the first and
565 in the range. Single-line ranges have the same line number for the first and
566 last line. Excludes any empty ranges that result from lines that are only
566 last line. Excludes any empty ranges that result from lines that are only
567 present in content1. Relies on mdiff's idea of where the line endings are in
567 present in content1. Relies on mdiff's idea of where the line endings are in
568 the string.
568 the string.
569
569
570 >>> from mercurial import pycompat
570 >>> from mercurial import pycompat
571 >>> lines = lambda s: b'\\n'.join([c for c in pycompat.iterbytestr(s)])
571 >>> lines = lambda s: b'\\n'.join([c for c in pycompat.iterbytestr(s)])
572 >>> difflineranges2 = lambda a, b: difflineranges(lines(a), lines(b))
572 >>> difflineranges2 = lambda a, b: difflineranges(lines(a), lines(b))
573 >>> difflineranges2(b'', b'')
573 >>> difflineranges2(b'', b'')
574 []
574 []
575 >>> difflineranges2(b'a', b'')
575 >>> difflineranges2(b'a', b'')
576 []
576 []
577 >>> difflineranges2(b'', b'A')
577 >>> difflineranges2(b'', b'A')
578 [(1, 1)]
578 [(1, 1)]
579 >>> difflineranges2(b'a', b'a')
579 >>> difflineranges2(b'a', b'a')
580 []
580 []
581 >>> difflineranges2(b'a', b'A')
581 >>> difflineranges2(b'a', b'A')
582 [(1, 1)]
582 [(1, 1)]
583 >>> difflineranges2(b'ab', b'')
583 >>> difflineranges2(b'ab', b'')
584 []
584 []
585 >>> difflineranges2(b'', b'AB')
585 >>> difflineranges2(b'', b'AB')
586 [(1, 2)]
586 [(1, 2)]
587 >>> difflineranges2(b'abc', b'ac')
587 >>> difflineranges2(b'abc', b'ac')
588 []
588 []
589 >>> difflineranges2(b'ab', b'aCb')
589 >>> difflineranges2(b'ab', b'aCb')
590 [(2, 2)]
590 [(2, 2)]
591 >>> difflineranges2(b'abc', b'aBc')
591 >>> difflineranges2(b'abc', b'aBc')
592 [(2, 2)]
592 [(2, 2)]
593 >>> difflineranges2(b'ab', b'AB')
593 >>> difflineranges2(b'ab', b'AB')
594 [(1, 2)]
594 [(1, 2)]
595 >>> difflineranges2(b'abcde', b'aBcDe')
595 >>> difflineranges2(b'abcde', b'aBcDe')
596 [(2, 2), (4, 4)]
596 [(2, 2), (4, 4)]
597 >>> difflineranges2(b'abcde', b'aBCDe')
597 >>> difflineranges2(b'abcde', b'aBCDe')
598 [(2, 4)]
598 [(2, 4)]
599 """
599 """
600 ranges = []
600 ranges = []
601 for lines, kind in mdiff.allblocks(content1, content2):
601 for lines, kind in mdiff.allblocks(content1, content2):
602 firstline, lastline = lines[2:4]
602 firstline, lastline = lines[2:4]
603 if kind == b'!' and firstline != lastline:
603 if kind == b'!' and firstline != lastline:
604 ranges.append((firstline + 1, lastline))
604 ranges.append((firstline + 1, lastline))
605 return ranges
605 return ranges
606
606
607
607
608 def getbasectxs(repo, opts, revstofix):
608 def getbasectxs(repo, opts, revstofix):
609 """Returns a map of the base contexts for each revision
609 """Returns a map of the base contexts for each revision
610
610
611 The base contexts determine which lines are considered modified when we
611 The base contexts determine which lines are considered modified when we
612 attempt to fix just the modified lines in a file. It also determines which
612 attempt to fix just the modified lines in a file. It also determines which
613 files we attempt to fix, so it is important to compute this even when
613 files we attempt to fix, so it is important to compute this even when
614 --whole is used.
614 --whole is used.
615 """
615 """
616 # The --base flag overrides the usual logic, and we give every revision
616 # The --base flag overrides the usual logic, and we give every revision
617 # exactly the set of baserevs that the user specified.
617 # exactly the set of baserevs that the user specified.
618 if opts.get(b'base'):
618 if opts.get(b'base'):
619 baserevs = set(scmutil.revrange(repo, opts.get(b'base')))
619 baserevs = set(scmutil.revrange(repo, opts.get(b'base')))
620 if not baserevs:
620 if not baserevs:
621 baserevs = {nullrev}
621 baserevs = {nullrev}
622 basectxs = {repo[rev] for rev in baserevs}
622 basectxs = {repo[rev] for rev in baserevs}
623 return {rev: basectxs for rev in revstofix}
623 return {rev: basectxs for rev in revstofix}
624
624
625 # Proceed in topological order so that we can easily determine each
625 # Proceed in topological order so that we can easily determine each
626 # revision's baserevs by looking at its parents and their baserevs.
626 # revision's baserevs by looking at its parents and their baserevs.
627 basectxs = collections.defaultdict(set)
627 basectxs = collections.defaultdict(set)
628 for rev in sorted(revstofix):
628 for rev in sorted(revstofix):
629 ctx = repo[rev]
629 ctx = repo[rev]
630 for pctx in ctx.parents():
630 for pctx in ctx.parents():
631 if pctx.rev() in basectxs:
631 if pctx.rev() in basectxs:
632 basectxs[rev].update(basectxs[pctx.rev()])
632 basectxs[rev].update(basectxs[pctx.rev()])
633 else:
633 else:
634 basectxs[rev].add(pctx)
634 basectxs[rev].add(pctx)
635 return basectxs
635 return basectxs
636
636
637
637
638 def _prefetchfiles(repo, workqueue, basepaths):
638 def _prefetchfiles(repo, workqueue, basepaths):
639 toprefetch = set()
639 toprefetch = set()
640
640
641 # Prefetch the files that will be fixed.
641 # Prefetch the files that will be fixed.
642 for rev, path in workqueue:
642 for rev, path in workqueue:
643 if rev == wdirrev:
643 if rev == wdirrev:
644 continue
644 continue
645 toprefetch.add((rev, path))
645 toprefetch.add((rev, path))
646
646
647 # Prefetch the base contents for lineranges().
647 # Prefetch the base contents for lineranges().
648 for (baserev, fixrev, path), basepath in basepaths.items():
648 for (baserev, fixrev, path), basepath in basepaths.items():
649 toprefetch.add((baserev, basepath))
649 toprefetch.add((baserev, basepath))
650
650
651 if toprefetch:
651 if toprefetch:
652 scmutil.prefetchfiles(
652 scmutil.prefetchfiles(
653 repo,
653 repo,
654 [
654 [
655 (rev, scmutil.matchfiles(repo, [path]))
655 (rev, scmutil.matchfiles(repo, [path]))
656 for rev, path in toprefetch
656 for rev, path in toprefetch
657 ],
657 ],
658 )
658 )
659
659
660
660
661 def fixfile(ui, repo, opts, fixers, fixctx, path, basepaths, basectxs):
661 def fixfile(ui, repo, opts, fixers, fixctx, path, basepaths, basectxs):
662 """Run any configured fixers that should affect the file in this context
662 """Run any configured fixers that should affect the file in this context
663
663
664 Returns the file content that results from applying the fixers in some order
664 Returns the file content that results from applying the fixers in some order
665 starting with the file's content in the fixctx. Fixers that support line
665 starting with the file's content in the fixctx. Fixers that support line
666 ranges will affect lines that have changed relative to any of the basectxs
666 ranges will affect lines that have changed relative to any of the basectxs
667 (i.e. they will only avoid lines that are common to all basectxs).
667 (i.e. they will only avoid lines that are common to all basectxs).
668
668
669 A fixer tool's stdout will become the file's new content if and only if it
669 A fixer tool's stdout will become the file's new content if and only if it
670 exits with code zero. The fixer tool's working directory is the repository's
670 exits with code zero. The fixer tool's working directory is the repository's
671 root.
671 root.
672 """
672 """
673 metadata = {}
673 metadata = {}
674 newdata = fixctx[path].data()
674 newdata = fixctx[path].data()
675 for fixername, fixer in pycompat.iteritems(fixers):
675 for fixername, fixer in pycompat.iteritems(fixers):
676 if fixer.affects(opts, fixctx, path):
676 if fixer.affects(opts, fixctx, path):
677 ranges = lineranges(
677 ranges = lineranges(
678 opts, path, basepaths, basectxs, fixctx, newdata
678 opts, path, basepaths, basectxs, fixctx, newdata
679 )
679 )
680 command = fixer.command(ui, path, ranges)
680 command = fixer.command(ui, path, ranges)
681 if command is None:
681 if command is None:
682 continue
682 continue
683 ui.debug(b'subprocess: %s\n' % (command,))
683 ui.debug(b'subprocess: %s\n' % (command,))
684 proc = subprocess.Popen(
684 proc = subprocess.Popen(
685 procutil.tonativestr(command),
685 procutil.tonativestr(command),
686 shell=True,
686 shell=True,
687 cwd=procutil.tonativestr(repo.root),
687 cwd=procutil.tonativestr(repo.root),
688 stdin=subprocess.PIPE,
688 stdin=subprocess.PIPE,
689 stdout=subprocess.PIPE,
689 stdout=subprocess.PIPE,
690 stderr=subprocess.PIPE,
690 stderr=subprocess.PIPE,
691 )
691 )
692 stdout, stderr = proc.communicate(newdata)
692 stdout, stderr = proc.communicate(newdata)
693 if stderr:
693 if stderr:
694 showstderr(ui, fixctx.rev(), fixername, stderr)
694 showstderr(ui, fixctx.rev(), fixername, stderr)
695 newerdata = stdout
695 newerdata = stdout
696 if fixer.shouldoutputmetadata():
696 if fixer.shouldoutputmetadata():
697 try:
697 try:
698 metadatajson, newerdata = stdout.split(b'\0', 1)
698 metadatajson, newerdata = stdout.split(b'\0', 1)
699 metadata[fixername] = pycompat.json_loads(metadatajson)
699 metadata[fixername] = pycompat.json_loads(metadatajson)
700 except ValueError:
700 except ValueError:
701 ui.warn(
701 ui.warn(
702 _(b'ignored invalid output from fixer tool: %s\n')
702 _(b'ignored invalid output from fixer tool: %s\n')
703 % (fixername,)
703 % (fixername,)
704 )
704 )
705 continue
705 continue
706 else:
706 else:
707 metadata[fixername] = None
707 metadata[fixername] = None
708 if proc.returncode == 0:
708 if proc.returncode == 0:
709 newdata = newerdata
709 newdata = newerdata
710 else:
710 else:
711 if not stderr:
711 if not stderr:
712 message = _(b'exited with status %d\n') % (proc.returncode,)
712 message = _(b'exited with status %d\n') % (proc.returncode,)
713 showstderr(ui, fixctx.rev(), fixername, message)
713 showstderr(ui, fixctx.rev(), fixername, message)
714 checktoolfailureaction(
714 checktoolfailureaction(
715 ui,
715 ui,
716 _(b'no fixes will be applied'),
716 _(b'no fixes will be applied'),
717 hint=_(
717 hint=_(
718 b'use --config fix.failure=continue to apply any '
718 b'use --config fix.failure=continue to apply any '
719 b'successful fixes anyway'
719 b'successful fixes anyway'
720 ),
720 ),
721 )
721 )
722 return metadata, newdata
722 return metadata, newdata
723
723
724
724
725 def showstderr(ui, rev, fixername, stderr):
725 def showstderr(ui, rev, fixername, stderr):
726 """Writes the lines of the stderr string as warnings on the ui
726 """Writes the lines of the stderr string as warnings on the ui
727
727
728 Uses the revision number and fixername to give more context to each line of
728 Uses the revision number and fixername to give more context to each line of
729 the error message. Doesn't include file names, since those take up a lot of
729 the error message. Doesn't include file names, since those take up a lot of
730 space and would tend to be included in the error message if they were
730 space and would tend to be included in the error message if they were
731 relevant.
731 relevant.
732 """
732 """
733 for line in re.split(b'[\r\n]+', stderr):
733 for line in re.split(b'[\r\n]+', stderr):
734 if line:
734 if line:
735 ui.warn(b'[')
735 ui.warn(b'[')
736 if rev is None:
736 if rev is None:
737 ui.warn(_(b'wdir'), label=b'evolve.rev')
737 ui.warn(_(b'wdir'), label=b'evolve.rev')
738 else:
738 else:
739 ui.warn(b'%d' % rev, label=b'evolve.rev')
739 ui.warn(b'%d' % rev, label=b'evolve.rev')
740 ui.warn(b'] %s: %s\n' % (fixername, line))
740 ui.warn(b'] %s: %s\n' % (fixername, line))
741
741
742
742
743 def writeworkingdir(repo, ctx, filedata, replacements):
743 def writeworkingdir(repo, ctx, filedata, replacements):
744 """Write new content to the working copy and check out the new p1 if any
744 """Write new content to the working copy and check out the new p1 if any
745
745
746 We check out a new revision if and only if we fixed something in both the
746 We check out a new revision if and only if we fixed something in both the
747 working directory and its parent revision. This avoids the need for a full
747 working directory and its parent revision. This avoids the need for a full
748 update/merge, and means that the working directory simply isn't affected
748 update/merge, and means that the working directory simply isn't affected
749 unless the --working-dir flag is given.
749 unless the --working-dir flag is given.
750
750
751 Directly updates the dirstate for the affected files.
751 Directly updates the dirstate for the affected files.
752 """
752 """
753 for path, data in pycompat.iteritems(filedata):
753 for path, data in pycompat.iteritems(filedata):
754 fctx = ctx[path]
754 fctx = ctx[path]
755 fctx.write(data, fctx.flags())
755 fctx.write(data, fctx.flags())
756 if repo.dirstate[path] == b'n':
756 if repo.dirstate[path] == b'n':
757 repo.dirstate.normallookup(path)
757 repo.dirstate.normallookup(path)
758
758
759 oldparentnodes = repo.dirstate.parents()
759 oldparentnodes = repo.dirstate.parents()
760 newparentnodes = [replacements.get(n, n) for n in oldparentnodes]
760 newparentnodes = [replacements.get(n, n) for n in oldparentnodes]
761 if newparentnodes != oldparentnodes:
761 if newparentnodes != oldparentnodes:
762 repo.setparents(*newparentnodes)
762 repo.setparents(*newparentnodes)
763
763
764
764
765 def replacerev(ui, repo, ctx, filedata, replacements):
765 def replacerev(ui, repo, ctx, filedata, replacements):
766 """Commit a new revision like the given one, but with file content changes
766 """Commit a new revision like the given one, but with file content changes
767
767
768 "ctx" is the original revision to be replaced by a modified one.
768 "ctx" is the original revision to be replaced by a modified one.
769
769
770 "filedata" is a dict that maps paths to their new file content. All other
770 "filedata" is a dict that maps paths to their new file content. All other
771 paths will be recreated from the original revision without changes.
771 paths will be recreated from the original revision without changes.
772 "filedata" may contain paths that didn't exist in the original revision;
772 "filedata" may contain paths that didn't exist in the original revision;
773 they will be added.
773 they will be added.
774
774
775 "replacements" is a dict that maps a single node to a single node, and it is
775 "replacements" is a dict that maps a single node to a single node, and it is
776 updated to indicate the original revision is replaced by the newly created
776 updated to indicate the original revision is replaced by the newly created
777 one. No entry is added if the replacement's node already exists.
777 one. No entry is added if the replacement's node already exists.
778
778
779 The new revision has the same parents as the old one, unless those parents
779 The new revision has the same parents as the old one, unless those parents
780 have already been replaced, in which case those replacements are the parents
780 have already been replaced, in which case those replacements are the parents
781 of this new revision. Thus, if revisions are replaced in topological order,
781 of this new revision. Thus, if revisions are replaced in topological order,
782 there is no need to rebase them into the original topology later.
782 there is no need to rebase them into the original topology later.
783 """
783 """
784
784
785 p1rev, p2rev = repo.changelog.parentrevs(ctx.rev())
785 p1rev, p2rev = repo.changelog.parentrevs(ctx.rev())
786 p1ctx, p2ctx = repo[p1rev], repo[p2rev]
786 p1ctx, p2ctx = repo[p1rev], repo[p2rev]
787 newp1node = replacements.get(p1ctx.node(), p1ctx.node())
787 newp1node = replacements.get(p1ctx.node(), p1ctx.node())
788 newp2node = replacements.get(p2ctx.node(), p2ctx.node())
788 newp2node = replacements.get(p2ctx.node(), p2ctx.node())
789
789
790 # We don't want to create a revision that has no changes from the original,
790 # We don't want to create a revision that has no changes from the original,
791 # but we should if the original revision's parent has been replaced.
791 # but we should if the original revision's parent has been replaced.
792 # Otherwise, we would produce an orphan that needs no actual human
792 # Otherwise, we would produce an orphan that needs no actual human
793 # intervention to evolve. We can't rely on commit() to avoid creating the
793 # intervention to evolve. We can't rely on commit() to avoid creating the
794 # un-needed revision because the extra field added below produces a new hash
794 # un-needed revision because the extra field added below produces a new hash
795 # regardless of file content changes.
795 # regardless of file content changes.
796 if (
796 if (
797 not filedata
797 not filedata
798 and p1ctx.node() not in replacements
798 and p1ctx.node() not in replacements
799 and p2ctx.node() not in replacements
799 and p2ctx.node() not in replacements
800 ):
800 ):
801 return
801 return
802
802
803 extra = ctx.extra().copy()
803 extra = ctx.extra().copy()
804 extra[b'fix_source'] = ctx.hex()
804 extra[b'fix_source'] = ctx.hex()
805
805
806 wctx = context.overlayworkingctx(repo)
806 wctx = context.overlayworkingctx(repo)
807 wctx.setbase(repo[newp1node])
807 wctx.setbase(repo[newp1node])
808 merge.revert_to(ctx, wc=wctx)
808 merge.revert_to(ctx, wc=wctx)
809 copies.graftcopies(wctx, ctx, ctx.p1())
809 copies.graftcopies(wctx, ctx, ctx.p1())
810
810
811 for path in filedata.keys():
811 for path in filedata.keys():
812 fctx = ctx[path]
812 fctx = ctx[path]
813 copysource = fctx.copysource()
813 copysource = fctx.copysource()
814 wctx.write(path, filedata[path], flags=fctx.flags())
814 wctx.write(path, filedata[path], flags=fctx.flags())
815 if copysource:
815 if copysource:
816 wctx.markcopied(path, copysource)
816 wctx.markcopied(path, copysource)
817
817
818 desc = rewriteutil.update_hash_refs(
818 desc = rewriteutil.update_hash_refs(
819 repo,
819 repo,
820 ctx.description(),
820 ctx.description(),
821 {oldnode: [newnode] for oldnode, newnode in replacements.items()},
821 {oldnode: [newnode] for oldnode, newnode in replacements.items()},
822 )
822 )
823
823
824 memctx = wctx.tomemctx(
824 memctx = wctx.tomemctx(
825 text=desc,
825 text=desc,
826 branch=ctx.branch(),
826 branch=ctx.branch(),
827 extra=extra,
827 extra=extra,
828 date=ctx.date(),
828 date=ctx.date(),
829 parents=(newp1node, newp2node),
829 parents=(newp1node, newp2node),
830 user=ctx.user(),
830 user=ctx.user(),
831 )
831 )
832
832
833 sucnode = memctx.commit()
833 sucnode = memctx.commit()
834 prenode = ctx.node()
834 prenode = ctx.node()
835 if prenode == sucnode:
835 if prenode == sucnode:
836 ui.debug(b'node %s already existed\n' % (ctx.hex()))
836 ui.debug(b'node %s already existed\n' % (ctx.hex()))
837 else:
837 else:
838 replacements[ctx.node()] = sucnode
838 replacements[ctx.node()] = sucnode
839
839
840
840
841 def getfixers(ui):
841 def getfixers(ui):
842 """Returns a map of configured fixer tools indexed by their names
842 """Returns a map of configured fixer tools indexed by their names
843
843
844 Each value is a Fixer object with methods that implement the behavior of the
844 Each value is a Fixer object with methods that implement the behavior of the
845 fixer's config suboptions. Does not validate the config values.
845 fixer's config suboptions. Does not validate the config values.
846 """
846 """
847 fixers = {}
847 fixers = {}
848 for name in fixernames(ui):
848 for name in fixernames(ui):
849 enabled = ui.configbool(b'fix', name + b':enabled')
849 enabled = ui.configbool(b'fix', name + b':enabled')
850 command = ui.config(b'fix', name + b':command')
850 command = ui.config(b'fix', name + b':command')
851 pattern = ui.config(b'fix', name + b':pattern')
851 pattern = ui.config(b'fix', name + b':pattern')
852 linerange = ui.config(b'fix', name + b':linerange')
852 linerange = ui.config(b'fix', name + b':linerange')
853 priority = ui.configint(b'fix', name + b':priority')
853 priority = ui.configint(b'fix', name + b':priority')
854 metadata = ui.configbool(b'fix', name + b':metadata')
854 metadata = ui.configbool(b'fix', name + b':metadata')
855 skipclean = ui.configbool(b'fix', name + b':skipclean')
855 skipclean = ui.configbool(b'fix', name + b':skipclean')
856 # Don't use a fixer if it has no pattern configured. It would be
856 # Don't use a fixer if it has no pattern configured. It would be
857 # dangerous to let it affect all files. It would be pointless to let it
857 # dangerous to let it affect all files. It would be pointless to let it
858 # affect no files. There is no reasonable subset of files to use as the
858 # affect no files. There is no reasonable subset of files to use as the
859 # default.
859 # default.
860 if command is None:
860 if command is None:
861 ui.warn(
861 ui.warn(
862 _(b'fixer tool has no command configuration: %s\n') % (name,)
862 _(b'fixer tool has no command configuration: %s\n') % (name,)
863 )
863 )
864 elif pattern is None:
864 elif pattern is None:
865 ui.warn(
865 ui.warn(
866 _(b'fixer tool has no pattern configuration: %s\n') % (name,)
866 _(b'fixer tool has no pattern configuration: %s\n') % (name,)
867 )
867 )
868 elif not enabled:
868 elif not enabled:
869 ui.debug(b'ignoring disabled fixer tool: %s\n' % (name,))
869 ui.debug(b'ignoring disabled fixer tool: %s\n' % (name,))
870 else:
870 else:
871 fixers[name] = Fixer(
871 fixers[name] = Fixer(
872 command, pattern, linerange, priority, metadata, skipclean
872 command, pattern, linerange, priority, metadata, skipclean
873 )
873 )
874 return collections.OrderedDict(
874 return collections.OrderedDict(
875 sorted(fixers.items(), key=lambda item: item[1]._priority, reverse=True)
875 sorted(fixers.items(), key=lambda item: item[1]._priority, reverse=True)
876 )
876 )
877
877
878
878
879 def fixernames(ui):
879 def fixernames(ui):
880 """Returns the names of [fix] config options that have suboptions"""
880 """Returns the names of [fix] config options that have suboptions"""
881 names = set()
881 names = set()
882 for k, v in ui.configitems(b'fix'):
882 for k, v in ui.configitems(b'fix'):
883 if b':' in k:
883 if b':' in k:
884 names.add(k.split(b':', 1)[0])
884 names.add(k.split(b':', 1)[0])
885 return names
885 return names
886
886
887
887
888 class Fixer(object):
888 class Fixer(object):
889 """Wraps the raw config values for a fixer with methods"""
889 """Wraps the raw config values for a fixer with methods"""
890
890
891 def __init__(
891 def __init__(
892 self, command, pattern, linerange, priority, metadata, skipclean
892 self, command, pattern, linerange, priority, metadata, skipclean
893 ):
893 ):
894 self._command = command
894 self._command = command
895 self._pattern = pattern
895 self._pattern = pattern
896 self._linerange = linerange
896 self._linerange = linerange
897 self._priority = priority
897 self._priority = priority
898 self._metadata = metadata
898 self._metadata = metadata
899 self._skipclean = skipclean
899 self._skipclean = skipclean
900
900
901 def affects(self, opts, fixctx, path):
901 def affects(self, opts, fixctx, path):
902 """Should this fixer run on the file at the given path and context?"""
902 """Should this fixer run on the file at the given path and context?"""
903 repo = fixctx.repo()
903 repo = fixctx.repo()
904 matcher = matchmod.match(
904 matcher = matchmod.match(
905 repo.root, repo.root, [self._pattern], ctx=fixctx
905 repo.root, repo.root, [self._pattern], ctx=fixctx
906 )
906 )
907 return matcher(path)
907 return matcher(path)
908
908
909 def shouldoutputmetadata(self):
909 def shouldoutputmetadata(self):
910 """Should the stdout of this fixer start with JSON and a null byte?"""
910 """Should the stdout of this fixer start with JSON and a null byte?"""
911 return self._metadata
911 return self._metadata
912
912
913 def command(self, ui, path, ranges):
913 def command(self, ui, path, ranges):
914 """A shell command to use to invoke this fixer on the given file/lines
914 """A shell command to use to invoke this fixer on the given file/lines
915
915
916 May return None if there is no appropriate command to run for the given
916 May return None if there is no appropriate command to run for the given
917 parameters.
917 parameters.
918 """
918 """
919 expand = cmdutil.rendercommandtemplate
919 expand = cmdutil.rendercommandtemplate
920 parts = [
920 parts = [
921 expand(
921 expand(
922 ui,
922 ui,
923 self._command,
923 self._command,
924 {b'rootpath': path, b'basename': os.path.basename(path)},
924 {b'rootpath': path, b'basename': os.path.basename(path)},
925 )
925 )
926 ]
926 ]
927 if self._linerange:
927 if self._linerange:
928 if self._skipclean and not ranges:
928 if self._skipclean and not ranges:
929 # No line ranges to fix, so don't run the fixer.
929 # No line ranges to fix, so don't run the fixer.
930 return None
930 return None
931 for first, last in ranges:
931 for first, last in ranges:
932 parts.append(
932 parts.append(
933 expand(
933 expand(
934 ui, self._linerange, {b'first': first, b'last': last}
934 ui, self._linerange, {b'first': first, b'last': last}
935 )
935 )
936 )
936 )
937 return b' '.join(parts)
937 return b' '.join(parts)
@@ -1,556 +1,553 b''
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 > strip =
24 > strip =
25 > debugdrawdag=$TESTDIR/drawdag.py
25 > debugdrawdag=$TESTDIR/drawdag.py
26 > [fix]
26 > [fix]
27 > uppercase-whole-file:command="$PYTHON" $UPPERCASEPY
27 > uppercase-whole-file:command="$PYTHON" $UPPERCASEPY
28 > uppercase-whole-file:pattern=set:**
28 > uppercase-whole-file:pattern=set:**
29 > EOF
29 > EOF
30
30
31 This tests the only behavior that should really be affected by obsolescence, so
31 This tests the only behavior that should really be affected by obsolescence, so
32 we'll test it with evolution off and on. This only changes the revision
32 we'll test it with evolution off and on. This only changes the revision
33 numbers, if all is well.
33 numbers, if all is well.
34
34
35 #testcases obsstore-off obsstore-on
35 #testcases obsstore-off obsstore-on
36 #if obsstore-on
36 #if obsstore-on
37 $ cat >> $HGRCPATH <<EOF
37 $ cat >> $HGRCPATH <<EOF
38 > [experimental]
38 > [experimental]
39 > evolution.createmarkers=True
39 > evolution.createmarkers=True
40 > evolution.allowunstable=True
40 > evolution.allowunstable=True
41 > EOF
41 > EOF
42 #endif
42 #endif
43
43
44 Setting up the test topology. Scroll down to see the graph produced. We make it
44 Setting up the test topology. Scroll down to see the graph produced. We make it
45 clear which files were modified in each revision. It's enough to test at the
45 clear which files were modified in each revision. It's enough to test at the
46 file granularity, because that demonstrates which baserevs were diffed against.
46 file granularity, because that demonstrates which baserevs were diffed against.
47 The computation of changed lines is orthogonal and tested separately.
47 The computation of changed lines is orthogonal and tested separately.
48
48
49 $ hg init repo
49 $ hg init repo
50 $ cd repo
50 $ cd repo
51
51
52 $ printf "aaaa\n" > a
52 $ printf "aaaa\n" > a
53 $ hg commit -Am "change A"
53 $ hg commit -Am "change A"
54 adding a
54 adding a
55 $ printf "bbbb\n" > b
55 $ printf "bbbb\n" > b
56 $ hg commit -Am "change B"
56 $ hg commit -Am "change B"
57 adding b
57 adding b
58 $ printf "cccc\n" > c
58 $ printf "cccc\n" > c
59 $ hg commit -Am "change C"
59 $ hg commit -Am "change C"
60 adding c
60 adding c
61 $ hg checkout 0
61 $ hg checkout 0
62 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
62 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
63 $ printf "dddd\n" > d
63 $ printf "dddd\n" > d
64 $ hg commit -Am "change D"
64 $ hg commit -Am "change D"
65 adding d
65 adding d
66 created new head
66 created new head
67 $ hg merge -r 2
67 $ hg merge -r 2
68 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
68 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
69 (branch merge, don't forget to commit)
69 (branch merge, don't forget to commit)
70 $ printf "eeee\n" > e
70 $ printf "eeee\n" > e
71 $ hg commit -Am "change E"
71 $ hg commit -Am "change E"
72 adding e
72 adding e
73 $ hg checkout 0
73 $ hg checkout 0
74 0 files updated, 0 files merged, 4 files removed, 0 files unresolved
74 0 files updated, 0 files merged, 4 files removed, 0 files unresolved
75 $ printf "ffff\n" > f
75 $ printf "ffff\n" > f
76 $ hg commit -Am "change F"
76 $ hg commit -Am "change F"
77 adding f
77 adding f
78 created new head
78 created new head
79 $ hg checkout 0
79 $ hg checkout 0
80 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
80 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
81 $ printf "gggg\n" > g
81 $ printf "gggg\n" > g
82 $ hg commit -Am "change G"
82 $ hg commit -Am "change G"
83 adding g
83 adding g
84 created new head
84 created new head
85 $ hg merge -r 5
85 $ hg merge -r 5
86 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
86 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
87 (branch merge, don't forget to commit)
87 (branch merge, don't forget to commit)
88 $ printf "hhhh\n" > h
88 $ printf "hhhh\n" > h
89 $ hg commit -Am "change H (child of b53d63e816fb and 0e49f92ee6e9)"
89 $ hg commit -Am "change H (child of b53d63e816fb and 0e49f92ee6e9)"
90 adding h
90 adding h
91 $ hg merge -r 4
91 $ hg merge -r 4
92 4 files updated, 0 files merged, 0 files removed, 0 files unresolved
92 4 files updated, 0 files merged, 0 files removed, 0 files unresolved
93 (branch merge, don't forget to commit)
93 (branch merge, don't forget to commit)
94 $ printf "iiii\n" > i
94 $ printf "iiii\n" > i
95 $ hg commit -Am "change I"
95 $ hg commit -Am "change I"
96 adding i
96 adding i
97 $ hg checkout 2
97 $ hg checkout 2
98 0 files updated, 0 files merged, 6 files removed, 0 files unresolved
98 0 files updated, 0 files merged, 6 files removed, 0 files unresolved
99 $ printf "jjjj\n" > j
99 $ printf "jjjj\n" > j
100 $ hg commit -Am "change J (child of 7f371349286e)"
100 $ hg commit -Am "change J (child of 7f371349286e)"
101 adding j
101 adding j
102 created new head
102 created new head
103 $ hg checkout 7
103 $ hg checkout 7
104 3 files updated, 0 files merged, 3 files removed, 0 files unresolved
104 3 files updated, 0 files merged, 3 files removed, 0 files unresolved
105 $ printf "kkkk\n" > k
105 $ printf "kkkk\n" > k
106 $ hg add
106 $ hg add
107 adding k
107 adding k
108
108
109 $ hg log --graph --template '{rev}:{node|short} {desc}\n'
109 $ hg log --graph --template '{rev}:{node|short} {desc}\n'
110 o 9:884041ccc490 change J (child of 7f371349286e)
110 o 9:884041ccc490 change J (child of 7f371349286e)
111 |
111 |
112 | o 8:b7c772105fd2 change I
112 | o 8:b7c772105fd2 change I
113 | |\
113 | |\
114 | | @ 7:4e7b9312dad2 change H (child of b53d63e816fb and 0e49f92ee6e9)
114 | | @ 7:4e7b9312dad2 change H (child of b53d63e816fb and 0e49f92ee6e9)
115 | | |\
115 | | |\
116 | | | o 6:0e49f92ee6e9 change G
116 | | | o 6:0e49f92ee6e9 change G
117 | | | |
117 | | | |
118 | | o | 5:b53d63e816fb change F
118 | | o | 5:b53d63e816fb change F
119 | | |/
119 | | |/
120 | o | 4:ddad58af5e51 change E
120 | o | 4:ddad58af5e51 change E
121 |/| |
121 |/| |
122 | o | 3:c015ebfd2bfe change D
122 | o | 3:c015ebfd2bfe change D
123 | |/
123 | |/
124 o | 2:7f371349286e change C
124 o | 2:7f371349286e change C
125 | |
125 | |
126 o | 1:388fdd33fea0 change B
126 o | 1:388fdd33fea0 change B
127 |/
127 |/
128 o 0:a55a84d97a24 change A
128 o 0:a55a84d97a24 change A
129
129
130
130
131 Fix all but the root revision and its four children.
131 Fix all but the root revision and its four children.
132
132
133 $ hg fix -r '2|4|7|8|9' --working-dir
133 $ hg fix -r '2|4|7|8|9' --working-dir
134 saved backup bundle to * (glob) (obsstore-off !)
134 saved backup bundle to * (glob) (obsstore-off !)
135
135
136 The five revisions remain, but the other revisions were fixed and replaced. All
136 The five revisions remain, but the other revisions were fixed and replaced. All
137 parent pointers have been accurately set to reproduce the previous topology
137 parent pointers have been accurately set to reproduce the previous topology
138 (though it is rendered in a slightly different order now).
138 (though it is rendered in a slightly different order now).
139
139
140 #if obsstore-on
140 #if obsstore-on
141 $ hg log --graph --template '{rev}:{node|short} {desc}\n'
141 $ hg log --graph --template '{rev}:{node|short} {desc}\n'
142 o 14:d8d0e7974598 change J (child of 89de0da1d5da)
142 o 14:d8d0e7974598 change J (child of 89de0da1d5da)
143 |
143 |
144 | o 13:4fc0b354461e change I
144 | o 13:4fc0b354461e change I
145 | |\
145 | |\
146 | | @ 12:1c45f3923443 change H (child of b53d63e816fb and 0e49f92ee6e9)
146 | | @ 12:1c45f3923443 change H (child of b53d63e816fb and 0e49f92ee6e9)
147 | | |\
147 | | |\
148 | o | | 11:d75754455722 change E
148 | o | | 11:d75754455722 change E
149 |/| | |
149 |/| | |
150 o | | | 10:89de0da1d5da change C
150 o | | | 10:89de0da1d5da change C
151 | | | |
151 | | | |
152 | | | o 6:0e49f92ee6e9 change G
152 | | | o 6:0e49f92ee6e9 change G
153 | | | |
153 | | | |
154 | | o | 5:b53d63e816fb change F
154 | | o | 5:b53d63e816fb change F
155 | | |/
155 | | |/
156 | o / 3:c015ebfd2bfe change D
156 | o / 3:c015ebfd2bfe change D
157 | |/
157 | |/
158 o / 1:388fdd33fea0 change B
158 o / 1:388fdd33fea0 change B
159 |/
159 |/
160 o 0:a55a84d97a24 change A
160 o 0:a55a84d97a24 change A
161
161
162 $ C=10
162 $ C=10
163 $ E=11
163 $ E=11
164 $ H=12
164 $ H=12
165 $ I=13
165 $ I=13
166 $ J=14
166 $ J=14
167 #else
167 #else
168 $ hg log --graph --template '{rev}:{node|short} {desc}\n'
168 $ hg log --graph --template '{rev}:{node|short} {desc}\n'
169 o 9:d8d0e7974598 change J (child of 89de0da1d5da)
169 o 9:d8d0e7974598 change J (child of 89de0da1d5da)
170 |
170 |
171 | o 8:4fc0b354461e change I
171 | o 8:4fc0b354461e change I
172 | |\
172 | |\
173 | | @ 7:1c45f3923443 change H (child of b53d63e816fb and 0e49f92ee6e9)
173 | | @ 7:1c45f3923443 change H (child of b53d63e816fb and 0e49f92ee6e9)
174 | | |\
174 | | |\
175 | o | | 6:d75754455722 change E
175 | o | | 6:d75754455722 change E
176 |/| | |
176 |/| | |
177 o | | | 5:89de0da1d5da change C
177 o | | | 5:89de0da1d5da change C
178 | | | |
178 | | | |
179 | | | o 4:0e49f92ee6e9 change G
179 | | | o 4:0e49f92ee6e9 change G
180 | | | |
180 | | | |
181 | | o | 3:b53d63e816fb change F
181 | | o | 3:b53d63e816fb change F
182 | | |/
182 | | |/
183 | o / 2:c015ebfd2bfe change D
183 | o / 2:c015ebfd2bfe change D
184 | |/
184 | |/
185 o / 1:388fdd33fea0 change B
185 o / 1:388fdd33fea0 change B
186 |/
186 |/
187 o 0:a55a84d97a24 change A
187 o 0:a55a84d97a24 change A
188
188
189 $ C=5
189 $ C=5
190 $ E=6
190 $ E=6
191 $ H=7
191 $ H=7
192 $ I=8
192 $ I=8
193 $ J=9
193 $ J=9
194 #endif
194 #endif
195
195
196 Change C is a root of the set being fixed, so all we fix is what has changed
196 Change C is a root of the set being fixed, so all we fix is what has changed
197 since its parent. That parent, change B, is its baserev.
197 since its parent. That parent, change B, is its baserev.
198
198
199 $ hg cat -r $C 'set:**'
199 $ hg cat -r $C 'set:**'
200 aaaa
200 aaaa
201 bbbb
201 bbbb
202 CCCC
202 CCCC
203
203
204 Change E is a merge with only one parent being fixed. Its baserevs are the
204 Change E is a merge with only one parent being fixed. Its baserevs are the
205 unfixed parent plus the baserevs of the other parent. This evaluates to changes
205 unfixed parent plus the baserevs of the other parent. This evaluates to changes
206 B and D. We now have to decide what it means to incrementally fix a merge
206 B and D. We now have to decide what it means to incrementally fix a merge
207 commit. We choose to fix anything that has changed versus any baserev. Only the
207 commit. We choose to fix anything that has changed versus any baserev. Only the
208 undisturbed content of the common ancestor, change A, is unfixed.
208 undisturbed content of the common ancestor, change A, is unfixed.
209
209
210 $ hg cat -r $E 'set:**'
210 $ hg cat -r $E 'set:**'
211 aaaa
211 aaaa
212 BBBB
212 BBBB
213 CCCC
213 CCCC
214 DDDD
214 DDDD
215 EEEE
215 EEEE
216
216
217 Change H is a merge with neither parent being fixed. This is essentially
217 Change H is a merge with neither parent being fixed. This is essentially
218 equivalent to the previous case because there is still only one baserev for
218 equivalent to the previous case because there is still only one baserev for
219 each parent of the merge.
219 each parent of the merge.
220
220
221 $ hg cat -r $H 'set:**'
221 $ hg cat -r $H 'set:**'
222 aaaa
222 aaaa
223 FFFF
223 FFFF
224 GGGG
224 GGGG
225 HHHH
225 HHHH
226
226
227 Change I is a merge that has four baserevs; two from each parent. We handle
227 Change I is a merge that has four baserevs; two from each parent. We handle
228 multiple baserevs in the same way regardless of how many came from each parent.
228 multiple baserevs in the same way regardless of how many came from each parent.
229 So, fixing change H will fix any files that were not exactly the same in each
229 So, fixing change H will fix any files that were not exactly the same in each
230 baserev.
230 baserev.
231
231
232 $ hg cat -r $I 'set:**'
232 $ hg cat -r $I 'set:**'
233 aaaa
233 aaaa
234 BBBB
234 BBBB
235 CCCC
235 CCCC
236 DDDD
236 DDDD
237 EEEE
237 EEEE
238 FFFF
238 FFFF
239 GGGG
239 GGGG
240 HHHH
240 HHHH
241 IIII
241 IIII
242
242
243 Change J is a simple case with one baserev, but its baserev is not its parent,
243 Change J is a simple case with one baserev, but its baserev is not its parent,
244 change C. Its baserev is its grandparent, change B.
244 change C. Its baserev is its grandparent, change B.
245
245
246 $ hg cat -r $J 'set:**'
246 $ hg cat -r $J 'set:**'
247 aaaa
247 aaaa
248 bbbb
248 bbbb
249 CCCC
249 CCCC
250 JJJJ
250 JJJJ
251
251
252 The working copy was dirty, so it is treated much like a revision. The baserevs
252 The working copy was dirty, so it is treated much like a revision. The baserevs
253 for the working copy are inherited from its parent, change H, because it is
253 for the working copy are inherited from its parent, change H, because it is
254 also being fixed.
254 also being fixed.
255
255
256 $ cat *
256 $ cat *
257 aaaa
257 aaaa
258 FFFF
258 FFFF
259 GGGG
259 GGGG
260 HHHH
260 HHHH
261 KKKK
261 KKKK
262
262
263 Change A was never a baserev because none of its children were to be fixed.
263 Change A was never a baserev because none of its children were to be fixed.
264
264
265 $ cd ..
265 $ cd ..
266
266
267
267
268 Test the --source option. We only do this with obsstore on to avoid duplicating
268 Test the --source option. We only do this with obsstore on to avoid duplicating
269 test code. We rely on the other tests to prove that obsolescence is not an
269 test code. We rely on the other tests to prove that obsolescence is not an
270 important factor here.
270 important factor here.
271
271
272 #if obsstore-on
272 #if obsstore-on
273 $ hg init source-arg
273 $ hg init source-arg
274 $ cd source-arg
274 $ cd source-arg
275 $ printf "aaaa\n" > a
275 $ printf "aaaa\n" > a
276 $ hg commit -Am "change A"
276 $ hg commit -Am "change A"
277 adding a
277 adding a
278 $ printf "bbbb\n" > b
278 $ printf "bbbb\n" > b
279 $ hg commit -Am "change B"
279 $ hg commit -Am "change B"
280 adding b
280 adding b
281 $ printf "cccc\n" > c
281 $ printf "cccc\n" > c
282 $ hg commit -Am "change C"
282 $ hg commit -Am "change C"
283 adding c
283 adding c
284 $ hg checkout 0
284 $ hg checkout 0
285 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
285 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
286 $ printf "dddd\n" > d
286 $ printf "dddd\n" > d
287 $ hg commit -Am "change D"
287 $ hg commit -Am "change D"
288 adding d
288 adding d
289 created new head
289 created new head
290 $ hg log --graph --template '{rev} {desc}\n'
290 $ hg log --graph --template '{rev} {desc}\n'
291 @ 3 change D
291 @ 3 change D
292 |
292 |
293 | o 2 change C
293 | o 2 change C
294 | |
294 | |
295 | o 1 change B
295 | o 1 change B
296 |/
296 |/
297 o 0 change A
297 o 0 change A
298
298
299
299
300 Test passing 'wdir()' to --source
300 Test passing 'wdir()' to --source
301 $ printf "xxxx\n" > x
301 $ printf "xxxx\n" > x
302 $ hg add x
302 $ hg add x
303 $ hg fix -s 'wdir()'
303 $ hg fix -s 'wdir()'
304 $ cat *
304 $ cat *
305 aaaa
305 aaaa
306 dddd
306 dddd
307 XXXX
307 XXXX
308
308
309 Test passing '.' to --source
309 Test passing '.' to --source
310 $ printf "xxxx\n" > x
310 $ printf "xxxx\n" > x
311 $ hg fix -s .
311 $ hg fix -s .
312 $ hg log --graph --template '{rev} {desc}\n'
312 $ hg log --graph --template '{rev} {desc}\n'
313 @ 4 change D
313 @ 4 change D
314 |
314 |
315 | o 2 change C
315 | o 2 change C
316 | |
316 | |
317 | o 1 change B
317 | o 1 change B
318 |/
318 |/
319 o 0 change A
319 o 0 change A
320
320
321 $ cat *
321 $ cat *
322 aaaa
322 aaaa
323 DDDD
323 DDDD
324 XXXX
324 XXXX
325 $ hg strip -qf 4
325 $ hg strip -qf 4
326 $ hg co -q 3
326 $ hg co -q 3
327
327
328 Test passing other branch to --source
328 Test passing other branch to --source
329 $ printf "xxxx\n" > x
329 $ printf "xxxx\n" > x
330 $ hg add x
330 $ hg add x
331 $ hg fix -s 2
331 $ hg fix -s 2
332 $ hg log --graph --template '{rev} {desc}\n'
332 $ hg log --graph --template '{rev} {desc}\n'
333 o 4 change C
333 o 4 change C
334 |
334 |
335 | @ 3 change D
335 | @ 3 change D
336 | |
336 | |
337 o | 1 change B
337 o | 1 change B
338 |/
338 |/
339 o 0 change A
339 o 0 change A
340
340
341 $ hg cat -r 4 b c
341 $ hg cat -r 4 b c
342 bbbb
342 bbbb
343 CCCC
343 CCCC
344 $ cat *
344 $ cat *
345 aaaa
345 aaaa
346 dddd
346 dddd
347 xxxx
347 xxxx
348 $ hg strip -qf 4
348 $ hg strip -qf 4
349
349
350 Test passing multiple revisions to --source
350 Test passing multiple revisions to --source
351 $ hg fix -s '2 + .'
351 $ hg fix -s '2 + .'
352 $ hg log --graph --template '{rev} {desc}\n'
352 $ hg log --graph --template '{rev} {desc}\n'
353 @ 5 change D
353 @ 5 change D
354 |
354 |
355 | o 4 change C
355 | o 4 change C
356 | |
356 | |
357 | o 1 change B
357 | o 1 change B
358 |/
358 |/
359 o 0 change A
359 o 0 change A
360
360
361 $ hg cat -r 4 b c
361 $ hg cat -r 4 b c
362 bbbb
362 bbbb
363 CCCC
363 CCCC
364 $ cat *
364 $ cat *
365 aaaa
365 aaaa
366 DDDD
366 DDDD
367 XXXX
367 XXXX
368
368
369 $ cd ..
369 $ cd ..
370
370
371 $ hg init exclude-obsolete
371 $ hg init exclude-obsolete
372 $ cd exclude-obsolete
372 $ cd exclude-obsolete
373 $ hg debugdrawdag <<'EOS'
373 $ hg debugdrawdag <<'EOS'
374 > E C # prune: C
374 > E C # prune: C
375 > | |
375 > | |
376 > D B # prune: B, D
376 > D B # prune: B, D
377 > |/
377 > |/
378 > A
378 > A
379 > EOS
379 > EOS
380 1 new orphan changesets
380 1 new orphan changesets
381 $ hg log --graph --template '{rev} {desc}\n'
381 $ hg log --graph --template '{rev} {desc}\n'
382 * 4 E
382 * 4 E
383 |
383 |
384 | x 3 C
384 | x 3 C
385 | |
385 | |
386 x | 2 D
386 x | 2 D
387 | |
387 | |
388 | x 1 B
388 | x 1 B
389 |/
389 |/
390 o 0 A
390 o 0 A
391
391
392 $ hg fix -s A
392 $ hg fix -s A
393 abort: fixing obsolete revision could cause divergence
394 [255]
395 $ hg fix -s B
393 $ hg fix -s B
396 abort: fixing obsolete revision could cause divergence
394 abort: no changesets specified
395 (use --source or --working-dir)
397 [255]
396 [255]
398 $ hg fix -s D
397 $ hg fix -s D
399 abort: fixing obsolete revision could cause divergence
400 [255]
401 $ hg fix -s E
398 $ hg fix -s E
402 $ cd ..
399 $ cd ..
403
400
404 #endif
401 #endif
405
402
406 The --all flag should fix anything that wouldn't cause a problem if you fixed
403 The --all flag should fix anything that wouldn't cause a problem if you fixed
407 it, including the working copy. Obsolete revisions are not fixed because that
404 it, including the working copy. Obsolete revisions are not fixed because that
408 could cause divergence. Public revisions would cause an abort because they are
405 could cause divergence. Public revisions would cause an abort because they are
409 immutable. We can fix orphans because their successors are still just orphans
406 immutable. We can fix orphans because their successors are still just orphans
410 of the original obsolete parent. When obsolesence is off, we're just fixing and
407 of the original obsolete parent. When obsolesence is off, we're just fixing and
411 replacing anything that isn't public.
408 replacing anything that isn't public.
412
409
413 $ hg init fixall
410 $ hg init fixall
414 $ cd fixall
411 $ cd fixall
415 $ hg fix --all --working-dir
412 $ hg fix --all --working-dir
416 abort: cannot specify both --working-dir and --all
413 abort: cannot specify both --working-dir and --all
417 [255]
414 [255]
418
415
419 #if obsstore-on
416 #if obsstore-on
420 $ printf "one\n" > foo.whole
417 $ printf "one\n" > foo.whole
421 $ hg commit -Aqm "first"
418 $ hg commit -Aqm "first"
422 $ hg phase --public
419 $ hg phase --public
423 $ hg tag --local root
420 $ hg tag --local root
424 $ printf "two\n" > foo.whole
421 $ printf "two\n" > foo.whole
425 $ hg commit -m "second"
422 $ hg commit -m "second"
426 $ printf "three\n" > foo.whole
423 $ printf "three\n" > foo.whole
427 $ hg commit -m "third" --secret
424 $ hg commit -m "third" --secret
428 $ hg tag --local secret
425 $ hg tag --local secret
429 $ hg checkout root
426 $ hg checkout root
430 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
427 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
431 $ printf "four\n" > foo.whole
428 $ printf "four\n" > foo.whole
432 $ hg commit -m "fourth"
429 $ hg commit -m "fourth"
433 created new head
430 created new head
434 $ printf "five\n" > foo.whole
431 $ printf "five\n" > foo.whole
435 $ hg commit -m "fifth"
432 $ hg commit -m "fifth"
436 $ hg tag --local replaced
433 $ hg tag --local replaced
437 $ printf "six\n" > foo.whole
434 $ printf "six\n" > foo.whole
438 $ hg commit -m "sixth"
435 $ hg commit -m "sixth"
439 $ hg checkout replaced
436 $ hg checkout replaced
440 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
437 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
441 $ printf "seven\n" > foo.whole
438 $ printf "seven\n" > foo.whole
442 $ hg commit --amend
439 $ hg commit --amend
443 1 new orphan changesets
440 1 new orphan changesets
444 $ hg checkout secret
441 $ hg checkout secret
445 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
442 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
446 $ printf "uncommitted\n" > foo.whole
443 $ printf "uncommitted\n" > foo.whole
447
444
448 $ hg log --graph --template '{rev} {desc} {phase}\n'
445 $ hg log --graph --template '{rev} {desc} {phase}\n'
449 o 6 fifth draft
446 o 6 fifth draft
450 |
447 |
451 | * 5 sixth draft
448 | * 5 sixth draft
452 | |
449 | |
453 | x 4 fifth draft
450 | x 4 fifth draft
454 |/
451 |/
455 o 3 fourth draft
452 o 3 fourth draft
456 |
453 |
457 | @ 2 third secret
454 | @ 2 third secret
458 | |
455 | |
459 | o 1 second draft
456 | o 1 second draft
460 |/
457 |/
461 o 0 first public
458 o 0 first public
462
459
463
460
464 $ hg fix --all
461 $ hg fix --all
465
462
466 $ hg log --graph --template '{rev} {desc}\n' -r 'sort(all(), topo)' --hidden
463 $ hg log --graph --template '{rev} {desc}\n' -r 'sort(all(), topo)' --hidden
467 o 11 fifth
464 o 11 fifth
468 |
465 |
469 o 9 fourth
466 o 9 fourth
470 |
467 |
471 | @ 8 third
468 | @ 8 third
472 | |
469 | |
473 | o 7 second
470 | o 7 second
474 |/
471 |/
475 | * 10 sixth
472 | * 10 sixth
476 | |
473 | |
477 | | x 5 sixth
474 | | x 5 sixth
478 | |/
475 | |/
479 | x 4 fifth
476 | x 4 fifth
480 | |
477 | |
481 | | x 6 fifth
478 | | x 6 fifth
482 | |/
479 | |/
483 | x 3 fourth
480 | x 3 fourth
484 |/
481 |/
485 | x 2 third
482 | x 2 third
486 | |
483 | |
487 | x 1 second
484 | x 1 second
488 |/
485 |/
489 o 0 first
486 o 0 first
490
487
491
488
492 $ hg cat -r 7 foo.whole
489 $ hg cat -r 7 foo.whole
493 TWO
490 TWO
494 $ hg cat -r 8 foo.whole
491 $ hg cat -r 8 foo.whole
495 THREE
492 THREE
496 $ hg cat -r 9 foo.whole
493 $ hg cat -r 9 foo.whole
497 FOUR
494 FOUR
498 $ hg cat -r 10 foo.whole
495 $ hg cat -r 10 foo.whole
499 SIX
496 SIX
500 $ hg cat -r 11 foo.whole
497 $ hg cat -r 11 foo.whole
501 SEVEN
498 SEVEN
502 $ cat foo.whole
499 $ cat foo.whole
503 UNCOMMITTED
500 UNCOMMITTED
504 #else
501 #else
505 $ printf "one\n" > foo.whole
502 $ printf "one\n" > foo.whole
506 $ hg commit -Aqm "first"
503 $ hg commit -Aqm "first"
507 $ hg phase --public
504 $ hg phase --public
508 $ hg tag --local root
505 $ hg tag --local root
509 $ printf "two\n" > foo.whole
506 $ printf "two\n" > foo.whole
510 $ hg commit -m "second"
507 $ hg commit -m "second"
511 $ printf "three\n" > foo.whole
508 $ printf "three\n" > foo.whole
512 $ hg commit -m "third" --secret
509 $ hg commit -m "third" --secret
513 $ hg tag --local secret
510 $ hg tag --local secret
514 $ hg checkout root
511 $ hg checkout root
515 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
512 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
516 $ printf "four\n" > foo.whole
513 $ printf "four\n" > foo.whole
517 $ hg commit -m "fourth"
514 $ hg commit -m "fourth"
518 created new head
515 created new head
519 $ printf "uncommitted\n" > foo.whole
516 $ printf "uncommitted\n" > foo.whole
520
517
521 $ hg log --graph --template '{rev} {desc} {phase}\n'
518 $ hg log --graph --template '{rev} {desc} {phase}\n'
522 @ 3 fourth draft
519 @ 3 fourth draft
523 |
520 |
524 | o 2 third secret
521 | o 2 third secret
525 | |
522 | |
526 | o 1 second draft
523 | o 1 second draft
527 |/
524 |/
528 o 0 first public
525 o 0 first public
529
526
530
527
531 $ hg fix --all
528 $ hg fix --all
532 saved backup bundle to * (glob)
529 saved backup bundle to * (glob)
533
530
534 $ hg log --graph --template '{rev} {desc} {phase}\n'
531 $ hg log --graph --template '{rev} {desc} {phase}\n'
535 @ 3 fourth draft
532 @ 3 fourth draft
536 |
533 |
537 | o 2 third secret
534 | o 2 third secret
538 | |
535 | |
539 | o 1 second draft
536 | o 1 second draft
540 |/
537 |/
541 o 0 first public
538 o 0 first public
542
539
543 $ hg cat -r 0 foo.whole
540 $ hg cat -r 0 foo.whole
544 one
541 one
545 $ hg cat -r 1 foo.whole
542 $ hg cat -r 1 foo.whole
546 TWO
543 TWO
547 $ hg cat -r 2 foo.whole
544 $ hg cat -r 2 foo.whole
548 THREE
545 THREE
549 $ hg cat -r 3 foo.whole
546 $ hg cat -r 3 foo.whole
550 FOUR
547 FOUR
551 $ cat foo.whole
548 $ cat foo.whole
552 UNCOMMITTED
549 UNCOMMITTED
553 #endif
550 #endif
554
551
555 $ cd ..
552 $ cd ..
556
553
General Comments 0
You need to be logged in to leave comments. Login now