##// END OF EJS Templates
extdiff: refactor logic which does diff of patches...
Pulkit Goyal -
r45686:48c38018 default
parent child Browse files
Show More
@@ -1,719 +1,736 b''
1 # extdiff.py - external diff program support for mercurial
1 # extdiff.py - external diff program support for mercurial
2 #
2 #
3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
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
7
8 '''command to allow external programs to compare revisions
8 '''command to allow external programs to compare revisions
9
9
10 The extdiff Mercurial extension allows you to use external programs
10 The extdiff Mercurial extension allows you to use external programs
11 to compare revisions, or revision with working directory. The external
11 to compare revisions, or revision with working directory. The external
12 diff programs are called with a configurable set of options and two
12 diff programs are called with a configurable set of options and two
13 non-option arguments: paths to directories containing snapshots of
13 non-option arguments: paths to directories containing snapshots of
14 files to compare.
14 files to compare.
15
15
16 If there is more than one file being compared and the "child" revision
16 If there is more than one file being compared and the "child" revision
17 is the working directory, any modifications made in the external diff
17 is the working directory, any modifications made in the external diff
18 program will be copied back to the working directory from the temporary
18 program will be copied back to the working directory from the temporary
19 directory.
19 directory.
20
20
21 The extdiff extension also allows you to configure new diff commands, so
21 The extdiff extension also allows you to configure new diff commands, so
22 you do not need to type :hg:`extdiff -p kdiff3` always. ::
22 you do not need to type :hg:`extdiff -p kdiff3` always. ::
23
23
24 [extdiff]
24 [extdiff]
25 # add new command that runs GNU diff(1) in 'context diff' mode
25 # add new command that runs GNU diff(1) in 'context diff' mode
26 cdiff = gdiff -Nprc5
26 cdiff = gdiff -Nprc5
27 ## or the old way:
27 ## or the old way:
28 #cmd.cdiff = gdiff
28 #cmd.cdiff = gdiff
29 #opts.cdiff = -Nprc5
29 #opts.cdiff = -Nprc5
30
30
31 # add new command called meld, runs meld (no need to name twice). If
31 # add new command called meld, runs meld (no need to name twice). If
32 # the meld executable is not available, the meld tool in [merge-tools]
32 # the meld executable is not available, the meld tool in [merge-tools]
33 # will be used, if available
33 # will be used, if available
34 meld =
34 meld =
35
35
36 # add new command called vimdiff, runs gvimdiff with DirDiff plugin
36 # add new command called vimdiff, runs gvimdiff with DirDiff plugin
37 # (see http://www.vim.org/scripts/script.php?script_id=102) Non
37 # (see http://www.vim.org/scripts/script.php?script_id=102) Non
38 # English user, be sure to put "let g:DirDiffDynamicDiffText = 1" in
38 # English user, be sure to put "let g:DirDiffDynamicDiffText = 1" in
39 # your .vimrc
39 # your .vimrc
40 vimdiff = gvim -f "+next" \\
40 vimdiff = gvim -f "+next" \\
41 "+execute 'DirDiff' fnameescape(argv(0)) fnameescape(argv(1))"
41 "+execute 'DirDiff' fnameescape(argv(0)) fnameescape(argv(1))"
42
42
43 Tool arguments can include variables that are expanded at runtime::
43 Tool arguments can include variables that are expanded at runtime::
44
44
45 $parent1, $plabel1 - filename, descriptive label of first parent
45 $parent1, $plabel1 - filename, descriptive label of first parent
46 $child, $clabel - filename, descriptive label of child revision
46 $child, $clabel - filename, descriptive label of child revision
47 $parent2, $plabel2 - filename, descriptive label of second parent
47 $parent2, $plabel2 - filename, descriptive label of second parent
48 $root - repository root
48 $root - repository root
49 $parent is an alias for $parent1.
49 $parent is an alias for $parent1.
50
50
51 The extdiff extension will look in your [diff-tools] and [merge-tools]
51 The extdiff extension will look in your [diff-tools] and [merge-tools]
52 sections for diff tool arguments, when none are specified in [extdiff].
52 sections for diff tool arguments, when none are specified in [extdiff].
53
53
54 ::
54 ::
55
55
56 [extdiff]
56 [extdiff]
57 kdiff3 =
57 kdiff3 =
58
58
59 [diff-tools]
59 [diff-tools]
60 kdiff3.diffargs=--L1 '$plabel1' --L2 '$clabel' $parent $child
60 kdiff3.diffargs=--L1 '$plabel1' --L2 '$clabel' $parent $child
61
61
62 If a program has a graphical interface, it might be interesting to tell
62 If a program has a graphical interface, it might be interesting to tell
63 Mercurial about it. It will prevent the program from being mistakenly
63 Mercurial about it. It will prevent the program from being mistakenly
64 used in a terminal-only environment (such as an SSH terminal session),
64 used in a terminal-only environment (such as an SSH terminal session),
65 and will make :hg:`extdiff --per-file` open multiple file diffs at once
65 and will make :hg:`extdiff --per-file` open multiple file diffs at once
66 instead of one by one (if you still want to open file diffs one by one,
66 instead of one by one (if you still want to open file diffs one by one,
67 you can use the --confirm option).
67 you can use the --confirm option).
68
68
69 Declaring that a tool has a graphical interface can be done with the
69 Declaring that a tool has a graphical interface can be done with the
70 ``gui`` flag next to where ``diffargs`` are specified:
70 ``gui`` flag next to where ``diffargs`` are specified:
71
71
72 ::
72 ::
73
73
74 [diff-tools]
74 [diff-tools]
75 kdiff3.diffargs=--L1 '$plabel1' --L2 '$clabel' $parent $child
75 kdiff3.diffargs=--L1 '$plabel1' --L2 '$clabel' $parent $child
76 kdiff3.gui = true
76 kdiff3.gui = true
77
77
78 You can use -I/-X and list of file or directory names like normal
78 You can use -I/-X and list of file or directory names like normal
79 :hg:`diff` command. The extdiff extension makes snapshots of only
79 :hg:`diff` command. The extdiff extension makes snapshots of only
80 needed files, so running the external diff program will actually be
80 needed files, so running the external diff program will actually be
81 pretty fast (at least faster than having to compare the entire tree).
81 pretty fast (at least faster than having to compare the entire tree).
82 '''
82 '''
83
83
84 from __future__ import absolute_import
84 from __future__ import absolute_import
85
85
86 import os
86 import os
87 import re
87 import re
88 import shutil
88 import shutil
89 import stat
89 import stat
90 import subprocess
90 import subprocess
91
91
92 from mercurial.i18n import _
92 from mercurial.i18n import _
93 from mercurial.node import (
93 from mercurial.node import (
94 nullid,
94 nullid,
95 short,
95 short,
96 )
96 )
97 from mercurial import (
97 from mercurial import (
98 archival,
98 archival,
99 cmdutil,
99 cmdutil,
100 encoding,
100 encoding,
101 error,
101 error,
102 filemerge,
102 filemerge,
103 formatter,
103 formatter,
104 pycompat,
104 pycompat,
105 registrar,
105 registrar,
106 scmutil,
106 scmutil,
107 util,
107 util,
108 )
108 )
109 from mercurial.utils import (
109 from mercurial.utils import (
110 procutil,
110 procutil,
111 stringutil,
111 stringutil,
112 )
112 )
113
113
114 cmdtable = {}
114 cmdtable = {}
115 command = registrar.command(cmdtable)
115 command = registrar.command(cmdtable)
116
116
117 configtable = {}
117 configtable = {}
118 configitem = registrar.configitem(configtable)
118 configitem = registrar.configitem(configtable)
119
119
120 configitem(
120 configitem(
121 b'extdiff', br'opts\..*', default=b'', generic=True,
121 b'extdiff', br'opts\..*', default=b'', generic=True,
122 )
122 )
123
123
124 configitem(
124 configitem(
125 b'extdiff', br'gui\..*', generic=True,
125 b'extdiff', br'gui\..*', generic=True,
126 )
126 )
127
127
128 configitem(
128 configitem(
129 b'diff-tools', br'.*\.diffargs$', default=None, generic=True,
129 b'diff-tools', br'.*\.diffargs$', default=None, generic=True,
130 )
130 )
131
131
132 configitem(
132 configitem(
133 b'diff-tools', br'.*\.gui$', generic=True,
133 b'diff-tools', br'.*\.gui$', generic=True,
134 )
134 )
135
135
136 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
136 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
137 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
137 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
138 # be specifying the version(s) of Mercurial they are tested with, or
138 # be specifying the version(s) of Mercurial they are tested with, or
139 # leave the attribute unspecified.
139 # leave the attribute unspecified.
140 testedwith = b'ships-with-hg-core'
140 testedwith = b'ships-with-hg-core'
141
141
142
142
143 def snapshot(ui, repo, files, node, tmproot, listsubrepos):
143 def snapshot(ui, repo, files, node, tmproot, listsubrepos):
144 '''snapshot files as of some revision
144 '''snapshot files as of some revision
145 if not using snapshot, -I/-X does not work and recursive diff
145 if not using snapshot, -I/-X does not work and recursive diff
146 in tools like kdiff3 and meld displays too many files.'''
146 in tools like kdiff3 and meld displays too many files.'''
147 dirname = os.path.basename(repo.root)
147 dirname = os.path.basename(repo.root)
148 if dirname == b"":
148 if dirname == b"":
149 dirname = b"root"
149 dirname = b"root"
150 if node is not None:
150 if node is not None:
151 dirname = b'%s.%s' % (dirname, short(node))
151 dirname = b'%s.%s' % (dirname, short(node))
152 base = os.path.join(tmproot, dirname)
152 base = os.path.join(tmproot, dirname)
153 os.mkdir(base)
153 os.mkdir(base)
154 fnsandstat = []
154 fnsandstat = []
155
155
156 if node is not None:
156 if node is not None:
157 ui.note(
157 ui.note(
158 _(b'making snapshot of %d files from rev %s\n')
158 _(b'making snapshot of %d files from rev %s\n')
159 % (len(files), short(node))
159 % (len(files), short(node))
160 )
160 )
161 else:
161 else:
162 ui.note(
162 ui.note(
163 _(b'making snapshot of %d files from working directory\n')
163 _(b'making snapshot of %d files from working directory\n')
164 % (len(files))
164 % (len(files))
165 )
165 )
166
166
167 if files:
167 if files:
168 repo.ui.setconfig(b"ui", b"archivemeta", False)
168 repo.ui.setconfig(b"ui", b"archivemeta", False)
169
169
170 archival.archive(
170 archival.archive(
171 repo,
171 repo,
172 base,
172 base,
173 node,
173 node,
174 b'files',
174 b'files',
175 match=scmutil.matchfiles(repo, files),
175 match=scmutil.matchfiles(repo, files),
176 subrepos=listsubrepos,
176 subrepos=listsubrepos,
177 )
177 )
178
178
179 for fn in sorted(files):
179 for fn in sorted(files):
180 wfn = util.pconvert(fn)
180 wfn = util.pconvert(fn)
181 ui.note(b' %s\n' % wfn)
181 ui.note(b' %s\n' % wfn)
182
182
183 if node is None:
183 if node is None:
184 dest = os.path.join(base, wfn)
184 dest = os.path.join(base, wfn)
185
185
186 fnsandstat.append((dest, repo.wjoin(fn), os.lstat(dest)))
186 fnsandstat.append((dest, repo.wjoin(fn), os.lstat(dest)))
187 return dirname, fnsandstat
187 return dirname, fnsandstat
188
188
189
189
190 def formatcmdline(
190 def formatcmdline(
191 cmdline,
191 cmdline,
192 repo_root,
192 repo_root,
193 do3way,
193 do3way,
194 parent1,
194 parent1,
195 plabel1,
195 plabel1,
196 parent2,
196 parent2,
197 plabel2,
197 plabel2,
198 child,
198 child,
199 clabel,
199 clabel,
200 ):
200 ):
201 # Function to quote file/dir names in the argument string.
201 # Function to quote file/dir names in the argument string.
202 # When not operating in 3-way mode, an empty string is
202 # When not operating in 3-way mode, an empty string is
203 # returned for parent2
203 # returned for parent2
204 replace = {
204 replace = {
205 b'parent': parent1,
205 b'parent': parent1,
206 b'parent1': parent1,
206 b'parent1': parent1,
207 b'parent2': parent2,
207 b'parent2': parent2,
208 b'plabel1': plabel1,
208 b'plabel1': plabel1,
209 b'plabel2': plabel2,
209 b'plabel2': plabel2,
210 b'child': child,
210 b'child': child,
211 b'clabel': clabel,
211 b'clabel': clabel,
212 b'root': repo_root,
212 b'root': repo_root,
213 }
213 }
214
214
215 def quote(match):
215 def quote(match):
216 pre = match.group(2)
216 pre = match.group(2)
217 key = match.group(3)
217 key = match.group(3)
218 if not do3way and key == b'parent2':
218 if not do3way and key == b'parent2':
219 return pre
219 return pre
220 return pre + procutil.shellquote(replace[key])
220 return pre + procutil.shellquote(replace[key])
221
221
222 # Match parent2 first, so 'parent1?' will match both parent1 and parent
222 # Match parent2 first, so 'parent1?' will match both parent1 and parent
223 regex = (
223 regex = (
224 br'''(['"]?)([^\s'"$]*)'''
224 br'''(['"]?)([^\s'"$]*)'''
225 br'\$(parent2|parent1?|child|plabel1|plabel2|clabel|root)\1'
225 br'\$(parent2|parent1?|child|plabel1|plabel2|clabel|root)\1'
226 )
226 )
227 if not do3way and not re.search(regex, cmdline):
227 if not do3way and not re.search(regex, cmdline):
228 cmdline += b' $parent1 $child'
228 cmdline += b' $parent1 $child'
229 return re.sub(regex, quote, cmdline)
229 return re.sub(regex, quote, cmdline)
230
230
231
231
232 def _systembackground(cmd, environ=None, cwd=None):
232 def _systembackground(cmd, environ=None, cwd=None):
233 ''' like 'procutil.system', but returns the Popen object directly
233 ''' like 'procutil.system', but returns the Popen object directly
234 so we don't have to wait on it.
234 so we don't have to wait on it.
235 '''
235 '''
236 env = procutil.shellenviron(environ)
236 env = procutil.shellenviron(environ)
237 proc = subprocess.Popen(
237 proc = subprocess.Popen(
238 procutil.tonativestr(cmd),
238 procutil.tonativestr(cmd),
239 shell=True,
239 shell=True,
240 close_fds=procutil.closefds,
240 close_fds=procutil.closefds,
241 env=procutil.tonativeenv(env),
241 env=procutil.tonativeenv(env),
242 cwd=pycompat.rapply(procutil.tonativestr, cwd),
242 cwd=pycompat.rapply(procutil.tonativestr, cwd),
243 )
243 )
244 return proc
244 return proc
245
245
246
246
247 def _runperfilediff(
247 def _runperfilediff(
248 cmdline,
248 cmdline,
249 repo_root,
249 repo_root,
250 ui,
250 ui,
251 guitool,
251 guitool,
252 do3way,
252 do3way,
253 confirm,
253 confirm,
254 commonfiles,
254 commonfiles,
255 tmproot,
255 tmproot,
256 dir1a,
256 dir1a,
257 dir1b,
257 dir1b,
258 dir2root,
258 dir2root,
259 dir2,
259 dir2,
260 rev1a,
260 rev1a,
261 rev1b,
261 rev1b,
262 rev2,
262 rev2,
263 ):
263 ):
264 # Note that we need to sort the list of files because it was
264 # Note that we need to sort the list of files because it was
265 # built in an "unstable" way and it's annoying to get files in a
265 # built in an "unstable" way and it's annoying to get files in a
266 # random order, especially when "confirm" mode is enabled.
266 # random order, especially when "confirm" mode is enabled.
267 waitprocs = []
267 waitprocs = []
268 totalfiles = len(commonfiles)
268 totalfiles = len(commonfiles)
269 for idx, commonfile in enumerate(sorted(commonfiles)):
269 for idx, commonfile in enumerate(sorted(commonfiles)):
270 path1a = os.path.join(tmproot, dir1a, commonfile)
270 path1a = os.path.join(tmproot, dir1a, commonfile)
271 label1a = commonfile + rev1a
271 label1a = commonfile + rev1a
272 if not os.path.isfile(path1a):
272 if not os.path.isfile(path1a):
273 path1a = pycompat.osdevnull
273 path1a = pycompat.osdevnull
274
274
275 path1b = b''
275 path1b = b''
276 label1b = b''
276 label1b = b''
277 if do3way:
277 if do3way:
278 path1b = os.path.join(tmproot, dir1b, commonfile)
278 path1b = os.path.join(tmproot, dir1b, commonfile)
279 label1b = commonfile + rev1b
279 label1b = commonfile + rev1b
280 if not os.path.isfile(path1b):
280 if not os.path.isfile(path1b):
281 path1b = pycompat.osdevnull
281 path1b = pycompat.osdevnull
282
282
283 path2 = os.path.join(dir2root, dir2, commonfile)
283 path2 = os.path.join(dir2root, dir2, commonfile)
284 label2 = commonfile + rev2
284 label2 = commonfile + rev2
285
285
286 if confirm:
286 if confirm:
287 # Prompt before showing this diff
287 # Prompt before showing this diff
288 difffiles = _(b'diff %s (%d of %d)') % (
288 difffiles = _(b'diff %s (%d of %d)') % (
289 commonfile,
289 commonfile,
290 idx + 1,
290 idx + 1,
291 totalfiles,
291 totalfiles,
292 )
292 )
293 responses = _(
293 responses = _(
294 b'[Yns?]'
294 b'[Yns?]'
295 b'$$ &Yes, show diff'
295 b'$$ &Yes, show diff'
296 b'$$ &No, skip this diff'
296 b'$$ &No, skip this diff'
297 b'$$ &Skip remaining diffs'
297 b'$$ &Skip remaining diffs'
298 b'$$ &? (display help)'
298 b'$$ &? (display help)'
299 )
299 )
300 r = ui.promptchoice(b'%s %s' % (difffiles, responses))
300 r = ui.promptchoice(b'%s %s' % (difffiles, responses))
301 if r == 3: # ?
301 if r == 3: # ?
302 while r == 3:
302 while r == 3:
303 for c, t in ui.extractchoices(responses)[1]:
303 for c, t in ui.extractchoices(responses)[1]:
304 ui.write(b'%s - %s\n' % (c, encoding.lower(t)))
304 ui.write(b'%s - %s\n' % (c, encoding.lower(t)))
305 r = ui.promptchoice(b'%s %s' % (difffiles, responses))
305 r = ui.promptchoice(b'%s %s' % (difffiles, responses))
306 if r == 0: # yes
306 if r == 0: # yes
307 pass
307 pass
308 elif r == 1: # no
308 elif r == 1: # no
309 continue
309 continue
310 elif r == 2: # skip
310 elif r == 2: # skip
311 break
311 break
312
312
313 curcmdline = formatcmdline(
313 curcmdline = formatcmdline(
314 cmdline,
314 cmdline,
315 repo_root,
315 repo_root,
316 do3way=do3way,
316 do3way=do3way,
317 parent1=path1a,
317 parent1=path1a,
318 plabel1=label1a,
318 plabel1=label1a,
319 parent2=path1b,
319 parent2=path1b,
320 plabel2=label1b,
320 plabel2=label1b,
321 child=path2,
321 child=path2,
322 clabel=label2,
322 clabel=label2,
323 )
323 )
324
324
325 if confirm or not guitool:
325 if confirm or not guitool:
326 # Run the comparison program and wait for it to exit
326 # Run the comparison program and wait for it to exit
327 # before we show the next file.
327 # before we show the next file.
328 # This is because either we need to wait for confirmation
328 # This is because either we need to wait for confirmation
329 # from the user between each invocation, or because, as far
329 # from the user between each invocation, or because, as far
330 # as we know, the tool doesn't have a GUI, in which case
330 # as we know, the tool doesn't have a GUI, in which case
331 # we can't run multiple CLI programs at the same time.
331 # we can't run multiple CLI programs at the same time.
332 ui.debug(
332 ui.debug(
333 b'running %r in %s\n' % (pycompat.bytestr(curcmdline), tmproot)
333 b'running %r in %s\n' % (pycompat.bytestr(curcmdline), tmproot)
334 )
334 )
335 ui.system(curcmdline, cwd=tmproot, blockedtag=b'extdiff')
335 ui.system(curcmdline, cwd=tmproot, blockedtag=b'extdiff')
336 else:
336 else:
337 # Run the comparison program but don't wait, as we're
337 # Run the comparison program but don't wait, as we're
338 # going to rapid-fire each file diff and then wait on
338 # going to rapid-fire each file diff and then wait on
339 # the whole group.
339 # the whole group.
340 ui.debug(
340 ui.debug(
341 b'running %r in %s (backgrounded)\n'
341 b'running %r in %s (backgrounded)\n'
342 % (pycompat.bytestr(curcmdline), tmproot)
342 % (pycompat.bytestr(curcmdline), tmproot)
343 )
343 )
344 proc = _systembackground(curcmdline, cwd=tmproot)
344 proc = _systembackground(curcmdline, cwd=tmproot)
345 waitprocs.append(proc)
345 waitprocs.append(proc)
346
346
347 if waitprocs:
347 if waitprocs:
348 with ui.timeblockedsection(b'extdiff'):
348 with ui.timeblockedsection(b'extdiff'):
349 for proc in waitprocs:
349 for proc in waitprocs:
350 proc.wait()
350 proc.wait()
351
351
352
352
353 def diffpatch(ui, repo, node1a, node2, tmproot, matcher, cmdline, do3way):
354 template = b'hg-%h.patch'
355 with formatter.nullformatter(ui, b'extdiff', {}) as fm:
356 cmdutil.export(
357 repo,
358 [repo[node1a].rev(), repo[node2].rev()],
359 fm,
360 fntemplate=repo.vfs.reljoin(tmproot, template),
361 match=matcher,
362 )
363 label1a = cmdutil.makefilename(repo[node1a], template)
364 label2 = cmdutil.makefilename(repo[node2], template)
365 dir1a = repo.vfs.reljoin(tmproot, label1a)
366 dir2 = repo.vfs.reljoin(tmproot, label2)
367 dir1b = None
368 label1b = None
369 cmdline = formatcmdline(
370 cmdline,
371 repo.root,
372 do3way=do3way,
373 parent1=dir1a,
374 plabel1=label1a,
375 parent2=dir1b,
376 plabel2=label1b,
377 child=dir2,
378 clabel=label2,
379 )
380 ui.debug(b'running %r in %s\n' % (pycompat.bytestr(cmdline), tmproot))
381 ui.system(cmdline, cwd=tmproot, blockedtag=b'extdiff')
382 return 1
383
384
353 def dodiff(ui, repo, cmdline, pats, opts, guitool=False):
385 def dodiff(ui, repo, cmdline, pats, opts, guitool=False):
354 '''Do the actual diff:
386 '''Do the actual diff:
355
387
356 - copy to a temp structure if diffing 2 internal revisions
388 - copy to a temp structure if diffing 2 internal revisions
357 - copy to a temp structure if diffing working revision with
389 - copy to a temp structure if diffing working revision with
358 another one and more than 1 file is changed
390 another one and more than 1 file is changed
359 - just invoke the diff for a single file in the working dir
391 - just invoke the diff for a single file in the working dir
360 '''
392 '''
361
393
362 cmdutil.check_at_most_one_arg(opts, b'rev', b'change')
394 cmdutil.check_at_most_one_arg(opts, b'rev', b'change')
363 revs = opts.get(b'rev')
395 revs = opts.get(b'rev')
364 change = opts.get(b'change')
396 change = opts.get(b'change')
365 do3way = b'$parent2' in cmdline
397 do3way = b'$parent2' in cmdline
366
398
367 if change:
399 if change:
368 ctx2 = scmutil.revsingle(repo, change, None)
400 ctx2 = scmutil.revsingle(repo, change, None)
369 ctx1a, ctx1b = ctx2.p1(), ctx2.p2()
401 ctx1a, ctx1b = ctx2.p1(), ctx2.p2()
370 else:
402 else:
371 ctx1a, ctx2 = scmutil.revpair(repo, revs)
403 ctx1a, ctx2 = scmutil.revpair(repo, revs)
372 if not revs:
404 if not revs:
373 ctx1b = repo[None].p2()
405 ctx1b = repo[None].p2()
374 else:
406 else:
375 ctx1b = repo[nullid]
407 ctx1b = repo[nullid]
376
408
377 perfile = opts.get(b'per_file')
409 perfile = opts.get(b'per_file')
378 confirm = opts.get(b'confirm')
410 confirm = opts.get(b'confirm')
379
411
380 node1a = ctx1a.node()
412 node1a = ctx1a.node()
381 node1b = ctx1b.node()
413 node1b = ctx1b.node()
382 node2 = ctx2.node()
414 node2 = ctx2.node()
383
415
384 # Disable 3-way merge if there is only one parent
416 # Disable 3-way merge if there is only one parent
385 if do3way:
417 if do3way:
386 if node1b == nullid:
418 if node1b == nullid:
387 do3way = False
419 do3way = False
388
420
389 subrepos = opts.get(b'subrepos')
421 subrepos = opts.get(b'subrepos')
390
422
391 matcher = scmutil.match(repo[node2], pats, opts)
423 matcher = scmutil.match(repo[node2], pats, opts)
392
424
393 if opts.get(b'patch'):
425 if opts.get(b'patch'):
394 if subrepos:
426 if subrepos:
395 raise error.Abort(_(b'--patch cannot be used with --subrepos'))
427 raise error.Abort(_(b'--patch cannot be used with --subrepos'))
396 if perfile:
428 if perfile:
397 raise error.Abort(_(b'--patch cannot be used with --per-file'))
429 raise error.Abort(_(b'--patch cannot be used with --per-file'))
398 if node2 is None:
430 if node2 is None:
399 raise error.Abort(_(b'--patch requires two revisions'))
431 raise error.Abort(_(b'--patch requires two revisions'))
400 else:
432 else:
401 st = repo.status(node1a, node2, matcher, listsubrepos=subrepos)
433 st = repo.status(node1a, node2, matcher, listsubrepos=subrepos)
402 mod_a, add_a, rem_a = set(st.modified), set(st.added), set(st.removed)
434 mod_a, add_a, rem_a = set(st.modified), set(st.added), set(st.removed)
403 if do3way:
435 if do3way:
404 stb = repo.status(node1b, node2, matcher, listsubrepos=subrepos)
436 stb = repo.status(node1b, node2, matcher, listsubrepos=subrepos)
405 mod_b, add_b, rem_b = (
437 mod_b, add_b, rem_b = (
406 set(stb.modified),
438 set(stb.modified),
407 set(stb.added),
439 set(stb.added),
408 set(stb.removed),
440 set(stb.removed),
409 )
441 )
410 else:
442 else:
411 mod_b, add_b, rem_b = set(), set(), set()
443 mod_b, add_b, rem_b = set(), set(), set()
412 modadd = mod_a | add_a | mod_b | add_b
444 modadd = mod_a | add_a | mod_b | add_b
413 common = modadd | rem_a | rem_b
445 common = modadd | rem_a | rem_b
414 if not common:
446 if not common:
415 return 0
447 return 0
416
448
417 tmproot = pycompat.mkdtemp(prefix=b'extdiff.')
449 tmproot = pycompat.mkdtemp(prefix=b'extdiff.')
418 try:
450 try:
419 if not opts.get(b'patch'):
451 if opts.get(b'patch'):
452 return diffpatch(
453 ui, repo, node1a, node2, tmproot, matcher, cmdline, do3way
454 )
455
420 # Always make a copy of node1a (and node1b, if applicable)
456 # Always make a copy of node1a (and node1b, if applicable)
421 dir1a_files = mod_a | rem_a | ((mod_b | add_b) - add_a)
457 dir1a_files = mod_a | rem_a | ((mod_b | add_b) - add_a)
422 dir1a = snapshot(ui, repo, dir1a_files, node1a, tmproot, subrepos)[
458 dir1a = snapshot(ui, repo, dir1a_files, node1a, tmproot, subrepos)[0]
423 0
424 ]
425 rev1a = b'@%d' % repo[node1a].rev()
459 rev1a = b'@%d' % repo[node1a].rev()
426 if do3way:
460 if do3way:
427 dir1b_files = mod_b | rem_b | ((mod_a | add_a) - add_b)
461 dir1b_files = mod_b | rem_b | ((mod_a | add_a) - add_b)
428 dir1b = snapshot(
462 dir1b = snapshot(ui, repo, dir1b_files, node1b, tmproot, subrepos)[
429 ui, repo, dir1b_files, node1b, tmproot, subrepos
463 0
430 )[0]
464 ]
431 rev1b = b'@%d' % repo[node1b].rev()
465 rev1b = b'@%d' % repo[node1b].rev()
432 else:
466 else:
433 dir1b = None
467 dir1b = None
434 rev1b = b''
468 rev1b = b''
435
469
436 fnsandstat = []
470 fnsandstat = []
437
471
438 # If node2 in not the wc or there is >1 change, copy it
472 # If node2 in not the wc or there is >1 change, copy it
439 dir2root = b''
473 dir2root = b''
440 rev2 = b''
474 rev2 = b''
441 if node2:
475 if node2:
442 dir2 = snapshot(ui, repo, modadd, node2, tmproot, subrepos)[0]
476 dir2 = snapshot(ui, repo, modadd, node2, tmproot, subrepos)[0]
443 rev2 = b'@%d' % repo[node2].rev()
477 rev2 = b'@%d' % repo[node2].rev()
444 elif len(common) > 1:
478 elif len(common) > 1:
445 # we only actually need to get the files to copy back to
479 # we only actually need to get the files to copy back to
446 # the working dir in this case (because the other cases
480 # the working dir in this case (because the other cases
447 # are: diffing 2 revisions or single file -- in which case
481 # are: diffing 2 revisions or single file -- in which case
448 # the file is already directly passed to the diff tool).
482 # the file is already directly passed to the diff tool).
449 dir2, fnsandstat = snapshot(
483 dir2, fnsandstat = snapshot(
450 ui, repo, modadd, None, tmproot, subrepos
484 ui, repo, modadd, None, tmproot, subrepos
451 )
485 )
452 else:
486 else:
453 # This lets the diff tool open the changed file directly
487 # This lets the diff tool open the changed file directly
454 dir2 = b''
488 dir2 = b''
455 dir2root = repo.root
489 dir2root = repo.root
456
490
457 label1a = rev1a
491 label1a = rev1a
458 label1b = rev1b
492 label1b = rev1b
459 label2 = rev2
493 label2 = rev2
460
494
461 # If only one change, diff the files instead of the directories
495 # If only one change, diff the files instead of the directories
462 # Handle bogus modifies correctly by checking if the files exist
496 # Handle bogus modifies correctly by checking if the files exist
463 if len(common) == 1:
497 if len(common) == 1:
464 common_file = util.localpath(common.pop())
498 common_file = util.localpath(common.pop())
465 dir1a = os.path.join(tmproot, dir1a, common_file)
499 dir1a = os.path.join(tmproot, dir1a, common_file)
466 label1a = common_file + rev1a
500 label1a = common_file + rev1a
467 if not os.path.isfile(dir1a):
501 if not os.path.isfile(dir1a):
468 dir1a = pycompat.osdevnull
502 dir1a = pycompat.osdevnull
469 if do3way:
503 if do3way:
470 dir1b = os.path.join(tmproot, dir1b, common_file)
504 dir1b = os.path.join(tmproot, dir1b, common_file)
471 label1b = common_file + rev1b
505 label1b = common_file + rev1b
472 if not os.path.isfile(dir1b):
506 if not os.path.isfile(dir1b):
473 dir1b = pycompat.osdevnull
507 dir1b = pycompat.osdevnull
474 dir2 = os.path.join(dir2root, dir2, common_file)
508 dir2 = os.path.join(dir2root, dir2, common_file)
475 label2 = common_file + rev2
509 label2 = common_file + rev2
476 else:
477 template = b'hg-%h.patch'
478 with formatter.nullformatter(ui, b'extdiff', {}) as fm:
479 cmdutil.export(
480 repo,
481 [repo[node1a].rev(), repo[node2].rev()],
482 fm,
483 fntemplate=repo.vfs.reljoin(tmproot, template),
484 match=matcher,
485 )
486 label1a = cmdutil.makefilename(repo[node1a], template)
487 label2 = cmdutil.makefilename(repo[node2], template)
488 dir1a = repo.vfs.reljoin(tmproot, label1a)
489 dir2 = repo.vfs.reljoin(tmproot, label2)
490 dir1b = None
491 label1b = None
492 fnsandstat = []
493
510
494 if not perfile:
511 if not perfile:
495 # Run the external tool on the 2 temp directories or the patches
512 # Run the external tool on the 2 temp directories or the patches
496 cmdline = formatcmdline(
513 cmdline = formatcmdline(
497 cmdline,
514 cmdline,
498 repo.root,
515 repo.root,
499 do3way=do3way,
516 do3way=do3way,
500 parent1=dir1a,
517 parent1=dir1a,
501 plabel1=label1a,
518 plabel1=label1a,
502 parent2=dir1b,
519 parent2=dir1b,
503 plabel2=label1b,
520 plabel2=label1b,
504 child=dir2,
521 child=dir2,
505 clabel=label2,
522 clabel=label2,
506 )
523 )
507 ui.debug(
524 ui.debug(
508 b'running %r in %s\n' % (pycompat.bytestr(cmdline), tmproot)
525 b'running %r in %s\n' % (pycompat.bytestr(cmdline), tmproot)
509 )
526 )
510 ui.system(cmdline, cwd=tmproot, blockedtag=b'extdiff')
527 ui.system(cmdline, cwd=tmproot, blockedtag=b'extdiff')
511 else:
528 else:
512 # Run the external tool once for each pair of files
529 # Run the external tool once for each pair of files
513 _runperfilediff(
530 _runperfilediff(
514 cmdline,
531 cmdline,
515 repo.root,
532 repo.root,
516 ui,
533 ui,
517 guitool=guitool,
534 guitool=guitool,
518 do3way=do3way,
535 do3way=do3way,
519 confirm=confirm,
536 confirm=confirm,
520 commonfiles=common,
537 commonfiles=common,
521 tmproot=tmproot,
538 tmproot=tmproot,
522 dir1a=dir1a,
539 dir1a=dir1a,
523 dir1b=dir1b,
540 dir1b=dir1b,
524 dir2root=dir2root,
541 dir2root=dir2root,
525 dir2=dir2,
542 dir2=dir2,
526 rev1a=rev1a,
543 rev1a=rev1a,
527 rev1b=rev1b,
544 rev1b=rev1b,
528 rev2=rev2,
545 rev2=rev2,
529 )
546 )
530
547
531 for copy_fn, working_fn, st in fnsandstat:
548 for copy_fn, working_fn, st in fnsandstat:
532 cpstat = os.lstat(copy_fn)
549 cpstat = os.lstat(copy_fn)
533 # Some tools copy the file and attributes, so mtime may not detect
550 # Some tools copy the file and attributes, so mtime may not detect
534 # all changes. A size check will detect more cases, but not all.
551 # all changes. A size check will detect more cases, but not all.
535 # The only certain way to detect every case is to diff all files,
552 # The only certain way to detect every case is to diff all files,
536 # which could be expensive.
553 # which could be expensive.
537 # copyfile() carries over the permission, so the mode check could
554 # copyfile() carries over the permission, so the mode check could
538 # be in an 'elif' branch, but for the case where the file has
555 # be in an 'elif' branch, but for the case where the file has
539 # changed without affecting mtime or size.
556 # changed without affecting mtime or size.
540 if (
557 if (
541 cpstat[stat.ST_MTIME] != st[stat.ST_MTIME]
558 cpstat[stat.ST_MTIME] != st[stat.ST_MTIME]
542 or cpstat.st_size != st.st_size
559 or cpstat.st_size != st.st_size
543 or (cpstat.st_mode & 0o100) != (st.st_mode & 0o100)
560 or (cpstat.st_mode & 0o100) != (st.st_mode & 0o100)
544 ):
561 ):
545 ui.debug(
562 ui.debug(
546 b'file changed while diffing. '
563 b'file changed while diffing. '
547 b'Overwriting: %s (src: %s)\n' % (working_fn, copy_fn)
564 b'Overwriting: %s (src: %s)\n' % (working_fn, copy_fn)
548 )
565 )
549 util.copyfile(copy_fn, working_fn)
566 util.copyfile(copy_fn, working_fn)
550
567
551 return 1
568 return 1
552 finally:
569 finally:
553 ui.note(_(b'cleaning up temp directory\n'))
570 ui.note(_(b'cleaning up temp directory\n'))
554 shutil.rmtree(tmproot)
571 shutil.rmtree(tmproot)
555
572
556
573
557 extdiffopts = (
574 extdiffopts = (
558 [
575 [
559 (
576 (
560 b'o',
577 b'o',
561 b'option',
578 b'option',
562 [],
579 [],
563 _(b'pass option to comparison program'),
580 _(b'pass option to comparison program'),
564 _(b'OPT'),
581 _(b'OPT'),
565 ),
582 ),
566 (b'r', b'rev', [], _(b'revision'), _(b'REV')),
583 (b'r', b'rev', [], _(b'revision'), _(b'REV')),
567 (b'c', b'change', b'', _(b'change made by revision'), _(b'REV')),
584 (b'c', b'change', b'', _(b'change made by revision'), _(b'REV')),
568 (
585 (
569 b'',
586 b'',
570 b'per-file',
587 b'per-file',
571 False,
588 False,
572 _(b'compare each file instead of revision snapshots'),
589 _(b'compare each file instead of revision snapshots'),
573 ),
590 ),
574 (
591 (
575 b'',
592 b'',
576 b'confirm',
593 b'confirm',
577 False,
594 False,
578 _(b'prompt user before each external program invocation'),
595 _(b'prompt user before each external program invocation'),
579 ),
596 ),
580 (b'', b'patch', None, _(b'compare patches for two revisions')),
597 (b'', b'patch', None, _(b'compare patches for two revisions')),
581 ]
598 ]
582 + cmdutil.walkopts
599 + cmdutil.walkopts
583 + cmdutil.subrepoopts
600 + cmdutil.subrepoopts
584 )
601 )
585
602
586
603
587 @command(
604 @command(
588 b'extdiff',
605 b'extdiff',
589 [(b'p', b'program', b'', _(b'comparison program to run'), _(b'CMD')),]
606 [(b'p', b'program', b'', _(b'comparison program to run'), _(b'CMD')),]
590 + extdiffopts,
607 + extdiffopts,
591 _(b'hg extdiff [OPT]... [FILE]...'),
608 _(b'hg extdiff [OPT]... [FILE]...'),
592 helpcategory=command.CATEGORY_FILE_CONTENTS,
609 helpcategory=command.CATEGORY_FILE_CONTENTS,
593 inferrepo=True,
610 inferrepo=True,
594 )
611 )
595 def extdiff(ui, repo, *pats, **opts):
612 def extdiff(ui, repo, *pats, **opts):
596 '''use external program to diff repository (or selected files)
613 '''use external program to diff repository (or selected files)
597
614
598 Show differences between revisions for the specified files, using
615 Show differences between revisions for the specified files, using
599 an external program. The default program used is diff, with
616 an external program. The default program used is diff, with
600 default options "-Npru".
617 default options "-Npru".
601
618
602 To select a different program, use the -p/--program option. The
619 To select a different program, use the -p/--program option. The
603 program will be passed the names of two directories to compare,
620 program will be passed the names of two directories to compare,
604 unless the --per-file option is specified (see below). To pass
621 unless the --per-file option is specified (see below). To pass
605 additional options to the program, use -o/--option. These will be
622 additional options to the program, use -o/--option. These will be
606 passed before the names of the directories or files to compare.
623 passed before the names of the directories or files to compare.
607
624
608 When two revision arguments are given, then changes are shown
625 When two revision arguments are given, then changes are shown
609 between those revisions. If only one revision is specified then
626 between those revisions. If only one revision is specified then
610 that revision is compared to the working directory, and, when no
627 that revision is compared to the working directory, and, when no
611 revisions are specified, the working directory files are compared
628 revisions are specified, the working directory files are compared
612 to its parent.
629 to its parent.
613
630
614 The --per-file option runs the external program repeatedly on each
631 The --per-file option runs the external program repeatedly on each
615 file to diff, instead of once on two directories. By default,
632 file to diff, instead of once on two directories. By default,
616 this happens one by one, where the next file diff is open in the
633 this happens one by one, where the next file diff is open in the
617 external program only once the previous external program (for the
634 external program only once the previous external program (for the
618 previous file diff) has exited. If the external program has a
635 previous file diff) has exited. If the external program has a
619 graphical interface, it can open all the file diffs at once instead
636 graphical interface, it can open all the file diffs at once instead
620 of one by one. See :hg:`help -e extdiff` for information about how
637 of one by one. See :hg:`help -e extdiff` for information about how
621 to tell Mercurial that a given program has a graphical interface.
638 to tell Mercurial that a given program has a graphical interface.
622
639
623 The --confirm option will prompt the user before each invocation of
640 The --confirm option will prompt the user before each invocation of
624 the external program. It is ignored if --per-file isn't specified.
641 the external program. It is ignored if --per-file isn't specified.
625 '''
642 '''
626 opts = pycompat.byteskwargs(opts)
643 opts = pycompat.byteskwargs(opts)
627 program = opts.get(b'program')
644 program = opts.get(b'program')
628 option = opts.get(b'option')
645 option = opts.get(b'option')
629 if not program:
646 if not program:
630 program = b'diff'
647 program = b'diff'
631 option = option or [b'-Npru']
648 option = option or [b'-Npru']
632 cmdline = b' '.join(map(procutil.shellquote, [program] + option))
649 cmdline = b' '.join(map(procutil.shellquote, [program] + option))
633 return dodiff(ui, repo, cmdline, pats, opts)
650 return dodiff(ui, repo, cmdline, pats, opts)
634
651
635
652
636 class savedcmd(object):
653 class savedcmd(object):
637 """use external program to diff repository (or selected files)
654 """use external program to diff repository (or selected files)
638
655
639 Show differences between revisions for the specified files, using
656 Show differences between revisions for the specified files, using
640 the following program::
657 the following program::
641
658
642 %(path)s
659 %(path)s
643
660
644 When two revision arguments are given, then changes are shown
661 When two revision arguments are given, then changes are shown
645 between those revisions. If only one revision is specified then
662 between those revisions. If only one revision is specified then
646 that revision is compared to the working directory, and, when no
663 that revision is compared to the working directory, and, when no
647 revisions are specified, the working directory files are compared
664 revisions are specified, the working directory files are compared
648 to its parent.
665 to its parent.
649 """
666 """
650
667
651 def __init__(self, path, cmdline, isgui):
668 def __init__(self, path, cmdline, isgui):
652 # We can't pass non-ASCII through docstrings (and path is
669 # We can't pass non-ASCII through docstrings (and path is
653 # in an unknown encoding anyway), but avoid double separators on
670 # in an unknown encoding anyway), but avoid double separators on
654 # Windows
671 # Windows
655 docpath = stringutil.escapestr(path).replace(b'\\\\', b'\\')
672 docpath = stringutil.escapestr(path).replace(b'\\\\', b'\\')
656 self.__doc__ %= {'path': pycompat.sysstr(stringutil.uirepr(docpath))}
673 self.__doc__ %= {'path': pycompat.sysstr(stringutil.uirepr(docpath))}
657 self._cmdline = cmdline
674 self._cmdline = cmdline
658 self._isgui = isgui
675 self._isgui = isgui
659
676
660 def __call__(self, ui, repo, *pats, **opts):
677 def __call__(self, ui, repo, *pats, **opts):
661 opts = pycompat.byteskwargs(opts)
678 opts = pycompat.byteskwargs(opts)
662 options = b' '.join(map(procutil.shellquote, opts[b'option']))
679 options = b' '.join(map(procutil.shellquote, opts[b'option']))
663 if options:
680 if options:
664 options = b' ' + options
681 options = b' ' + options
665 return dodiff(
682 return dodiff(
666 ui, repo, self._cmdline + options, pats, opts, guitool=self._isgui
683 ui, repo, self._cmdline + options, pats, opts, guitool=self._isgui
667 )
684 )
668
685
669
686
670 def uisetup(ui):
687 def uisetup(ui):
671 for cmd, path in ui.configitems(b'extdiff'):
688 for cmd, path in ui.configitems(b'extdiff'):
672 path = util.expandpath(path)
689 path = util.expandpath(path)
673 if cmd.startswith(b'cmd.'):
690 if cmd.startswith(b'cmd.'):
674 cmd = cmd[4:]
691 cmd = cmd[4:]
675 if not path:
692 if not path:
676 path = procutil.findexe(cmd)
693 path = procutil.findexe(cmd)
677 if path is None:
694 if path is None:
678 path = filemerge.findexternaltool(ui, cmd) or cmd
695 path = filemerge.findexternaltool(ui, cmd) or cmd
679 diffopts = ui.config(b'extdiff', b'opts.' + cmd)
696 diffopts = ui.config(b'extdiff', b'opts.' + cmd)
680 cmdline = procutil.shellquote(path)
697 cmdline = procutil.shellquote(path)
681 if diffopts:
698 if diffopts:
682 cmdline += b' ' + diffopts
699 cmdline += b' ' + diffopts
683 isgui = ui.configbool(b'extdiff', b'gui.' + cmd)
700 isgui = ui.configbool(b'extdiff', b'gui.' + cmd)
684 elif cmd.startswith(b'opts.') or cmd.startswith(b'gui.'):
701 elif cmd.startswith(b'opts.') or cmd.startswith(b'gui.'):
685 continue
702 continue
686 else:
703 else:
687 if path:
704 if path:
688 # case "cmd = path opts"
705 # case "cmd = path opts"
689 cmdline = path
706 cmdline = path
690 diffopts = len(pycompat.shlexsplit(cmdline)) > 1
707 diffopts = len(pycompat.shlexsplit(cmdline)) > 1
691 else:
708 else:
692 # case "cmd ="
709 # case "cmd ="
693 path = procutil.findexe(cmd)
710 path = procutil.findexe(cmd)
694 if path is None:
711 if path is None:
695 path = filemerge.findexternaltool(ui, cmd) or cmd
712 path = filemerge.findexternaltool(ui, cmd) or cmd
696 cmdline = procutil.shellquote(path)
713 cmdline = procutil.shellquote(path)
697 diffopts = False
714 diffopts = False
698 isgui = ui.configbool(b'extdiff', b'gui.' + cmd)
715 isgui = ui.configbool(b'extdiff', b'gui.' + cmd)
699 # look for diff arguments in [diff-tools] then [merge-tools]
716 # look for diff arguments in [diff-tools] then [merge-tools]
700 if not diffopts:
717 if not diffopts:
701 key = cmd + b'.diffargs'
718 key = cmd + b'.diffargs'
702 for section in (b'diff-tools', b'merge-tools'):
719 for section in (b'diff-tools', b'merge-tools'):
703 args = ui.config(section, key)
720 args = ui.config(section, key)
704 if args:
721 if args:
705 cmdline += b' ' + args
722 cmdline += b' ' + args
706 if isgui is None:
723 if isgui is None:
707 isgui = ui.configbool(section, cmd + b'.gui') or False
724 isgui = ui.configbool(section, cmd + b'.gui') or False
708 break
725 break
709 command(
726 command(
710 cmd,
727 cmd,
711 extdiffopts[:],
728 extdiffopts[:],
712 _(b'hg %s [OPTION]... [FILE]...') % cmd,
729 _(b'hg %s [OPTION]... [FILE]...') % cmd,
713 helpcategory=command.CATEGORY_FILE_CONTENTS,
730 helpcategory=command.CATEGORY_FILE_CONTENTS,
714 inferrepo=True,
731 inferrepo=True,
715 )(savedcmd(path, cmdline, isgui))
732 )(savedcmd(path, cmdline, isgui))
716
733
717
734
718 # tell hggettext to extract docstrings from these functions:
735 # tell hggettext to extract docstrings from these functions:
719 i18nfunctions = [savedcmd]
736 i18nfunctions = [savedcmd]
General Comments 0
You need to be logged in to leave comments. Login now