##// END OF EJS Templates
extdiff: move external tool command line building into separate function
Ludovic Chabant -
r41232:4f675c12 default
parent child Browse files
Show More
@@ -1,437 +1,446 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 You can use -I/-X and list of file or directory names like normal
62 You can use -I/-X and list of file or directory names like normal
63 :hg:`diff` command. The extdiff extension makes snapshots of only
63 :hg:`diff` command. The extdiff extension makes snapshots of only
64 needed files, so running the external diff program will actually be
64 needed files, so running the external diff program will actually be
65 pretty fast (at least faster than having to compare the entire tree).
65 pretty fast (at least faster than having to compare the entire tree).
66 '''
66 '''
67
67
68 from __future__ import absolute_import
68 from __future__ import absolute_import
69
69
70 import os
70 import os
71 import re
71 import re
72 import shutil
72 import shutil
73 import stat
73 import stat
74
74
75 from mercurial.i18n import _
75 from mercurial.i18n import _
76 from mercurial.node import (
76 from mercurial.node import (
77 nullid,
77 nullid,
78 short,
78 short,
79 )
79 )
80 from mercurial import (
80 from mercurial import (
81 archival,
81 archival,
82 cmdutil,
82 cmdutil,
83 error,
83 error,
84 filemerge,
84 filemerge,
85 formatter,
85 formatter,
86 pycompat,
86 pycompat,
87 registrar,
87 registrar,
88 scmutil,
88 scmutil,
89 util,
89 util,
90 )
90 )
91 from mercurial.utils import (
91 from mercurial.utils import (
92 procutil,
92 procutil,
93 stringutil,
93 stringutil,
94 )
94 )
95
95
96 cmdtable = {}
96 cmdtable = {}
97 command = registrar.command(cmdtable)
97 command = registrar.command(cmdtable)
98
98
99 configtable = {}
99 configtable = {}
100 configitem = registrar.configitem(configtable)
100 configitem = registrar.configitem(configtable)
101
101
102 configitem('extdiff', br'opts\..*',
102 configitem('extdiff', br'opts\..*',
103 default='',
103 default='',
104 generic=True,
104 generic=True,
105 )
105 )
106
106
107 configitem('diff-tools', br'.*\.diffargs$',
107 configitem('diff-tools', br'.*\.diffargs$',
108 default=None,
108 default=None,
109 generic=True,
109 generic=True,
110 )
110 )
111
111
112 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
112 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
113 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
113 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
114 # be specifying the version(s) of Mercurial they are tested with, or
114 # be specifying the version(s) of Mercurial they are tested with, or
115 # leave the attribute unspecified.
115 # leave the attribute unspecified.
116 testedwith = 'ships-with-hg-core'
116 testedwith = 'ships-with-hg-core'
117
117
118 def snapshot(ui, repo, files, node, tmproot, listsubrepos):
118 def snapshot(ui, repo, files, node, tmproot, listsubrepos):
119 '''snapshot files as of some revision
119 '''snapshot files as of some revision
120 if not using snapshot, -I/-X does not work and recursive diff
120 if not using snapshot, -I/-X does not work and recursive diff
121 in tools like kdiff3 and meld displays too many files.'''
121 in tools like kdiff3 and meld displays too many files.'''
122 dirname = os.path.basename(repo.root)
122 dirname = os.path.basename(repo.root)
123 if dirname == "":
123 if dirname == "":
124 dirname = "root"
124 dirname = "root"
125 if node is not None:
125 if node is not None:
126 dirname = '%s.%s' % (dirname, short(node))
126 dirname = '%s.%s' % (dirname, short(node))
127 base = os.path.join(tmproot, dirname)
127 base = os.path.join(tmproot, dirname)
128 os.mkdir(base)
128 os.mkdir(base)
129 fnsandstat = []
129 fnsandstat = []
130
130
131 if node is not None:
131 if node is not None:
132 ui.note(_('making snapshot of %d files from rev %s\n') %
132 ui.note(_('making snapshot of %d files from rev %s\n') %
133 (len(files), short(node)))
133 (len(files), short(node)))
134 else:
134 else:
135 ui.note(_('making snapshot of %d files from working directory\n') %
135 ui.note(_('making snapshot of %d files from working directory\n') %
136 (len(files)))
136 (len(files)))
137
137
138 if files:
138 if files:
139 repo.ui.setconfig("ui", "archivemeta", False)
139 repo.ui.setconfig("ui", "archivemeta", False)
140
140
141 archival.archive(repo, base, node, 'files',
141 archival.archive(repo, base, node, 'files',
142 match=scmutil.matchfiles(repo, files),
142 match=scmutil.matchfiles(repo, files),
143 subrepos=listsubrepos)
143 subrepos=listsubrepos)
144
144
145 for fn in sorted(files):
145 for fn in sorted(files):
146 wfn = util.pconvert(fn)
146 wfn = util.pconvert(fn)
147 ui.note(' %s\n' % wfn)
147 ui.note(' %s\n' % wfn)
148
148
149 if node is None:
149 if node is None:
150 dest = os.path.join(base, wfn)
150 dest = os.path.join(base, wfn)
151
151
152 fnsandstat.append((dest, repo.wjoin(fn), os.lstat(dest)))
152 fnsandstat.append((dest, repo.wjoin(fn), os.lstat(dest)))
153 return dirname, fnsandstat
153 return dirname, fnsandstat
154
154
155 def formatcmdline(cmdline, repo_root, do3way,
156 parent1, plabel1, parent2, plabel2, child, clabel):
157 # Function to quote file/dir names in the argument string.
158 # When not operating in 3-way mode, an empty string is
159 # returned for parent2
160 replace = {'parent': parent1, 'parent1': parent1, 'parent2': parent2,
161 'plabel1': plabel1, 'plabel2': plabel2,
162 'child': child, 'clabel': clabel,
163 'root': repo_root}
164 def quote(match):
165 pre = match.group(2)
166 key = match.group(3)
167 if not do3way and key == 'parent2':
168 return pre
169 return pre + procutil.shellquote(replace[key])
170
171 # Match parent2 first, so 'parent1?' will match both parent1 and parent
172 regex = (br'''(['"]?)([^\s'"$]*)'''
173 br'\$(parent2|parent1?|child|plabel1|plabel2|clabel|root)\1')
174 if not do3way and not re.search(regex, cmdline):
175 cmdline += ' $parent1 $child'
176 return re.sub(regex, quote, cmdline)
177
155 def dodiff(ui, repo, cmdline, pats, opts):
178 def dodiff(ui, repo, cmdline, pats, opts):
156 '''Do the actual diff:
179 '''Do the actual diff:
157
180
158 - copy to a temp structure if diffing 2 internal revisions
181 - copy to a temp structure if diffing 2 internal revisions
159 - copy to a temp structure if diffing working revision with
182 - copy to a temp structure if diffing working revision with
160 another one and more than 1 file is changed
183 another one and more than 1 file is changed
161 - just invoke the diff for a single file in the working dir
184 - just invoke the diff for a single file in the working dir
162 '''
185 '''
163
186
164 revs = opts.get('rev')
187 revs = opts.get('rev')
165 change = opts.get('change')
188 change = opts.get('change')
166 do3way = '$parent2' in cmdline
189 do3way = '$parent2' in cmdline
167
190
168 if revs and change:
191 if revs and change:
169 msg = _('cannot specify --rev and --change at the same time')
192 msg = _('cannot specify --rev and --change at the same time')
170 raise error.Abort(msg)
193 raise error.Abort(msg)
171 elif change:
194 elif change:
172 ctx2 = scmutil.revsingle(repo, change, None)
195 ctx2 = scmutil.revsingle(repo, change, None)
173 ctx1a, ctx1b = ctx2.p1(), ctx2.p2()
196 ctx1a, ctx1b = ctx2.p1(), ctx2.p2()
174 else:
197 else:
175 ctx1a, ctx2 = scmutil.revpair(repo, revs)
198 ctx1a, ctx2 = scmutil.revpair(repo, revs)
176 if not revs:
199 if not revs:
177 ctx1b = repo[None].p2()
200 ctx1b = repo[None].p2()
178 else:
201 else:
179 ctx1b = repo[nullid]
202 ctx1b = repo[nullid]
180
203
181 node1a = ctx1a.node()
204 node1a = ctx1a.node()
182 node1b = ctx1b.node()
205 node1b = ctx1b.node()
183 node2 = ctx2.node()
206 node2 = ctx2.node()
184
207
185 # Disable 3-way merge if there is only one parent
208 # Disable 3-way merge if there is only one parent
186 if do3way:
209 if do3way:
187 if node1b == nullid:
210 if node1b == nullid:
188 do3way = False
211 do3way = False
189
212
190 subrepos=opts.get('subrepos')
213 subrepos=opts.get('subrepos')
191
214
192 matcher = scmutil.match(repo[node2], pats, opts)
215 matcher = scmutil.match(repo[node2], pats, opts)
193
216
194 if opts.get('patch'):
217 if opts.get('patch'):
195 if subrepos:
218 if subrepos:
196 raise error.Abort(_('--patch cannot be used with --subrepos'))
219 raise error.Abort(_('--patch cannot be used with --subrepos'))
197 if node2 is None:
220 if node2 is None:
198 raise error.Abort(_('--patch requires two revisions'))
221 raise error.Abort(_('--patch requires two revisions'))
199 else:
222 else:
200 mod_a, add_a, rem_a = map(set, repo.status(node1a, node2, matcher,
223 mod_a, add_a, rem_a = map(set, repo.status(node1a, node2, matcher,
201 listsubrepos=subrepos)[:3])
224 listsubrepos=subrepos)[:3])
202 if do3way:
225 if do3way:
203 mod_b, add_b, rem_b = map(set,
226 mod_b, add_b, rem_b = map(set,
204 repo.status(node1b, node2, matcher,
227 repo.status(node1b, node2, matcher,
205 listsubrepos=subrepos)[:3])
228 listsubrepos=subrepos)[:3])
206 else:
229 else:
207 mod_b, add_b, rem_b = set(), set(), set()
230 mod_b, add_b, rem_b = set(), set(), set()
208 modadd = mod_a | add_a | mod_b | add_b
231 modadd = mod_a | add_a | mod_b | add_b
209 common = modadd | rem_a | rem_b
232 common = modadd | rem_a | rem_b
210 if not common:
233 if not common:
211 return 0
234 return 0
212
235
213 tmproot = pycompat.mkdtemp(prefix='extdiff.')
236 tmproot = pycompat.mkdtemp(prefix='extdiff.')
214 try:
237 try:
215 if not opts.get('patch'):
238 if not opts.get('patch'):
216 # Always make a copy of node1a (and node1b, if applicable)
239 # Always make a copy of node1a (and node1b, if applicable)
217 dir1a_files = mod_a | rem_a | ((mod_b | add_b) - add_a)
240 dir1a_files = mod_a | rem_a | ((mod_b | add_b) - add_a)
218 dir1a = snapshot(ui, repo, dir1a_files, node1a, tmproot,
241 dir1a = snapshot(ui, repo, dir1a_files, node1a, tmproot,
219 subrepos)[0]
242 subrepos)[0]
220 rev1a = '@%d' % repo[node1a].rev()
243 rev1a = '@%d' % repo[node1a].rev()
221 if do3way:
244 if do3way:
222 dir1b_files = mod_b | rem_b | ((mod_a | add_a) - add_b)
245 dir1b_files = mod_b | rem_b | ((mod_a | add_a) - add_b)
223 dir1b = snapshot(ui, repo, dir1b_files, node1b, tmproot,
246 dir1b = snapshot(ui, repo, dir1b_files, node1b, tmproot,
224 subrepos)[0]
247 subrepos)[0]
225 rev1b = '@%d' % repo[node1b].rev()
248 rev1b = '@%d' % repo[node1b].rev()
226 else:
249 else:
227 dir1b = None
250 dir1b = None
228 rev1b = ''
251 rev1b = ''
229
252
230 fnsandstat = []
253 fnsandstat = []
231
254
232 # If node2 in not the wc or there is >1 change, copy it
255 # If node2 in not the wc or there is >1 change, copy it
233 dir2root = ''
256 dir2root = ''
234 rev2 = ''
257 rev2 = ''
235 if node2:
258 if node2:
236 dir2 = snapshot(ui, repo, modadd, node2, tmproot, subrepos)[0]
259 dir2 = snapshot(ui, repo, modadd, node2, tmproot, subrepos)[0]
237 rev2 = '@%d' % repo[node2].rev()
260 rev2 = '@%d' % repo[node2].rev()
238 elif len(common) > 1:
261 elif len(common) > 1:
239 #we only actually need to get the files to copy back to
262 #we only actually need to get the files to copy back to
240 #the working dir in this case (because the other cases
263 #the working dir in this case (because the other cases
241 #are: diffing 2 revisions or single file -- in which case
264 #are: diffing 2 revisions or single file -- in which case
242 #the file is already directly passed to the diff tool).
265 #the file is already directly passed to the diff tool).
243 dir2, fnsandstat = snapshot(ui, repo, modadd, None, tmproot,
266 dir2, fnsandstat = snapshot(ui, repo, modadd, None, tmproot,
244 subrepos)
267 subrepos)
245 else:
268 else:
246 # This lets the diff tool open the changed file directly
269 # This lets the diff tool open the changed file directly
247 dir2 = ''
270 dir2 = ''
248 dir2root = repo.root
271 dir2root = repo.root
249
272
250 label1a = rev1a
273 label1a = rev1a
251 label1b = rev1b
274 label1b = rev1b
252 label2 = rev2
275 label2 = rev2
253
276
254 # If only one change, diff the files instead of the directories
277 # If only one change, diff the files instead of the directories
255 # Handle bogus modifies correctly by checking if the files exist
278 # Handle bogus modifies correctly by checking if the files exist
256 if len(common) == 1:
279 if len(common) == 1:
257 common_file = util.localpath(common.pop())
280 common_file = util.localpath(common.pop())
258 dir1a = os.path.join(tmproot, dir1a, common_file)
281 dir1a = os.path.join(tmproot, dir1a, common_file)
259 label1a = common_file + rev1a
282 label1a = common_file + rev1a
260 if not os.path.isfile(dir1a):
283 if not os.path.isfile(dir1a):
261 dir1a = os.devnull
284 dir1a = os.devnull
262 if do3way:
285 if do3way:
263 dir1b = os.path.join(tmproot, dir1b, common_file)
286 dir1b = os.path.join(tmproot, dir1b, common_file)
264 label1b = common_file + rev1b
287 label1b = common_file + rev1b
265 if not os.path.isfile(dir1b):
288 if not os.path.isfile(dir1b):
266 dir1b = os.devnull
289 dir1b = os.devnull
267 dir2 = os.path.join(dir2root, dir2, common_file)
290 dir2 = os.path.join(dir2root, dir2, common_file)
268 label2 = common_file + rev2
291 label2 = common_file + rev2
269 else:
292 else:
270 template = 'hg-%h.patch'
293 template = 'hg-%h.patch'
271 with formatter.nullformatter(ui, 'extdiff', {}) as fm:
294 with formatter.nullformatter(ui, 'extdiff', {}) as fm:
272 cmdutil.export(repo, [repo[node1a].rev(), repo[node2].rev()],
295 cmdutil.export(repo, [repo[node1a].rev(), repo[node2].rev()],
273 fm,
296 fm,
274 fntemplate=repo.vfs.reljoin(tmproot, template),
297 fntemplate=repo.vfs.reljoin(tmproot, template),
275 match=matcher)
298 match=matcher)
276 label1a = cmdutil.makefilename(repo[node1a], template)
299 label1a = cmdutil.makefilename(repo[node1a], template)
277 label2 = cmdutil.makefilename(repo[node2], template)
300 label2 = cmdutil.makefilename(repo[node2], template)
278 dir1a = repo.vfs.reljoin(tmproot, label1a)
301 dir1a = repo.vfs.reljoin(tmproot, label1a)
279 dir2 = repo.vfs.reljoin(tmproot, label2)
302 dir2 = repo.vfs.reljoin(tmproot, label2)
280 dir1b = None
303 dir1b = None
281 label1b = None
304 label1b = None
282 fnsandstat = []
305 fnsandstat = []
283
306
284 # Function to quote file/dir names in the argument string.
307 # Run the external tool on the 2 temp directories or the patches
285 # When not operating in 3-way mode, an empty string is
308 cmdline = formatcmdline(
286 # returned for parent2
309 cmdline, repo.root, do3way=do3way,
287 replace = {'parent': dir1a, 'parent1': dir1a, 'parent2': dir1b,
310 parent1=dir1a, plabel1=label1a,
288 'plabel1': label1a, 'plabel2': label1b,
311 parent2=dir1b, plabel2=label1b,
289 'clabel': label2, 'child': dir2,
312 child=dir2, clabel=label2)
290 'root': repo.root}
313 ui.debug('running %r in %s\n' % (pycompat.bytestr(cmdline),
291 def quote(match):
314 tmproot))
292 pre = match.group(2)
293 key = match.group(3)
294 if not do3way and key == 'parent2':
295 return pre
296 return pre + procutil.shellquote(replace[key])
297
298 # Match parent2 first, so 'parent1?' will match both parent1 and parent
299 regex = (br'''(['"]?)([^\s'"$]*)'''
300 br'\$(parent2|parent1?|child|plabel1|plabel2|clabel|root)\1')
301 if not do3way and not re.search(regex, cmdline):
302 cmdline += ' $parent1 $child'
303 cmdline = re.sub(regex, quote, cmdline)
304
305 ui.debug('running %r in %s\n' % (pycompat.bytestr(cmdline), tmproot))
306 ui.system(cmdline, cwd=tmproot, blockedtag='extdiff')
315 ui.system(cmdline, cwd=tmproot, blockedtag='extdiff')
307
316
308 for copy_fn, working_fn, st in fnsandstat:
317 for copy_fn, working_fn, st in fnsandstat:
309 cpstat = os.lstat(copy_fn)
318 cpstat = os.lstat(copy_fn)
310 # Some tools copy the file and attributes, so mtime may not detect
319 # Some tools copy the file and attributes, so mtime may not detect
311 # all changes. A size check will detect more cases, but not all.
320 # all changes. A size check will detect more cases, but not all.
312 # The only certain way to detect every case is to diff all files,
321 # The only certain way to detect every case is to diff all files,
313 # which could be expensive.
322 # which could be expensive.
314 # copyfile() carries over the permission, so the mode check could
323 # copyfile() carries over the permission, so the mode check could
315 # be in an 'elif' branch, but for the case where the file has
324 # be in an 'elif' branch, but for the case where the file has
316 # changed without affecting mtime or size.
325 # changed without affecting mtime or size.
317 if (cpstat[stat.ST_MTIME] != st[stat.ST_MTIME]
326 if (cpstat[stat.ST_MTIME] != st[stat.ST_MTIME]
318 or cpstat.st_size != st.st_size
327 or cpstat.st_size != st.st_size
319 or (cpstat.st_mode & 0o100) != (st.st_mode & 0o100)):
328 or (cpstat.st_mode & 0o100) != (st.st_mode & 0o100)):
320 ui.debug('file changed while diffing. '
329 ui.debug('file changed while diffing. '
321 'Overwriting: %s (src: %s)\n' % (working_fn, copy_fn))
330 'Overwriting: %s (src: %s)\n' % (working_fn, copy_fn))
322 util.copyfile(copy_fn, working_fn)
331 util.copyfile(copy_fn, working_fn)
323
332
324 return 1
333 return 1
325 finally:
334 finally:
326 ui.note(_('cleaning up temp directory\n'))
335 ui.note(_('cleaning up temp directory\n'))
327 shutil.rmtree(tmproot)
336 shutil.rmtree(tmproot)
328
337
329 extdiffopts = [
338 extdiffopts = [
330 ('o', 'option', [],
339 ('o', 'option', [],
331 _('pass option to comparison program'), _('OPT')),
340 _('pass option to comparison program'), _('OPT')),
332 ('r', 'rev', [], _('revision'), _('REV')),
341 ('r', 'rev', [], _('revision'), _('REV')),
333 ('c', 'change', '', _('change made by revision'), _('REV')),
342 ('c', 'change', '', _('change made by revision'), _('REV')),
334 ('', 'patch', None, _('compare patches for two revisions'))
343 ('', 'patch', None, _('compare patches for two revisions'))
335 ] + cmdutil.walkopts + cmdutil.subrepoopts
344 ] + cmdutil.walkopts + cmdutil.subrepoopts
336
345
337 @command('extdiff',
346 @command('extdiff',
338 [('p', 'program', '', _('comparison program to run'), _('CMD')),
347 [('p', 'program', '', _('comparison program to run'), _('CMD')),
339 ] + extdiffopts,
348 ] + extdiffopts,
340 _('hg extdiff [OPT]... [FILE]...'),
349 _('hg extdiff [OPT]... [FILE]...'),
341 helpcategory=command.CATEGORY_FILE_CONTENTS,
350 helpcategory=command.CATEGORY_FILE_CONTENTS,
342 inferrepo=True)
351 inferrepo=True)
343 def extdiff(ui, repo, *pats, **opts):
352 def extdiff(ui, repo, *pats, **opts):
344 '''use external program to diff repository (or selected files)
353 '''use external program to diff repository (or selected files)
345
354
346 Show differences between revisions for the specified files, using
355 Show differences between revisions for the specified files, using
347 an external program. The default program used is diff, with
356 an external program. The default program used is diff, with
348 default options "-Npru".
357 default options "-Npru".
349
358
350 To select a different program, use the -p/--program option. The
359 To select a different program, use the -p/--program option. The
351 program will be passed the names of two directories to compare. To
360 program will be passed the names of two directories to compare. To
352 pass additional options to the program, use -o/--option. These
361 pass additional options to the program, use -o/--option. These
353 will be passed before the names of the directories to compare.
362 will be passed before the names of the directories to compare.
354
363
355 When two revision arguments are given, then changes are shown
364 When two revision arguments are given, then changes are shown
356 between those revisions. If only one revision is specified then
365 between those revisions. If only one revision is specified then
357 that revision is compared to the working directory, and, when no
366 that revision is compared to the working directory, and, when no
358 revisions are specified, the working directory files are compared
367 revisions are specified, the working directory files are compared
359 to its parent.'''
368 to its parent.'''
360 opts = pycompat.byteskwargs(opts)
369 opts = pycompat.byteskwargs(opts)
361 program = opts.get('program')
370 program = opts.get('program')
362 option = opts.get('option')
371 option = opts.get('option')
363 if not program:
372 if not program:
364 program = 'diff'
373 program = 'diff'
365 option = option or ['-Npru']
374 option = option or ['-Npru']
366 cmdline = ' '.join(map(procutil.shellquote, [program] + option))
375 cmdline = ' '.join(map(procutil.shellquote, [program] + option))
367 return dodiff(ui, repo, cmdline, pats, opts)
376 return dodiff(ui, repo, cmdline, pats, opts)
368
377
369 class savedcmd(object):
378 class savedcmd(object):
370 """use external program to diff repository (or selected files)
379 """use external program to diff repository (or selected files)
371
380
372 Show differences between revisions for the specified files, using
381 Show differences between revisions for the specified files, using
373 the following program::
382 the following program::
374
383
375 %(path)s
384 %(path)s
376
385
377 When two revision arguments are given, then changes are shown
386 When two revision arguments are given, then changes are shown
378 between those revisions. If only one revision is specified then
387 between those revisions. If only one revision is specified then
379 that revision is compared to the working directory, and, when no
388 that revision is compared to the working directory, and, when no
380 revisions are specified, the working directory files are compared
389 revisions are specified, the working directory files are compared
381 to its parent.
390 to its parent.
382 """
391 """
383
392
384 def __init__(self, path, cmdline):
393 def __init__(self, path, cmdline):
385 # We can't pass non-ASCII through docstrings (and path is
394 # We can't pass non-ASCII through docstrings (and path is
386 # in an unknown encoding anyway), but avoid double separators on
395 # in an unknown encoding anyway), but avoid double separators on
387 # Windows
396 # Windows
388 docpath = stringutil.escapestr(path).replace(b'\\\\', b'\\')
397 docpath = stringutil.escapestr(path).replace(b'\\\\', b'\\')
389 self.__doc__ %= {r'path': pycompat.sysstr(stringutil.uirepr(docpath))}
398 self.__doc__ %= {r'path': pycompat.sysstr(stringutil.uirepr(docpath))}
390 self._cmdline = cmdline
399 self._cmdline = cmdline
391
400
392 def __call__(self, ui, repo, *pats, **opts):
401 def __call__(self, ui, repo, *pats, **opts):
393 opts = pycompat.byteskwargs(opts)
402 opts = pycompat.byteskwargs(opts)
394 options = ' '.join(map(procutil.shellquote, opts['option']))
403 options = ' '.join(map(procutil.shellquote, opts['option']))
395 if options:
404 if options:
396 options = ' ' + options
405 options = ' ' + options
397 return dodiff(ui, repo, self._cmdline + options, pats, opts)
406 return dodiff(ui, repo, self._cmdline + options, pats, opts)
398
407
399 def uisetup(ui):
408 def uisetup(ui):
400 for cmd, path in ui.configitems('extdiff'):
409 for cmd, path in ui.configitems('extdiff'):
401 path = util.expandpath(path)
410 path = util.expandpath(path)
402 if cmd.startswith('cmd.'):
411 if cmd.startswith('cmd.'):
403 cmd = cmd[4:]
412 cmd = cmd[4:]
404 if not path:
413 if not path:
405 path = procutil.findexe(cmd)
414 path = procutil.findexe(cmd)
406 if path is None:
415 if path is None:
407 path = filemerge.findexternaltool(ui, cmd) or cmd
416 path = filemerge.findexternaltool(ui, cmd) or cmd
408 diffopts = ui.config('extdiff', 'opts.' + cmd)
417 diffopts = ui.config('extdiff', 'opts.' + cmd)
409 cmdline = procutil.shellquote(path)
418 cmdline = procutil.shellquote(path)
410 if diffopts:
419 if diffopts:
411 cmdline += ' ' + diffopts
420 cmdline += ' ' + diffopts
412 elif cmd.startswith('opts.'):
421 elif cmd.startswith('opts.'):
413 continue
422 continue
414 else:
423 else:
415 if path:
424 if path:
416 # case "cmd = path opts"
425 # case "cmd = path opts"
417 cmdline = path
426 cmdline = path
418 diffopts = len(pycompat.shlexsplit(cmdline)) > 1
427 diffopts = len(pycompat.shlexsplit(cmdline)) > 1
419 else:
428 else:
420 # case "cmd ="
429 # case "cmd ="
421 path = procutil.findexe(cmd)
430 path = procutil.findexe(cmd)
422 if path is None:
431 if path is None:
423 path = filemerge.findexternaltool(ui, cmd) or cmd
432 path = filemerge.findexternaltool(ui, cmd) or cmd
424 cmdline = procutil.shellquote(path)
433 cmdline = procutil.shellquote(path)
425 diffopts = False
434 diffopts = False
426 # look for diff arguments in [diff-tools] then [merge-tools]
435 # look for diff arguments in [diff-tools] then [merge-tools]
427 if not diffopts:
436 if not diffopts:
428 args = ui.config('diff-tools', cmd+'.diffargs') or \
437 args = ui.config('diff-tools', cmd+'.diffargs') or \
429 ui.config('merge-tools', cmd+'.diffargs')
438 ui.config('merge-tools', cmd+'.diffargs')
430 if args:
439 if args:
431 cmdline += ' ' + args
440 cmdline += ' ' + args
432 command(cmd, extdiffopts[:], _('hg %s [OPTION]... [FILE]...') % cmd,
441 command(cmd, extdiffopts[:], _('hg %s [OPTION]... [FILE]...') % cmd,
433 helpcategory=command.CATEGORY_FILE_CONTENTS,
442 helpcategory=command.CATEGORY_FILE_CONTENTS,
434 inferrepo=True)(savedcmd(path, cmdline))
443 inferrepo=True)(savedcmd(path, cmdline))
435
444
436 # tell hggettext to extract docstrings from these functions:
445 # tell hggettext to extract docstrings from these functions:
437 i18nfunctions = [savedcmd]
446 i18nfunctions = [savedcmd]
General Comments 0
You need to be logged in to leave comments. Login now