##// END OF EJS Templates
extdiff: use field names instead of field numbers on scmutil.status...
Augie Fackler -
r44037:7415cd48 default
parent child Browse files
Show More
@@ -1,721 +1,722 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 cmd = procutil.quotecommand(cmd)
236 cmd = procutil.quotecommand(cmd)
237 env = procutil.shellenviron(environ)
237 env = procutil.shellenviron(environ)
238 proc = subprocess.Popen(
238 proc = subprocess.Popen(
239 procutil.tonativestr(cmd),
239 procutil.tonativestr(cmd),
240 shell=True,
240 shell=True,
241 close_fds=procutil.closefds,
241 close_fds=procutil.closefds,
242 env=procutil.tonativeenv(env),
242 env=procutil.tonativeenv(env),
243 cwd=pycompat.rapply(procutil.tonativestr, cwd),
243 cwd=pycompat.rapply(procutil.tonativestr, cwd),
244 )
244 )
245 return proc
245 return proc
246
246
247
247
248 def _runperfilediff(
248 def _runperfilediff(
249 cmdline,
249 cmdline,
250 repo_root,
250 repo_root,
251 ui,
251 ui,
252 guitool,
252 guitool,
253 do3way,
253 do3way,
254 confirm,
254 confirm,
255 commonfiles,
255 commonfiles,
256 tmproot,
256 tmproot,
257 dir1a,
257 dir1a,
258 dir1b,
258 dir1b,
259 dir2root,
259 dir2root,
260 dir2,
260 dir2,
261 rev1a,
261 rev1a,
262 rev1b,
262 rev1b,
263 rev2,
263 rev2,
264 ):
264 ):
265 # Note that we need to sort the list of files because it was
265 # Note that we need to sort the list of files because it was
266 # built in an "unstable" way and it's annoying to get files in a
266 # built in an "unstable" way and it's annoying to get files in a
267 # random order, especially when "confirm" mode is enabled.
267 # random order, especially when "confirm" mode is enabled.
268 waitprocs = []
268 waitprocs = []
269 totalfiles = len(commonfiles)
269 totalfiles = len(commonfiles)
270 for idx, commonfile in enumerate(sorted(commonfiles)):
270 for idx, commonfile in enumerate(sorted(commonfiles)):
271 path1a = os.path.join(tmproot, dir1a, commonfile)
271 path1a = os.path.join(tmproot, dir1a, commonfile)
272 label1a = commonfile + rev1a
272 label1a = commonfile + rev1a
273 if not os.path.isfile(path1a):
273 if not os.path.isfile(path1a):
274 path1a = os.devnull
274 path1a = os.devnull
275
275
276 path1b = b''
276 path1b = b''
277 label1b = b''
277 label1b = b''
278 if do3way:
278 if do3way:
279 path1b = os.path.join(tmproot, dir1b, commonfile)
279 path1b = os.path.join(tmproot, dir1b, commonfile)
280 label1b = commonfile + rev1b
280 label1b = commonfile + rev1b
281 if not os.path.isfile(path1b):
281 if not os.path.isfile(path1b):
282 path1b = os.devnull
282 path1b = os.devnull
283
283
284 path2 = os.path.join(dir2root, dir2, commonfile)
284 path2 = os.path.join(dir2root, dir2, commonfile)
285 label2 = commonfile + rev2
285 label2 = commonfile + rev2
286
286
287 if confirm:
287 if confirm:
288 # Prompt before showing this diff
288 # Prompt before showing this diff
289 difffiles = _(b'diff %s (%d of %d)') % (
289 difffiles = _(b'diff %s (%d of %d)') % (
290 commonfile,
290 commonfile,
291 idx + 1,
291 idx + 1,
292 totalfiles,
292 totalfiles,
293 )
293 )
294 responses = _(
294 responses = _(
295 b'[Yns?]'
295 b'[Yns?]'
296 b'$$ &Yes, show diff'
296 b'$$ &Yes, show diff'
297 b'$$ &No, skip this diff'
297 b'$$ &No, skip this diff'
298 b'$$ &Skip remaining diffs'
298 b'$$ &Skip remaining diffs'
299 b'$$ &? (display help)'
299 b'$$ &? (display help)'
300 )
300 )
301 r = ui.promptchoice(b'%s %s' % (difffiles, responses))
301 r = ui.promptchoice(b'%s %s' % (difffiles, responses))
302 if r == 3: # ?
302 if r == 3: # ?
303 while r == 3:
303 while r == 3:
304 for c, t in ui.extractchoices(responses)[1]:
304 for c, t in ui.extractchoices(responses)[1]:
305 ui.write(b'%s - %s\n' % (c, encoding.lower(t)))
305 ui.write(b'%s - %s\n' % (c, encoding.lower(t)))
306 r = ui.promptchoice(b'%s %s' % (difffiles, responses))
306 r = ui.promptchoice(b'%s %s' % (difffiles, responses))
307 if r == 0: # yes
307 if r == 0: # yes
308 pass
308 pass
309 elif r == 1: # no
309 elif r == 1: # no
310 continue
310 continue
311 elif r == 2: # skip
311 elif r == 2: # skip
312 break
312 break
313
313
314 curcmdline = formatcmdline(
314 curcmdline = formatcmdline(
315 cmdline,
315 cmdline,
316 repo_root,
316 repo_root,
317 do3way=do3way,
317 do3way=do3way,
318 parent1=path1a,
318 parent1=path1a,
319 plabel1=label1a,
319 plabel1=label1a,
320 parent2=path1b,
320 parent2=path1b,
321 plabel2=label1b,
321 plabel2=label1b,
322 child=path2,
322 child=path2,
323 clabel=label2,
323 clabel=label2,
324 )
324 )
325
325
326 if confirm or not guitool:
326 if confirm or not guitool:
327 # Run the comparison program and wait for it to exit
327 # Run the comparison program and wait for it to exit
328 # before we show the next file.
328 # before we show the next file.
329 # This is because either we need to wait for confirmation
329 # This is because either we need to wait for confirmation
330 # from the user between each invocation, or because, as far
330 # from the user between each invocation, or because, as far
331 # as we know, the tool doesn't have a GUI, in which case
331 # as we know, the tool doesn't have a GUI, in which case
332 # we can't run multiple CLI programs at the same time.
332 # we can't run multiple CLI programs at the same time.
333 ui.debug(
333 ui.debug(
334 b'running %r in %s\n' % (pycompat.bytestr(curcmdline), tmproot)
334 b'running %r in %s\n' % (pycompat.bytestr(curcmdline), tmproot)
335 )
335 )
336 ui.system(curcmdline, cwd=tmproot, blockedtag=b'extdiff')
336 ui.system(curcmdline, cwd=tmproot, blockedtag=b'extdiff')
337 else:
337 else:
338 # Run the comparison program but don't wait, as we're
338 # Run the comparison program but don't wait, as we're
339 # going to rapid-fire each file diff and then wait on
339 # going to rapid-fire each file diff and then wait on
340 # the whole group.
340 # the whole group.
341 ui.debug(
341 ui.debug(
342 b'running %r in %s (backgrounded)\n'
342 b'running %r in %s (backgrounded)\n'
343 % (pycompat.bytestr(curcmdline), tmproot)
343 % (pycompat.bytestr(curcmdline), tmproot)
344 )
344 )
345 proc = _systembackground(curcmdline, cwd=tmproot)
345 proc = _systembackground(curcmdline, cwd=tmproot)
346 waitprocs.append(proc)
346 waitprocs.append(proc)
347
347
348 if waitprocs:
348 if waitprocs:
349 with ui.timeblockedsection(b'extdiff'):
349 with ui.timeblockedsection(b'extdiff'):
350 for proc in waitprocs:
350 for proc in waitprocs:
351 proc.wait()
351 proc.wait()
352
352
353
353
354 def dodiff(ui, repo, cmdline, pats, opts, guitool=False):
354 def dodiff(ui, repo, cmdline, pats, opts, guitool=False):
355 '''Do the actual diff:
355 '''Do the actual diff:
356
356
357 - copy to a temp structure if diffing 2 internal revisions
357 - copy to a temp structure if diffing 2 internal revisions
358 - copy to a temp structure if diffing working revision with
358 - copy to a temp structure if diffing working revision with
359 another one and more than 1 file is changed
359 another one and more than 1 file is changed
360 - just invoke the diff for a single file in the working dir
360 - just invoke the diff for a single file in the working dir
361 '''
361 '''
362
362
363 revs = opts.get(b'rev')
363 revs = opts.get(b'rev')
364 change = opts.get(b'change')
364 change = opts.get(b'change')
365 do3way = b'$parent2' in cmdline
365 do3way = b'$parent2' in cmdline
366
366
367 if revs and change:
367 if revs and change:
368 msg = _(b'cannot specify --rev and --change at the same time')
368 msg = _(b'cannot specify --rev and --change at the same time')
369 raise error.Abort(msg)
369 raise error.Abort(msg)
370 elif change:
370 elif change:
371 ctx2 = scmutil.revsingle(repo, change, None)
371 ctx2 = scmutil.revsingle(repo, change, None)
372 ctx1a, ctx1b = ctx2.p1(), ctx2.p2()
372 ctx1a, ctx1b = ctx2.p1(), ctx2.p2()
373 else:
373 else:
374 ctx1a, ctx2 = scmutil.revpair(repo, revs)
374 ctx1a, ctx2 = scmutil.revpair(repo, revs)
375 if not revs:
375 if not revs:
376 ctx1b = repo[None].p2()
376 ctx1b = repo[None].p2()
377 else:
377 else:
378 ctx1b = repo[nullid]
378 ctx1b = repo[nullid]
379
379
380 perfile = opts.get(b'per_file')
380 perfile = opts.get(b'per_file')
381 confirm = opts.get(b'confirm')
381 confirm = opts.get(b'confirm')
382
382
383 node1a = ctx1a.node()
383 node1a = ctx1a.node()
384 node1b = ctx1b.node()
384 node1b = ctx1b.node()
385 node2 = ctx2.node()
385 node2 = ctx2.node()
386
386
387 # Disable 3-way merge if there is only one parent
387 # Disable 3-way merge if there is only one parent
388 if do3way:
388 if do3way:
389 if node1b == nullid:
389 if node1b == nullid:
390 do3way = False
390 do3way = False
391
391
392 subrepos = opts.get(b'subrepos')
392 subrepos = opts.get(b'subrepos')
393
393
394 matcher = scmutil.match(repo[node2], pats, opts)
394 matcher = scmutil.match(repo[node2], pats, opts)
395
395
396 if opts.get(b'patch'):
396 if opts.get(b'patch'):
397 if subrepos:
397 if subrepos:
398 raise error.Abort(_(b'--patch cannot be used with --subrepos'))
398 raise error.Abort(_(b'--patch cannot be used with --subrepos'))
399 if perfile:
399 if perfile:
400 raise error.Abort(_(b'--patch cannot be used with --per-file'))
400 raise error.Abort(_(b'--patch cannot be used with --per-file'))
401 if node2 is None:
401 if node2 is None:
402 raise error.Abort(_(b'--patch requires two revisions'))
402 raise error.Abort(_(b'--patch requires two revisions'))
403 else:
403 else:
404 mod_a, add_a, rem_a = map(
404 st = repo.status(node1a, node2, matcher, listsubrepos=subrepos)
405 set, repo.status(node1a, node2, matcher, listsubrepos=subrepos)[:3]
405 mod_a, add_a, rem_a = set(st.modified), set(st.added), set(st.removed)
406 )
407 if do3way:
406 if do3way:
408 mod_b, add_b, rem_b = map(
407 stb = repo.status(node1b, node2, matcher, listsubrepos=subrepos)
409 set,
408 mod_b, add_b, rem_b = (
410 repo.status(node1b, node2, matcher, listsubrepos=subrepos)[:3],
409 set(stb.modified),
410 set(stb.added),
411 set(stb.removed),
411 )
412 )
412 else:
413 else:
413 mod_b, add_b, rem_b = set(), set(), set()
414 mod_b, add_b, rem_b = set(), set(), set()
414 modadd = mod_a | add_a | mod_b | add_b
415 modadd = mod_a | add_a | mod_b | add_b
415 common = modadd | rem_a | rem_b
416 common = modadd | rem_a | rem_b
416 if not common:
417 if not common:
417 return 0
418 return 0
418
419
419 tmproot = pycompat.mkdtemp(prefix=b'extdiff.')
420 tmproot = pycompat.mkdtemp(prefix=b'extdiff.')
420 try:
421 try:
421 if not opts.get(b'patch'):
422 if not opts.get(b'patch'):
422 # Always make a copy of node1a (and node1b, if applicable)
423 # Always make a copy of node1a (and node1b, if applicable)
423 dir1a_files = mod_a | rem_a | ((mod_b | add_b) - add_a)
424 dir1a_files = mod_a | rem_a | ((mod_b | add_b) - add_a)
424 dir1a = snapshot(ui, repo, dir1a_files, node1a, tmproot, subrepos)[
425 dir1a = snapshot(ui, repo, dir1a_files, node1a, tmproot, subrepos)[
425 0
426 0
426 ]
427 ]
427 rev1a = b'@%d' % repo[node1a].rev()
428 rev1a = b'@%d' % repo[node1a].rev()
428 if do3way:
429 if do3way:
429 dir1b_files = mod_b | rem_b | ((mod_a | add_a) - add_b)
430 dir1b_files = mod_b | rem_b | ((mod_a | add_a) - add_b)
430 dir1b = snapshot(
431 dir1b = snapshot(
431 ui, repo, dir1b_files, node1b, tmproot, subrepos
432 ui, repo, dir1b_files, node1b, tmproot, subrepos
432 )[0]
433 )[0]
433 rev1b = b'@%d' % repo[node1b].rev()
434 rev1b = b'@%d' % repo[node1b].rev()
434 else:
435 else:
435 dir1b = None
436 dir1b = None
436 rev1b = b''
437 rev1b = b''
437
438
438 fnsandstat = []
439 fnsandstat = []
439
440
440 # If node2 in not the wc or there is >1 change, copy it
441 # If node2 in not the wc or there is >1 change, copy it
441 dir2root = b''
442 dir2root = b''
442 rev2 = b''
443 rev2 = b''
443 if node2:
444 if node2:
444 dir2 = snapshot(ui, repo, modadd, node2, tmproot, subrepos)[0]
445 dir2 = snapshot(ui, repo, modadd, node2, tmproot, subrepos)[0]
445 rev2 = b'@%d' % repo[node2].rev()
446 rev2 = b'@%d' % repo[node2].rev()
446 elif len(common) > 1:
447 elif len(common) > 1:
447 # we only actually need to get the files to copy back to
448 # we only actually need to get the files to copy back to
448 # the working dir in this case (because the other cases
449 # the working dir in this case (because the other cases
449 # are: diffing 2 revisions or single file -- in which case
450 # are: diffing 2 revisions or single file -- in which case
450 # the file is already directly passed to the diff tool).
451 # the file is already directly passed to the diff tool).
451 dir2, fnsandstat = snapshot(
452 dir2, fnsandstat = snapshot(
452 ui, repo, modadd, None, tmproot, subrepos
453 ui, repo, modadd, None, tmproot, subrepos
453 )
454 )
454 else:
455 else:
455 # This lets the diff tool open the changed file directly
456 # This lets the diff tool open the changed file directly
456 dir2 = b''
457 dir2 = b''
457 dir2root = repo.root
458 dir2root = repo.root
458
459
459 label1a = rev1a
460 label1a = rev1a
460 label1b = rev1b
461 label1b = rev1b
461 label2 = rev2
462 label2 = rev2
462
463
463 # If only one change, diff the files instead of the directories
464 # If only one change, diff the files instead of the directories
464 # Handle bogus modifies correctly by checking if the files exist
465 # Handle bogus modifies correctly by checking if the files exist
465 if len(common) == 1:
466 if len(common) == 1:
466 common_file = util.localpath(common.pop())
467 common_file = util.localpath(common.pop())
467 dir1a = os.path.join(tmproot, dir1a, common_file)
468 dir1a = os.path.join(tmproot, dir1a, common_file)
468 label1a = common_file + rev1a
469 label1a = common_file + rev1a
469 if not os.path.isfile(dir1a):
470 if not os.path.isfile(dir1a):
470 dir1a = os.devnull
471 dir1a = os.devnull
471 if do3way:
472 if do3way:
472 dir1b = os.path.join(tmproot, dir1b, common_file)
473 dir1b = os.path.join(tmproot, dir1b, common_file)
473 label1b = common_file + rev1b
474 label1b = common_file + rev1b
474 if not os.path.isfile(dir1b):
475 if not os.path.isfile(dir1b):
475 dir1b = os.devnull
476 dir1b = os.devnull
476 dir2 = os.path.join(dir2root, dir2, common_file)
477 dir2 = os.path.join(dir2root, dir2, common_file)
477 label2 = common_file + rev2
478 label2 = common_file + rev2
478 else:
479 else:
479 template = b'hg-%h.patch'
480 template = b'hg-%h.patch'
480 with formatter.nullformatter(ui, b'extdiff', {}) as fm:
481 with formatter.nullformatter(ui, b'extdiff', {}) as fm:
481 cmdutil.export(
482 cmdutil.export(
482 repo,
483 repo,
483 [repo[node1a].rev(), repo[node2].rev()],
484 [repo[node1a].rev(), repo[node2].rev()],
484 fm,
485 fm,
485 fntemplate=repo.vfs.reljoin(tmproot, template),
486 fntemplate=repo.vfs.reljoin(tmproot, template),
486 match=matcher,
487 match=matcher,
487 )
488 )
488 label1a = cmdutil.makefilename(repo[node1a], template)
489 label1a = cmdutil.makefilename(repo[node1a], template)
489 label2 = cmdutil.makefilename(repo[node2], template)
490 label2 = cmdutil.makefilename(repo[node2], template)
490 dir1a = repo.vfs.reljoin(tmproot, label1a)
491 dir1a = repo.vfs.reljoin(tmproot, label1a)
491 dir2 = repo.vfs.reljoin(tmproot, label2)
492 dir2 = repo.vfs.reljoin(tmproot, label2)
492 dir1b = None
493 dir1b = None
493 label1b = None
494 label1b = None
494 fnsandstat = []
495 fnsandstat = []
495
496
496 if not perfile:
497 if not perfile:
497 # Run the external tool on the 2 temp directories or the patches
498 # Run the external tool on the 2 temp directories or the patches
498 cmdline = formatcmdline(
499 cmdline = formatcmdline(
499 cmdline,
500 cmdline,
500 repo.root,
501 repo.root,
501 do3way=do3way,
502 do3way=do3way,
502 parent1=dir1a,
503 parent1=dir1a,
503 plabel1=label1a,
504 plabel1=label1a,
504 parent2=dir1b,
505 parent2=dir1b,
505 plabel2=label1b,
506 plabel2=label1b,
506 child=dir2,
507 child=dir2,
507 clabel=label2,
508 clabel=label2,
508 )
509 )
509 ui.debug(
510 ui.debug(
510 b'running %r in %s\n' % (pycompat.bytestr(cmdline), tmproot)
511 b'running %r in %s\n' % (pycompat.bytestr(cmdline), tmproot)
511 )
512 )
512 ui.system(cmdline, cwd=tmproot, blockedtag=b'extdiff')
513 ui.system(cmdline, cwd=tmproot, blockedtag=b'extdiff')
513 else:
514 else:
514 # Run the external tool once for each pair of files
515 # Run the external tool once for each pair of files
515 _runperfilediff(
516 _runperfilediff(
516 cmdline,
517 cmdline,
517 repo.root,
518 repo.root,
518 ui,
519 ui,
519 guitool=guitool,
520 guitool=guitool,
520 do3way=do3way,
521 do3way=do3way,
521 confirm=confirm,
522 confirm=confirm,
522 commonfiles=common,
523 commonfiles=common,
523 tmproot=tmproot,
524 tmproot=tmproot,
524 dir1a=dir1a,
525 dir1a=dir1a,
525 dir1b=dir1b,
526 dir1b=dir1b,
526 dir2root=dir2root,
527 dir2root=dir2root,
527 dir2=dir2,
528 dir2=dir2,
528 rev1a=rev1a,
529 rev1a=rev1a,
529 rev1b=rev1b,
530 rev1b=rev1b,
530 rev2=rev2,
531 rev2=rev2,
531 )
532 )
532
533
533 for copy_fn, working_fn, st in fnsandstat:
534 for copy_fn, working_fn, st in fnsandstat:
534 cpstat = os.lstat(copy_fn)
535 cpstat = os.lstat(copy_fn)
535 # Some tools copy the file and attributes, so mtime may not detect
536 # Some tools copy the file and attributes, so mtime may not detect
536 # all changes. A size check will detect more cases, but not all.
537 # all changes. A size check will detect more cases, but not all.
537 # The only certain way to detect every case is to diff all files,
538 # The only certain way to detect every case is to diff all files,
538 # which could be expensive.
539 # which could be expensive.
539 # copyfile() carries over the permission, so the mode check could
540 # copyfile() carries over the permission, so the mode check could
540 # be in an 'elif' branch, but for the case where the file has
541 # be in an 'elif' branch, but for the case where the file has
541 # changed without affecting mtime or size.
542 # changed without affecting mtime or size.
542 if (
543 if (
543 cpstat[stat.ST_MTIME] != st[stat.ST_MTIME]
544 cpstat[stat.ST_MTIME] != st[stat.ST_MTIME]
544 or cpstat.st_size != st.st_size
545 or cpstat.st_size != st.st_size
545 or (cpstat.st_mode & 0o100) != (st.st_mode & 0o100)
546 or (cpstat.st_mode & 0o100) != (st.st_mode & 0o100)
546 ):
547 ):
547 ui.debug(
548 ui.debug(
548 b'file changed while diffing. '
549 b'file changed while diffing. '
549 b'Overwriting: %s (src: %s)\n' % (working_fn, copy_fn)
550 b'Overwriting: %s (src: %s)\n' % (working_fn, copy_fn)
550 )
551 )
551 util.copyfile(copy_fn, working_fn)
552 util.copyfile(copy_fn, working_fn)
552
553
553 return 1
554 return 1
554 finally:
555 finally:
555 ui.note(_(b'cleaning up temp directory\n'))
556 ui.note(_(b'cleaning up temp directory\n'))
556 shutil.rmtree(tmproot)
557 shutil.rmtree(tmproot)
557
558
558
559
559 extdiffopts = (
560 extdiffopts = (
560 [
561 [
561 (
562 (
562 b'o',
563 b'o',
563 b'option',
564 b'option',
564 [],
565 [],
565 _(b'pass option to comparison program'),
566 _(b'pass option to comparison program'),
566 _(b'OPT'),
567 _(b'OPT'),
567 ),
568 ),
568 (b'r', b'rev', [], _(b'revision'), _(b'REV')),
569 (b'r', b'rev', [], _(b'revision'), _(b'REV')),
569 (b'c', b'change', b'', _(b'change made by revision'), _(b'REV')),
570 (b'c', b'change', b'', _(b'change made by revision'), _(b'REV')),
570 (
571 (
571 b'',
572 b'',
572 b'per-file',
573 b'per-file',
573 False,
574 False,
574 _(b'compare each file instead of revision snapshots'),
575 _(b'compare each file instead of revision snapshots'),
575 ),
576 ),
576 (
577 (
577 b'',
578 b'',
578 b'confirm',
579 b'confirm',
579 False,
580 False,
580 _(b'prompt user before each external program invocation'),
581 _(b'prompt user before each external program invocation'),
581 ),
582 ),
582 (b'', b'patch', None, _(b'compare patches for two revisions')),
583 (b'', b'patch', None, _(b'compare patches for two revisions')),
583 ]
584 ]
584 + cmdutil.walkopts
585 + cmdutil.walkopts
585 + cmdutil.subrepoopts
586 + cmdutil.subrepoopts
586 )
587 )
587
588
588
589
589 @command(
590 @command(
590 b'extdiff',
591 b'extdiff',
591 [(b'p', b'program', b'', _(b'comparison program to run'), _(b'CMD')),]
592 [(b'p', b'program', b'', _(b'comparison program to run'), _(b'CMD')),]
592 + extdiffopts,
593 + extdiffopts,
593 _(b'hg extdiff [OPT]... [FILE]...'),
594 _(b'hg extdiff [OPT]... [FILE]...'),
594 helpcategory=command.CATEGORY_FILE_CONTENTS,
595 helpcategory=command.CATEGORY_FILE_CONTENTS,
595 inferrepo=True,
596 inferrepo=True,
596 )
597 )
597 def extdiff(ui, repo, *pats, **opts):
598 def extdiff(ui, repo, *pats, **opts):
598 '''use external program to diff repository (or selected files)
599 '''use external program to diff repository (or selected files)
599
600
600 Show differences between revisions for the specified files, using
601 Show differences between revisions for the specified files, using
601 an external program. The default program used is diff, with
602 an external program. The default program used is diff, with
602 default options "-Npru".
603 default options "-Npru".
603
604
604 To select a different program, use the -p/--program option. The
605 To select a different program, use the -p/--program option. The
605 program will be passed the names of two directories to compare,
606 program will be passed the names of two directories to compare,
606 unless the --per-file option is specified (see below). To pass
607 unless the --per-file option is specified (see below). To pass
607 additional options to the program, use -o/--option. These will be
608 additional options to the program, use -o/--option. These will be
608 passed before the names of the directories or files to compare.
609 passed before the names of the directories or files to compare.
609
610
610 When two revision arguments are given, then changes are shown
611 When two revision arguments are given, then changes are shown
611 between those revisions. If only one revision is specified then
612 between those revisions. If only one revision is specified then
612 that revision is compared to the working directory, and, when no
613 that revision is compared to the working directory, and, when no
613 revisions are specified, the working directory files are compared
614 revisions are specified, the working directory files are compared
614 to its parent.
615 to its parent.
615
616
616 The --per-file option runs the external program repeatedly on each
617 The --per-file option runs the external program repeatedly on each
617 file to diff, instead of once on two directories. By default,
618 file to diff, instead of once on two directories. By default,
618 this happens one by one, where the next file diff is open in the
619 this happens one by one, where the next file diff is open in the
619 external program only once the previous external program (for the
620 external program only once the previous external program (for the
620 previous file diff) has exited. If the external program has a
621 previous file diff) has exited. If the external program has a
621 graphical interface, it can open all the file diffs at once instead
622 graphical interface, it can open all the file diffs at once instead
622 of one by one. See :hg:`help -e extdiff` for information about how
623 of one by one. See :hg:`help -e extdiff` for information about how
623 to tell Mercurial that a given program has a graphical interface.
624 to tell Mercurial that a given program has a graphical interface.
624
625
625 The --confirm option will prompt the user before each invocation of
626 The --confirm option will prompt the user before each invocation of
626 the external program. It is ignored if --per-file isn't specified.
627 the external program. It is ignored if --per-file isn't specified.
627 '''
628 '''
628 opts = pycompat.byteskwargs(opts)
629 opts = pycompat.byteskwargs(opts)
629 program = opts.get(b'program')
630 program = opts.get(b'program')
630 option = opts.get(b'option')
631 option = opts.get(b'option')
631 if not program:
632 if not program:
632 program = b'diff'
633 program = b'diff'
633 option = option or [b'-Npru']
634 option = option or [b'-Npru']
634 cmdline = b' '.join(map(procutil.shellquote, [program] + option))
635 cmdline = b' '.join(map(procutil.shellquote, [program] + option))
635 return dodiff(ui, repo, cmdline, pats, opts)
636 return dodiff(ui, repo, cmdline, pats, opts)
636
637
637
638
638 class savedcmd(object):
639 class savedcmd(object):
639 """use external program to diff repository (or selected files)
640 """use external program to diff repository (or selected files)
640
641
641 Show differences between revisions for the specified files, using
642 Show differences between revisions for the specified files, using
642 the following program::
643 the following program::
643
644
644 %(path)s
645 %(path)s
645
646
646 When two revision arguments are given, then changes are shown
647 When two revision arguments are given, then changes are shown
647 between those revisions. If only one revision is specified then
648 between those revisions. If only one revision is specified then
648 that revision is compared to the working directory, and, when no
649 that revision is compared to the working directory, and, when no
649 revisions are specified, the working directory files are compared
650 revisions are specified, the working directory files are compared
650 to its parent.
651 to its parent.
651 """
652 """
652
653
653 def __init__(self, path, cmdline, isgui):
654 def __init__(self, path, cmdline, isgui):
654 # We can't pass non-ASCII through docstrings (and path is
655 # We can't pass non-ASCII through docstrings (and path is
655 # in an unknown encoding anyway), but avoid double separators on
656 # in an unknown encoding anyway), but avoid double separators on
656 # Windows
657 # Windows
657 docpath = stringutil.escapestr(path).replace(b'\\\\', b'\\')
658 docpath = stringutil.escapestr(path).replace(b'\\\\', b'\\')
658 self.__doc__ %= {'path': pycompat.sysstr(stringutil.uirepr(docpath))}
659 self.__doc__ %= {'path': pycompat.sysstr(stringutil.uirepr(docpath))}
659 self._cmdline = cmdline
660 self._cmdline = cmdline
660 self._isgui = isgui
661 self._isgui = isgui
661
662
662 def __call__(self, ui, repo, *pats, **opts):
663 def __call__(self, ui, repo, *pats, **opts):
663 opts = pycompat.byteskwargs(opts)
664 opts = pycompat.byteskwargs(opts)
664 options = b' '.join(map(procutil.shellquote, opts[b'option']))
665 options = b' '.join(map(procutil.shellquote, opts[b'option']))
665 if options:
666 if options:
666 options = b' ' + options
667 options = b' ' + options
667 return dodiff(
668 return dodiff(
668 ui, repo, self._cmdline + options, pats, opts, guitool=self._isgui
669 ui, repo, self._cmdline + options, pats, opts, guitool=self._isgui
669 )
670 )
670
671
671
672
672 def uisetup(ui):
673 def uisetup(ui):
673 for cmd, path in ui.configitems(b'extdiff'):
674 for cmd, path in ui.configitems(b'extdiff'):
674 path = util.expandpath(path)
675 path = util.expandpath(path)
675 if cmd.startswith(b'cmd.'):
676 if cmd.startswith(b'cmd.'):
676 cmd = cmd[4:]
677 cmd = cmd[4:]
677 if not path:
678 if not path:
678 path = procutil.findexe(cmd)
679 path = procutil.findexe(cmd)
679 if path is None:
680 if path is None:
680 path = filemerge.findexternaltool(ui, cmd) or cmd
681 path = filemerge.findexternaltool(ui, cmd) or cmd
681 diffopts = ui.config(b'extdiff', b'opts.' + cmd)
682 diffopts = ui.config(b'extdiff', b'opts.' + cmd)
682 cmdline = procutil.shellquote(path)
683 cmdline = procutil.shellquote(path)
683 if diffopts:
684 if diffopts:
684 cmdline += b' ' + diffopts
685 cmdline += b' ' + diffopts
685 isgui = ui.configbool(b'extdiff', b'gui.' + cmd)
686 isgui = ui.configbool(b'extdiff', b'gui.' + cmd)
686 elif cmd.startswith(b'opts.') or cmd.startswith(b'gui.'):
687 elif cmd.startswith(b'opts.') or cmd.startswith(b'gui.'):
687 continue
688 continue
688 else:
689 else:
689 if path:
690 if path:
690 # case "cmd = path opts"
691 # case "cmd = path opts"
691 cmdline = path
692 cmdline = path
692 diffopts = len(pycompat.shlexsplit(cmdline)) > 1
693 diffopts = len(pycompat.shlexsplit(cmdline)) > 1
693 else:
694 else:
694 # case "cmd ="
695 # case "cmd ="
695 path = procutil.findexe(cmd)
696 path = procutil.findexe(cmd)
696 if path is None:
697 if path is None:
697 path = filemerge.findexternaltool(ui, cmd) or cmd
698 path = filemerge.findexternaltool(ui, cmd) or cmd
698 cmdline = procutil.shellquote(path)
699 cmdline = procutil.shellquote(path)
699 diffopts = False
700 diffopts = False
700 isgui = ui.configbool(b'extdiff', b'gui.' + cmd)
701 isgui = ui.configbool(b'extdiff', b'gui.' + cmd)
701 # look for diff arguments in [diff-tools] then [merge-tools]
702 # look for diff arguments in [diff-tools] then [merge-tools]
702 if not diffopts:
703 if not diffopts:
703 key = cmd + b'.diffargs'
704 key = cmd + b'.diffargs'
704 for section in (b'diff-tools', b'merge-tools'):
705 for section in (b'diff-tools', b'merge-tools'):
705 args = ui.config(section, key)
706 args = ui.config(section, key)
706 if args:
707 if args:
707 cmdline += b' ' + args
708 cmdline += b' ' + args
708 if isgui is None:
709 if isgui is None:
709 isgui = ui.configbool(section, cmd + b'.gui') or False
710 isgui = ui.configbool(section, cmd + b'.gui') or False
710 break
711 break
711 command(
712 command(
712 cmd,
713 cmd,
713 extdiffopts[:],
714 extdiffopts[:],
714 _(b'hg %s [OPTION]... [FILE]...') % cmd,
715 _(b'hg %s [OPTION]... [FILE]...') % cmd,
715 helpcategory=command.CATEGORY_FILE_CONTENTS,
716 helpcategory=command.CATEGORY_FILE_CONTENTS,
716 inferrepo=True,
717 inferrepo=True,
717 )(savedcmd(path, cmdline, isgui))
718 )(savedcmd(path, cmdline, isgui))
718
719
719
720
720 # tell hggettext to extract docstrings from these functions:
721 # tell hggettext to extract docstrings from these functions:
721 i18nfunctions = [savedcmd]
722 i18nfunctions = [savedcmd]
General Comments 0
You need to be logged in to leave comments. Login now