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