##// END OF EJS Templates
extdiff: register the configuration generated commands with a help category...
Matt Harbison -
r40837:97190b0b stable
parent child Browse files
Show More
@@ -1,435 +1,436 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 matchfn=scmutil.matchfiles(repo, files),
142 matchfn=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 dodiff(ui, repo, cmdline, pats, opts):
155 def dodiff(ui, repo, cmdline, pats, opts):
156 '''Do the actual diff:
156 '''Do the actual diff:
157
157
158 - copy to a temp structure if diffing 2 internal revisions
158 - copy to a temp structure if diffing 2 internal revisions
159 - copy to a temp structure if diffing working revision with
159 - copy to a temp structure if diffing working revision with
160 another one and more than 1 file is changed
160 another one and more than 1 file is changed
161 - just invoke the diff for a single file in the working dir
161 - just invoke the diff for a single file in the working dir
162 '''
162 '''
163
163
164 revs = opts.get('rev')
164 revs = opts.get('rev')
165 change = opts.get('change')
165 change = opts.get('change')
166 do3way = '$parent2' in cmdline
166 do3way = '$parent2' in cmdline
167
167
168 if revs and change:
168 if revs and change:
169 msg = _('cannot specify --rev and --change at the same time')
169 msg = _('cannot specify --rev and --change at the same time')
170 raise error.Abort(msg)
170 raise error.Abort(msg)
171 elif change:
171 elif change:
172 ctx2 = scmutil.revsingle(repo, change, None)
172 ctx2 = scmutil.revsingle(repo, change, None)
173 ctx1a, ctx1b = ctx2.p1(), ctx2.p2()
173 ctx1a, ctx1b = ctx2.p1(), ctx2.p2()
174 else:
174 else:
175 ctx1a, ctx2 = scmutil.revpair(repo, revs)
175 ctx1a, ctx2 = scmutil.revpair(repo, revs)
176 if not revs:
176 if not revs:
177 ctx1b = repo[None].p2()
177 ctx1b = repo[None].p2()
178 else:
178 else:
179 ctx1b = repo[nullid]
179 ctx1b = repo[nullid]
180
180
181 node1a = ctx1a.node()
181 node1a = ctx1a.node()
182 node1b = ctx1b.node()
182 node1b = ctx1b.node()
183 node2 = ctx2.node()
183 node2 = ctx2.node()
184
184
185 # Disable 3-way merge if there is only one parent
185 # Disable 3-way merge if there is only one parent
186 if do3way:
186 if do3way:
187 if node1b == nullid:
187 if node1b == nullid:
188 do3way = False
188 do3way = False
189
189
190 subrepos=opts.get('subrepos')
190 subrepos=opts.get('subrepos')
191
191
192 matcher = scmutil.match(repo[node2], pats, opts)
192 matcher = scmutil.match(repo[node2], pats, opts)
193
193
194 if opts.get('patch'):
194 if opts.get('patch'):
195 if subrepos:
195 if subrepos:
196 raise error.Abort(_('--patch cannot be used with --subrepos'))
196 raise error.Abort(_('--patch cannot be used with --subrepos'))
197 if node2 is None:
197 if node2 is None:
198 raise error.Abort(_('--patch requires two revisions'))
198 raise error.Abort(_('--patch requires two revisions'))
199 else:
199 else:
200 mod_a, add_a, rem_a = map(set, repo.status(node1a, node2, matcher,
200 mod_a, add_a, rem_a = map(set, repo.status(node1a, node2, matcher,
201 listsubrepos=subrepos)[:3])
201 listsubrepos=subrepos)[:3])
202 if do3way:
202 if do3way:
203 mod_b, add_b, rem_b = map(set,
203 mod_b, add_b, rem_b = map(set,
204 repo.status(node1b, node2, matcher,
204 repo.status(node1b, node2, matcher,
205 listsubrepos=subrepos)[:3])
205 listsubrepos=subrepos)[:3])
206 else:
206 else:
207 mod_b, add_b, rem_b = set(), set(), set()
207 mod_b, add_b, rem_b = set(), set(), set()
208 modadd = mod_a | add_a | mod_b | add_b
208 modadd = mod_a | add_a | mod_b | add_b
209 common = modadd | rem_a | rem_b
209 common = modadd | rem_a | rem_b
210 if not common:
210 if not common:
211 return 0
211 return 0
212
212
213 tmproot = pycompat.mkdtemp(prefix='extdiff.')
213 tmproot = pycompat.mkdtemp(prefix='extdiff.')
214 try:
214 try:
215 if not opts.get('patch'):
215 if not opts.get('patch'):
216 # Always make a copy of node1a (and node1b, if applicable)
216 # Always make a copy of node1a (and node1b, if applicable)
217 dir1a_files = mod_a | rem_a | ((mod_b | add_b) - add_a)
217 dir1a_files = mod_a | rem_a | ((mod_b | add_b) - add_a)
218 dir1a = snapshot(ui, repo, dir1a_files, node1a, tmproot,
218 dir1a = snapshot(ui, repo, dir1a_files, node1a, tmproot,
219 subrepos)[0]
219 subrepos)[0]
220 rev1a = '@%d' % repo[node1a].rev()
220 rev1a = '@%d' % repo[node1a].rev()
221 if do3way:
221 if do3way:
222 dir1b_files = mod_b | rem_b | ((mod_a | add_a) - add_b)
222 dir1b_files = mod_b | rem_b | ((mod_a | add_a) - add_b)
223 dir1b = snapshot(ui, repo, dir1b_files, node1b, tmproot,
223 dir1b = snapshot(ui, repo, dir1b_files, node1b, tmproot,
224 subrepos)[0]
224 subrepos)[0]
225 rev1b = '@%d' % repo[node1b].rev()
225 rev1b = '@%d' % repo[node1b].rev()
226 else:
226 else:
227 dir1b = None
227 dir1b = None
228 rev1b = ''
228 rev1b = ''
229
229
230 fnsandstat = []
230 fnsandstat = []
231
231
232 # If node2 in not the wc or there is >1 change, copy it
232 # If node2 in not the wc or there is >1 change, copy it
233 dir2root = ''
233 dir2root = ''
234 rev2 = ''
234 rev2 = ''
235 if node2:
235 if node2:
236 dir2 = snapshot(ui, repo, modadd, node2, tmproot, subrepos)[0]
236 dir2 = snapshot(ui, repo, modadd, node2, tmproot, subrepos)[0]
237 rev2 = '@%d' % repo[node2].rev()
237 rev2 = '@%d' % repo[node2].rev()
238 elif len(common) > 1:
238 elif len(common) > 1:
239 #we only actually need to get the files to copy back to
239 #we only actually need to get the files to copy back to
240 #the working dir in this case (because the other cases
240 #the working dir in this case (because the other cases
241 #are: diffing 2 revisions or single file -- in which case
241 #are: diffing 2 revisions or single file -- in which case
242 #the file is already directly passed to the diff tool).
242 #the file is already directly passed to the diff tool).
243 dir2, fnsandstat = snapshot(ui, repo, modadd, None, tmproot,
243 dir2, fnsandstat = snapshot(ui, repo, modadd, None, tmproot,
244 subrepos)
244 subrepos)
245 else:
245 else:
246 # This lets the diff tool open the changed file directly
246 # This lets the diff tool open the changed file directly
247 dir2 = ''
247 dir2 = ''
248 dir2root = repo.root
248 dir2root = repo.root
249
249
250 label1a = rev1a
250 label1a = rev1a
251 label1b = rev1b
251 label1b = rev1b
252 label2 = rev2
252 label2 = rev2
253
253
254 # If only one change, diff the files instead of the directories
254 # If only one change, diff the files instead of the directories
255 # Handle bogus modifies correctly by checking if the files exist
255 # Handle bogus modifies correctly by checking if the files exist
256 if len(common) == 1:
256 if len(common) == 1:
257 common_file = util.localpath(common.pop())
257 common_file = util.localpath(common.pop())
258 dir1a = os.path.join(tmproot, dir1a, common_file)
258 dir1a = os.path.join(tmproot, dir1a, common_file)
259 label1a = common_file + rev1a
259 label1a = common_file + rev1a
260 if not os.path.isfile(dir1a):
260 if not os.path.isfile(dir1a):
261 dir1a = os.devnull
261 dir1a = os.devnull
262 if do3way:
262 if do3way:
263 dir1b = os.path.join(tmproot, dir1b, common_file)
263 dir1b = os.path.join(tmproot, dir1b, common_file)
264 label1b = common_file + rev1b
264 label1b = common_file + rev1b
265 if not os.path.isfile(dir1b):
265 if not os.path.isfile(dir1b):
266 dir1b = os.devnull
266 dir1b = os.devnull
267 dir2 = os.path.join(dir2root, dir2, common_file)
267 dir2 = os.path.join(dir2root, dir2, common_file)
268 label2 = common_file + rev2
268 label2 = common_file + rev2
269 else:
269 else:
270 template = 'hg-%h.patch'
270 template = 'hg-%h.patch'
271 with formatter.nullformatter(ui, 'extdiff', {}) as fm:
271 with formatter.nullformatter(ui, 'extdiff', {}) as fm:
272 cmdutil.export(repo, [repo[node1a].rev(), repo[node2].rev()],
272 cmdutil.export(repo, [repo[node1a].rev(), repo[node2].rev()],
273 fm,
273 fm,
274 fntemplate=repo.vfs.reljoin(tmproot, template),
274 fntemplate=repo.vfs.reljoin(tmproot, template),
275 match=matcher)
275 match=matcher)
276 label1a = cmdutil.makefilename(repo[node1a], template)
276 label1a = cmdutil.makefilename(repo[node1a], template)
277 label2 = cmdutil.makefilename(repo[node2], template)
277 label2 = cmdutil.makefilename(repo[node2], template)
278 dir1a = repo.vfs.reljoin(tmproot, label1a)
278 dir1a = repo.vfs.reljoin(tmproot, label1a)
279 dir2 = repo.vfs.reljoin(tmproot, label2)
279 dir2 = repo.vfs.reljoin(tmproot, label2)
280 dir1b = None
280 dir1b = None
281 label1b = None
281 label1b = None
282 fnsandstat = []
282 fnsandstat = []
283
283
284 # Function to quote file/dir names in the argument string.
284 # Function to quote file/dir names in the argument string.
285 # When not operating in 3-way mode, an empty string is
285 # When not operating in 3-way mode, an empty string is
286 # returned for parent2
286 # returned for parent2
287 replace = {'parent': dir1a, 'parent1': dir1a, 'parent2': dir1b,
287 replace = {'parent': dir1a, 'parent1': dir1a, 'parent2': dir1b,
288 'plabel1': label1a, 'plabel2': label1b,
288 'plabel1': label1a, 'plabel2': label1b,
289 'clabel': label2, 'child': dir2,
289 'clabel': label2, 'child': dir2,
290 'root': repo.root}
290 'root': repo.root}
291 def quote(match):
291 def quote(match):
292 pre = match.group(2)
292 pre = match.group(2)
293 key = match.group(3)
293 key = match.group(3)
294 if not do3way and key == 'parent2':
294 if not do3way and key == 'parent2':
295 return pre
295 return pre
296 return pre + procutil.shellquote(replace[key])
296 return pre + procutil.shellquote(replace[key])
297
297
298 # Match parent2 first, so 'parent1?' will match both parent1 and parent
298 # Match parent2 first, so 'parent1?' will match both parent1 and parent
299 regex = (br'''(['"]?)([^\s'"$]*)'''
299 regex = (br'''(['"]?)([^\s'"$]*)'''
300 br'\$(parent2|parent1?|child|plabel1|plabel2|clabel|root)\1')
300 br'\$(parent2|parent1?|child|plabel1|plabel2|clabel|root)\1')
301 if not do3way and not re.search(regex, cmdline):
301 if not do3way and not re.search(regex, cmdline):
302 cmdline += ' $parent1 $child'
302 cmdline += ' $parent1 $child'
303 cmdline = re.sub(regex, quote, cmdline)
303 cmdline = re.sub(regex, quote, cmdline)
304
304
305 ui.debug('running %r in %s\n' % (pycompat.bytestr(cmdline), tmproot))
305 ui.debug('running %r in %s\n' % (pycompat.bytestr(cmdline), tmproot))
306 ui.system(cmdline, cwd=tmproot, blockedtag='extdiff')
306 ui.system(cmdline, cwd=tmproot, blockedtag='extdiff')
307
307
308 for copy_fn, working_fn, st in fnsandstat:
308 for copy_fn, working_fn, st in fnsandstat:
309 cpstat = os.lstat(copy_fn)
309 cpstat = os.lstat(copy_fn)
310 # Some tools copy the file and attributes, so mtime may not detect
310 # 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.
311 # 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,
312 # The only certain way to detect every case is to diff all files,
313 # which could be expensive.
313 # which could be expensive.
314 # copyfile() carries over the permission, so the mode check could
314 # copyfile() carries over the permission, so the mode check could
315 # be in an 'elif' branch, but for the case where the file has
315 # be in an 'elif' branch, but for the case where the file has
316 # changed without affecting mtime or size.
316 # changed without affecting mtime or size.
317 if (cpstat[stat.ST_MTIME] != st[stat.ST_MTIME]
317 if (cpstat[stat.ST_MTIME] != st[stat.ST_MTIME]
318 or cpstat.st_size != st.st_size
318 or cpstat.st_size != st.st_size
319 or (cpstat.st_mode & 0o100) != (st.st_mode & 0o100)):
319 or (cpstat.st_mode & 0o100) != (st.st_mode & 0o100)):
320 ui.debug('file changed while diffing. '
320 ui.debug('file changed while diffing. '
321 'Overwriting: %s (src: %s)\n' % (working_fn, copy_fn))
321 'Overwriting: %s (src: %s)\n' % (working_fn, copy_fn))
322 util.copyfile(copy_fn, working_fn)
322 util.copyfile(copy_fn, working_fn)
323
323
324 return 1
324 return 1
325 finally:
325 finally:
326 ui.note(_('cleaning up temp directory\n'))
326 ui.note(_('cleaning up temp directory\n'))
327 shutil.rmtree(tmproot)
327 shutil.rmtree(tmproot)
328
328
329 extdiffopts = [
329 extdiffopts = [
330 ('o', 'option', [],
330 ('o', 'option', [],
331 _('pass option to comparison program'), _('OPT')),
331 _('pass option to comparison program'), _('OPT')),
332 ('r', 'rev', [], _('revision'), _('REV')),
332 ('r', 'rev', [], _('revision'), _('REV')),
333 ('c', 'change', '', _('change made by revision'), _('REV')),
333 ('c', 'change', '', _('change made by revision'), _('REV')),
334 ('', 'patch', None, _('compare patches for two revisions'))
334 ('', 'patch', None, _('compare patches for two revisions'))
335 ] + cmdutil.walkopts + cmdutil.subrepoopts
335 ] + cmdutil.walkopts + cmdutil.subrepoopts
336
336
337 @command('extdiff',
337 @command('extdiff',
338 [('p', 'program', '', _('comparison program to run'), _('CMD')),
338 [('p', 'program', '', _('comparison program to run'), _('CMD')),
339 ] + extdiffopts,
339 ] + extdiffopts,
340 _('hg extdiff [OPT]... [FILE]...'),
340 _('hg extdiff [OPT]... [FILE]...'),
341 helpcategory=command.CATEGORY_FILE_CONTENTS,
341 helpcategory=command.CATEGORY_FILE_CONTENTS,
342 inferrepo=True)
342 inferrepo=True)
343 def extdiff(ui, repo, *pats, **opts):
343 def extdiff(ui, repo, *pats, **opts):
344 '''use external program to diff repository (or selected files)
344 '''use external program to diff repository (or selected files)
345
345
346 Show differences between revisions for the specified files, using
346 Show differences between revisions for the specified files, using
347 an external program. The default program used is diff, with
347 an external program. The default program used is diff, with
348 default options "-Npru".
348 default options "-Npru".
349
349
350 To select a different program, use the -p/--program option. The
350 To select a different program, use the -p/--program option. The
351 program will be passed the names of two directories to compare. To
351 program will be passed the names of two directories to compare. To
352 pass additional options to the program, use -o/--option. These
352 pass additional options to the program, use -o/--option. These
353 will be passed before the names of the directories to compare.
353 will be passed before the names of the directories to compare.
354
354
355 When two revision arguments are given, then changes are shown
355 When two revision arguments are given, then changes are shown
356 between those revisions. If only one revision is specified then
356 between those revisions. If only one revision is specified then
357 that revision is compared to the working directory, and, when no
357 that revision is compared to the working directory, and, when no
358 revisions are specified, the working directory files are compared
358 revisions are specified, the working directory files are compared
359 to its parent.'''
359 to its parent.'''
360 opts = pycompat.byteskwargs(opts)
360 opts = pycompat.byteskwargs(opts)
361 program = opts.get('program')
361 program = opts.get('program')
362 option = opts.get('option')
362 option = opts.get('option')
363 if not program:
363 if not program:
364 program = 'diff'
364 program = 'diff'
365 option = option or ['-Npru']
365 option = option or ['-Npru']
366 cmdline = ' '.join(map(procutil.shellquote, [program] + option))
366 cmdline = ' '.join(map(procutil.shellquote, [program] + option))
367 return dodiff(ui, repo, cmdline, pats, opts)
367 return dodiff(ui, repo, cmdline, pats, opts)
368
368
369 class savedcmd(object):
369 class savedcmd(object):
370 """use external program to diff repository (or selected files)
370 """use external program to diff repository (or selected files)
371
371
372 Show differences between revisions for the specified files, using
372 Show differences between revisions for the specified files, using
373 the following program::
373 the following program::
374
374
375 %(path)s
375 %(path)s
376
376
377 When two revision arguments are given, then changes are shown
377 When two revision arguments are given, then changes are shown
378 between those revisions. If only one revision is specified then
378 between those revisions. If only one revision is specified then
379 that revision is compared to the working directory, and, when no
379 that revision is compared to the working directory, and, when no
380 revisions are specified, the working directory files are compared
380 revisions are specified, the working directory files are compared
381 to its parent.
381 to its parent.
382 """
382 """
383
383
384 def __init__(self, path, cmdline):
384 def __init__(self, path, cmdline):
385 # We can't pass non-ASCII through docstrings (and path is
385 # We can't pass non-ASCII through docstrings (and path is
386 # in an unknown encoding anyway)
386 # in an unknown encoding anyway)
387 docpath = stringutil.escapestr(path)
387 docpath = stringutil.escapestr(path)
388 self.__doc__ %= {r'path': pycompat.sysstr(stringutil.uirepr(docpath))}
388 self.__doc__ %= {r'path': pycompat.sysstr(stringutil.uirepr(docpath))}
389 self._cmdline = cmdline
389 self._cmdline = cmdline
390
390
391 def __call__(self, ui, repo, *pats, **opts):
391 def __call__(self, ui, repo, *pats, **opts):
392 opts = pycompat.byteskwargs(opts)
392 opts = pycompat.byteskwargs(opts)
393 options = ' '.join(map(procutil.shellquote, opts['option']))
393 options = ' '.join(map(procutil.shellquote, opts['option']))
394 if options:
394 if options:
395 options = ' ' + options
395 options = ' ' + options
396 return dodiff(ui, repo, self._cmdline + options, pats, opts)
396 return dodiff(ui, repo, self._cmdline + options, pats, opts)
397
397
398 def uisetup(ui):
398 def uisetup(ui):
399 for cmd, path in ui.configitems('extdiff'):
399 for cmd, path in ui.configitems('extdiff'):
400 path = util.expandpath(path)
400 path = util.expandpath(path)
401 if cmd.startswith('cmd.'):
401 if cmd.startswith('cmd.'):
402 cmd = cmd[4:]
402 cmd = cmd[4:]
403 if not path:
403 if not path:
404 path = procutil.findexe(cmd)
404 path = procutil.findexe(cmd)
405 if path is None:
405 if path is None:
406 path = filemerge.findexternaltool(ui, cmd) or cmd
406 path = filemerge.findexternaltool(ui, cmd) or cmd
407 diffopts = ui.config('extdiff', 'opts.' + cmd)
407 diffopts = ui.config('extdiff', 'opts.' + cmd)
408 cmdline = procutil.shellquote(path)
408 cmdline = procutil.shellquote(path)
409 if diffopts:
409 if diffopts:
410 cmdline += ' ' + diffopts
410 cmdline += ' ' + diffopts
411 elif cmd.startswith('opts.'):
411 elif cmd.startswith('opts.'):
412 continue
412 continue
413 else:
413 else:
414 if path:
414 if path:
415 # case "cmd = path opts"
415 # case "cmd = path opts"
416 cmdline = path
416 cmdline = path
417 diffopts = len(pycompat.shlexsplit(cmdline)) > 1
417 diffopts = len(pycompat.shlexsplit(cmdline)) > 1
418 else:
418 else:
419 # case "cmd ="
419 # case "cmd ="
420 path = procutil.findexe(cmd)
420 path = procutil.findexe(cmd)
421 if path is None:
421 if path is None:
422 path = filemerge.findexternaltool(ui, cmd) or cmd
422 path = filemerge.findexternaltool(ui, cmd) or cmd
423 cmdline = procutil.shellquote(path)
423 cmdline = procutil.shellquote(path)
424 diffopts = False
424 diffopts = False
425 # look for diff arguments in [diff-tools] then [merge-tools]
425 # look for diff arguments in [diff-tools] then [merge-tools]
426 if not diffopts:
426 if not diffopts:
427 args = ui.config('diff-tools', cmd+'.diffargs') or \
427 args = ui.config('diff-tools', cmd+'.diffargs') or \
428 ui.config('merge-tools', cmd+'.diffargs')
428 ui.config('merge-tools', cmd+'.diffargs')
429 if args:
429 if args:
430 cmdline += ' ' + args
430 cmdline += ' ' + args
431 command(cmd, extdiffopts[:], _('hg %s [OPTION]... [FILE]...') % cmd,
431 command(cmd, extdiffopts[:], _('hg %s [OPTION]... [FILE]...') % cmd,
432 helpcategory=command.CATEGORY_FILE_CONTENTS,
432 inferrepo=True)(savedcmd(path, cmdline))
433 inferrepo=True)(savedcmd(path, cmdline))
433
434
434 # tell hggettext to extract docstrings from these functions:
435 # tell hggettext to extract docstrings from these functions:
435 i18nfunctions = [savedcmd]
436 i18nfunctions = [savedcmd]
General Comments 0
You need to be logged in to leave comments. Login now