##// END OF EJS Templates
extdiff: quote user-supplied options passed to shell...
Michael Fyles -
r23138:72a89cf8 stable
parent child Browse files
Show More
@@ -1,328 +1,327 b''
1 1 # extdiff.py - external diff program support for mercurial
2 2 #
3 3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 '''command to allow external programs to compare revisions
9 9
10 10 The extdiff Mercurial extension allows you to use external programs
11 11 to compare revisions, or revision with working directory. The external
12 12 diff programs are called with a configurable set of options and two
13 13 non-option arguments: paths to directories containing snapshots of
14 14 files to compare.
15 15
16 16 The extdiff extension also allows you to configure new diff commands, so
17 17 you do not need to type :hg:`extdiff -p kdiff3` always. ::
18 18
19 19 [extdiff]
20 20 # add new command that runs GNU diff(1) in 'context diff' mode
21 21 cdiff = gdiff -Nprc5
22 22 ## or the old way:
23 23 #cmd.cdiff = gdiff
24 24 #opts.cdiff = -Nprc5
25 25
26 26 # add new command called vdiff, runs kdiff3
27 27 vdiff = kdiff3
28 28
29 29 # add new command called meld, runs meld (no need to name twice)
30 30 meld =
31 31
32 32 # add new command called vimdiff, runs gvimdiff with DirDiff plugin
33 33 # (see http://www.vim.org/scripts/script.php?script_id=102) Non
34 34 # English user, be sure to put "let g:DirDiffDynamicDiffText = 1" in
35 35 # your .vimrc
36 36 vimdiff = gvim -f "+next" \\
37 37 "+execute 'DirDiff' fnameescape(argv(0)) fnameescape(argv(1))"
38 38
39 39 Tool arguments can include variables that are expanded at runtime::
40 40
41 41 $parent1, $plabel1 - filename, descriptive label of first parent
42 42 $child, $clabel - filename, descriptive label of child revision
43 43 $parent2, $plabel2 - filename, descriptive label of second parent
44 44 $root - repository root
45 45 $parent is an alias for $parent1.
46 46
47 47 The extdiff extension will look in your [diff-tools] and [merge-tools]
48 48 sections for diff tool arguments, when none are specified in [extdiff].
49 49
50 50 ::
51 51
52 52 [extdiff]
53 53 kdiff3 =
54 54
55 55 [diff-tools]
56 56 kdiff3.diffargs=--L1 '$plabel1' --L2 '$clabel' $parent $child
57 57
58 58 You can use -I/-X and list of file or directory names like normal
59 59 :hg:`diff` command. The extdiff extension makes snapshots of only
60 60 needed files, so running the external diff program will actually be
61 61 pretty fast (at least faster than having to compare the entire tree).
62 62 '''
63 63
64 64 from mercurial.i18n import _
65 65 from mercurial.node import short, nullid
66 66 from mercurial import cmdutil, scmutil, util, commands, encoding
67 67 import os, shlex, shutil, tempfile, re
68 68
69 69 cmdtable = {}
70 70 command = cmdutil.command(cmdtable)
71 71 testedwith = 'internal'
72 72
73 73 def snapshot(ui, repo, files, node, tmproot):
74 74 '''snapshot files as of some revision
75 75 if not using snapshot, -I/-X does not work and recursive diff
76 76 in tools like kdiff3 and meld displays too many files.'''
77 77 dirname = os.path.basename(repo.root)
78 78 if dirname == "":
79 79 dirname = "root"
80 80 if node is not None:
81 81 dirname = '%s.%s' % (dirname, short(node))
82 82 base = os.path.join(tmproot, dirname)
83 83 os.mkdir(base)
84 84 if node is not None:
85 85 ui.note(_('making snapshot of %d files from rev %s\n') %
86 86 (len(files), short(node)))
87 87 else:
88 88 ui.note(_('making snapshot of %d files from working directory\n') %
89 89 (len(files)))
90 90 wopener = scmutil.opener(base)
91 91 fns_and_mtime = []
92 92 ctx = repo[node]
93 93 for fn in files:
94 94 wfn = util.pconvert(fn)
95 95 if wfn not in ctx:
96 96 # File doesn't exist; could be a bogus modify
97 97 continue
98 98 ui.note(' %s\n' % wfn)
99 99 dest = os.path.join(base, wfn)
100 100 fctx = ctx[wfn]
101 101 data = repo.wwritedata(wfn, fctx.data())
102 102 if 'l' in fctx.flags():
103 103 wopener.symlink(data, wfn)
104 104 else:
105 105 wopener.write(wfn, data)
106 106 if 'x' in fctx.flags():
107 107 util.setflags(dest, False, True)
108 108 if node is None:
109 109 fns_and_mtime.append((dest, repo.wjoin(fn),
110 110 os.lstat(dest).st_mtime))
111 111 return dirname, fns_and_mtime
112 112
113 113 def dodiff(ui, repo, diffcmd, diffopts, pats, opts):
114 114 '''Do the actual diff:
115 115
116 116 - copy to a temp structure if diffing 2 internal revisions
117 117 - copy to a temp structure if diffing working revision with
118 118 another one and more than 1 file is changed
119 119 - just invoke the diff for a single file in the working dir
120 120 '''
121 121
122 122 revs = opts.get('rev')
123 123 change = opts.get('change')
124 args = ' '.join(diffopts)
124 args = ' '.join(map(util.shellquote, diffopts))
125 125 do3way = '$parent2' in args
126 126
127 127 if revs and change:
128 128 msg = _('cannot specify --rev and --change at the same time')
129 129 raise util.Abort(msg)
130 130 elif change:
131 131 node2 = scmutil.revsingle(repo, change, None).node()
132 132 node1a, node1b = repo.changelog.parents(node2)
133 133 else:
134 134 node1a, node2 = scmutil.revpair(repo, revs)
135 135 if not revs:
136 136 node1b = repo.dirstate.p2()
137 137 else:
138 138 node1b = nullid
139 139
140 140 # Disable 3-way merge if there is only one parent
141 141 if do3way:
142 142 if node1b == nullid:
143 143 do3way = False
144 144
145 145 matcher = scmutil.match(repo[node2], pats, opts)
146 146 mod_a, add_a, rem_a = map(set, repo.status(node1a, node2, matcher)[:3])
147 147 if do3way:
148 148 mod_b, add_b, rem_b = map(set, repo.status(node1b, node2, matcher)[:3])
149 149 else:
150 150 mod_b, add_b, rem_b = set(), set(), set()
151 151 modadd = mod_a | add_a | mod_b | add_b
152 152 common = modadd | rem_a | rem_b
153 153 if not common:
154 154 return 0
155 155
156 156 tmproot = tempfile.mkdtemp(prefix='extdiff.')
157 157 try:
158 158 # Always make a copy of node1a (and node1b, if applicable)
159 159 dir1a_files = mod_a | rem_a | ((mod_b | add_b) - add_a)
160 160 dir1a = snapshot(ui, repo, dir1a_files, node1a, tmproot)[0]
161 161 rev1a = '@%d' % repo[node1a].rev()
162 162 if do3way:
163 163 dir1b_files = mod_b | rem_b | ((mod_a | add_a) - add_b)
164 164 dir1b = snapshot(ui, repo, dir1b_files, node1b, tmproot)[0]
165 165 rev1b = '@%d' % repo[node1b].rev()
166 166 else:
167 167 dir1b = None
168 168 rev1b = ''
169 169
170 170 fns_and_mtime = []
171 171
172 172 # If node2 in not the wc or there is >1 change, copy it
173 173 dir2root = ''
174 174 rev2 = ''
175 175 if node2:
176 176 dir2 = snapshot(ui, repo, modadd, node2, tmproot)[0]
177 177 rev2 = '@%d' % repo[node2].rev()
178 178 elif len(common) > 1:
179 179 #we only actually need to get the files to copy back to
180 180 #the working dir in this case (because the other cases
181 181 #are: diffing 2 revisions or single file -- in which case
182 182 #the file is already directly passed to the diff tool).
183 183 dir2, fns_and_mtime = snapshot(ui, repo, modadd, None, tmproot)
184 184 else:
185 185 # This lets the diff tool open the changed file directly
186 186 dir2 = ''
187 187 dir2root = repo.root
188 188
189 189 label1a = rev1a
190 190 label1b = rev1b
191 191 label2 = rev2
192 192
193 193 # If only one change, diff the files instead of the directories
194 194 # Handle bogus modifies correctly by checking if the files exist
195 195 if len(common) == 1:
196 196 common_file = util.localpath(common.pop())
197 197 dir1a = os.path.join(tmproot, dir1a, common_file)
198 198 label1a = common_file + rev1a
199 199 if not os.path.isfile(dir1a):
200 200 dir1a = os.devnull
201 201 if do3way:
202 202 dir1b = os.path.join(tmproot, dir1b, common_file)
203 203 label1b = common_file + rev1b
204 204 if not os.path.isfile(dir1b):
205 205 dir1b = os.devnull
206 206 dir2 = os.path.join(dir2root, dir2, common_file)
207 207 label2 = common_file + rev2
208 208
209 209 # Function to quote file/dir names in the argument string.
210 210 # When not operating in 3-way mode, an empty string is
211 211 # returned for parent2
212 212 replace = {'parent': dir1a, 'parent1': dir1a, 'parent2': dir1b,
213 213 'plabel1': label1a, 'plabel2': label1b,
214 214 'clabel': label2, 'child': dir2,
215 215 'root': repo.root}
216 216 def quote(match):
217 217 key = match.group()[1:]
218 218 if not do3way and key == 'parent2':
219 219 return ''
220 220 return util.shellquote(replace[key])
221 221
222 222 # Match parent2 first, so 'parent1?' will match both parent1 and parent
223 223 regex = '\$(parent2|parent1?|child|plabel1|plabel2|clabel|root)'
224 224 if not do3way and not re.search(regex, args):
225 225 args += ' $parent1 $child'
226 226 args = re.sub(regex, quote, args)
227 227 cmdline = util.shellquote(diffcmd) + ' ' + args
228 228
229 229 ui.debug('running %r in %s\n' % (cmdline, tmproot))
230 230 util.system(cmdline, cwd=tmproot, out=ui.fout)
231 231
232 232 for copy_fn, working_fn, mtime in fns_and_mtime:
233 233 if os.lstat(copy_fn).st_mtime != mtime:
234 234 ui.debug('file changed while diffing. '
235 235 'Overwriting: %s (src: %s)\n' % (working_fn, copy_fn))
236 236 util.copyfile(copy_fn, working_fn)
237 237
238 238 return 1
239 239 finally:
240 240 ui.note(_('cleaning up temp directory\n'))
241 241 shutil.rmtree(tmproot)
242 242
243 243 @command('extdiff',
244 244 [('p', 'program', '',
245 245 _('comparison program to run'), _('CMD')),
246 246 ('o', 'option', [],
247 247 _('pass option to comparison program'), _('OPT')),
248 248 ('r', 'rev', [], _('revision'), _('REV')),
249 249 ('c', 'change', '', _('change made by revision'), _('REV')),
250 250 ] + commands.walkopts,
251 251 _('hg extdiff [OPT]... [FILE]...'),
252 252 inferrepo=True)
253 253 def extdiff(ui, repo, *pats, **opts):
254 254 '''use external program to diff repository (or selected files)
255 255
256 256 Show differences between revisions for the specified files, using
257 257 an external program. The default program used is diff, with
258 258 default options "-Npru".
259 259
260 260 To select a different program, use the -p/--program option. The
261 261 program will be passed the names of two directories to compare. To
262 262 pass additional options to the program, use -o/--option. These
263 263 will be passed before the names of the directories to compare.
264 264
265 265 When two revision arguments are given, then changes are shown
266 266 between those revisions. If only one revision is specified then
267 267 that revision is compared to the working directory, and, when no
268 268 revisions are specified, the working directory files are compared
269 269 to its parent.'''
270 270 program = opts.get('program')
271 271 option = opts.get('option')
272 272 if not program:
273 273 program = 'diff'
274 274 option = option or ['-Npru']
275 275 return dodiff(ui, repo, program, option, pats, opts)
276 276
277 277 def uisetup(ui):
278 278 for cmd, path in ui.configitems('extdiff'):
279 279 if cmd.startswith('cmd.'):
280 280 cmd = cmd[4:]
281 281 if not path:
282 282 path = cmd
283 diffopts = ui.config('extdiff', 'opts.' + cmd, '')
284 diffopts = diffopts and [diffopts] or []
283 diffopts = shlex.split(ui.config('extdiff', 'opts.' + cmd, ''))
285 284 elif cmd.startswith('opts.'):
286 285 continue
287 286 else:
288 287 # command = path opts
289 288 if path:
290 289 diffopts = shlex.split(path)
291 290 path = diffopts.pop(0)
292 291 else:
293 292 path, diffopts = cmd, []
294 293 # look for diff arguments in [diff-tools] then [merge-tools]
295 294 if diffopts == []:
296 295 args = ui.config('diff-tools', cmd+'.diffargs') or \
297 296 ui.config('merge-tools', cmd+'.diffargs')
298 297 if args:
299 298 diffopts = shlex.split(args)
300 299 def save(cmd, path, diffopts):
301 300 '''use closure to save diff command to use'''
302 301 def mydiff(ui, repo, *pats, **opts):
303 302 return dodiff(ui, repo, path, diffopts + opts['option'],
304 303 pats, opts)
305 304 doc = _('''\
306 305 use %(path)s to diff repository (or selected files)
307 306
308 307 Show differences between revisions for the specified files, using
309 308 the %(path)s program.
310 309
311 310 When two revision arguments are given, then changes are shown
312 311 between those revisions. If only one revision is specified then
313 312 that revision is compared to the working directory, and, when no
314 313 revisions are specified, the working directory files are compared
315 314 to its parent.\
316 315 ''') % {'path': util.uirepr(path)}
317 316
318 317 # We must translate the docstring right away since it is
319 318 # used as a format string. The string will unfortunately
320 319 # be translated again in commands.helpcmd and this will
321 320 # fail when the docstring contains non-ASCII characters.
322 321 # Decoding the string to a Unicode string here (using the
323 322 # right encoding) prevents that.
324 323 mydiff.__doc__ = doc.decode(encoding.encoding)
325 324 return mydiff
326 325 cmdtable[cmd] = (save(cmd, path, diffopts),
327 326 cmdtable['extdiff'][1][1:],
328 327 _('hg %s [OPTION]... [FILE]...') % cmd)
@@ -1,202 +1,214 b''
1 1 $ echo "[extensions]" >> $HGRCPATH
2 2 $ echo "extdiff=" >> $HGRCPATH
3 3
4 4 $ hg init a
5 5 $ cd a
6 6 $ echo a > a
7 7 $ echo b > b
8 8 $ hg add
9 9 adding a
10 10 adding b
11 11
12 12 Should diff cloned directories:
13 13
14 14 $ hg extdiff -o -r $opt
15 15 Only in a: a
16 16 Only in a: b
17 17 [1]
18 18
19 19 $ echo "[extdiff]" >> $HGRCPATH
20 20 $ echo "cmd.falabala=echo" >> $HGRCPATH
21 21 $ echo "opts.falabala=diffing" >> $HGRCPATH
22 $ echo "cmd.edspace=echo" >> $HGRCPATH
23 $ echo 'opts.edspace="name <user@example.com>"' >> $HGRCPATH
22 24
23 25 $ hg falabala
24 26 diffing a.000000000000 a
25 27 [1]
26 28
27 29 $ hg help falabala
28 30 hg falabala [OPTION]... [FILE]...
29 31
30 32 use 'echo' to diff repository (or selected files)
31 33
32 34 Show differences between revisions for the specified files, using the
33 35 'echo' program.
34 36
35 37 When two revision arguments are given, then changes are shown between
36 38 those revisions. If only one revision is specified then that revision is
37 39 compared to the working directory, and, when no revisions are specified,
38 40 the working directory files are compared to its parent.
39 41
40 42 options ([+] can be repeated):
41 43
42 44 -o --option OPT [+] pass option to comparison program
43 45 -r --rev REV [+] revision
44 46 -c --change REV change made by revision
45 47 -I --include PATTERN [+] include names matching the given patterns
46 48 -X --exclude PATTERN [+] exclude names matching the given patterns
47 49
48 50 (some details hidden, use --verbose to show complete help)
49 51
50 52 $ hg ci -d '0 0' -mtest1
51 53
52 54 $ echo b >> a
53 55 $ hg ci -d '1 0' -mtest2
54 56
55 57 Should diff cloned files directly:
56 58
57 59 $ hg falabala -r 0:1
58 60 diffing */extdiff.*/a.8a5febb7f867/a a.34eed99112ab/a (glob)
59 61 [1]
60 62
61 63 Test diff during merge:
62 64
63 65 $ hg update -C 0
64 66 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
65 67 $ echo c >> c
66 68 $ hg add c
67 69 $ hg ci -m "new branch" -d '1 0'
68 70 created new head
69 71 $ hg merge 1
70 72 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
71 73 (branch merge, don't forget to commit)
72 74
73 75 Should diff cloned file against wc file:
74 76
75 77 $ hg falabala
76 78 diffing */extdiff.*/a.2a13a4d2da36/a */a/a (glob)
77 79 [1]
78 80
79 81
80 82 Test --change option:
81 83
82 84 $ hg ci -d '2 0' -mtest3
83 85 $ hg falabala -c 1
84 86 diffing */extdiff.*/a.8a5febb7f867/a a.34eed99112ab/a (glob)
85 87 [1]
86 88
87 89 Check diff are made from the first parent:
88 90
89 91 $ hg falabala -c 3 || echo "diff-like tools yield a non-zero exit code"
90 92 diffing */extdiff.*/a.2a13a4d2da36/a a.46c0e4daeb72/a (glob)
91 93 diff-like tools yield a non-zero exit code
92 94
93 95 #if execbit
94 96
95 97 Test extdiff of multiple files in tmp dir:
96 98
97 99 $ hg update -C 0 > /dev/null
98 100 $ echo changed > a
99 101 $ echo changed > b
100 102 $ chmod +x b
101 103
102 104 Diff in working directory, before:
103 105
104 106 $ hg diff --git
105 107 diff --git a/a b/a
106 108 --- a/a
107 109 +++ b/a
108 110 @@ -1,1 +1,1 @@
109 111 -a
110 112 +changed
111 113 diff --git a/b b/b
112 114 old mode 100644
113 115 new mode 100755
114 116 --- a/b
115 117 +++ b/b
116 118 @@ -1,1 +1,1 @@
117 119 -b
118 120 +changed
119 121
120 122
121 123 Edit with extdiff -p:
122 124
123 125 Prepare custom diff/edit tool:
124 126
125 127 $ cat > 'diff tool.py' << EOT
126 128 > #!/usr/bin/env python
127 129 > import time
128 130 > time.sleep(1) # avoid unchanged-timestamp problems
129 131 > file('a/a', 'ab').write('edited\n')
130 132 > file('a/b', 'ab').write('edited\n')
131 133 > EOT
132 134
133 135 $ chmod +x 'diff tool.py'
134 136
135 137 will change to /tmp/extdiff.TMP and populate directories a.TMP and a
136 138 and start tool
137 139
138 140 $ hg extdiff -p "`pwd`/diff tool.py"
139 141 [1]
140 142
141 143 Diff in working directory, after:
142 144
143 145 $ hg diff --git
144 146 diff --git a/a b/a
145 147 --- a/a
146 148 +++ b/a
147 149 @@ -1,1 +1,2 @@
148 150 -a
149 151 +changed
150 152 +edited
151 153 diff --git a/b b/b
152 154 old mode 100644
153 155 new mode 100755
154 156 --- a/b
155 157 +++ b/b
156 158 @@ -1,1 +1,2 @@
157 159 -b
158 160 +changed
159 161 +edited
160 162
161 163 Test extdiff with --option:
162 164
163 165 $ hg extdiff -p echo -o this -c 1
164 166 this */extdiff.*/a.8a5febb7f867/a a.34eed99112ab/a (glob)
165 167 [1]
166 168
167 169 $ hg falabala -o this -c 1
168 170 diffing this */extdiff.*/a.8a5febb7f867/a a.34eed99112ab/a (glob)
169 171 [1]
170 172
173 Test extdiff's handling of options with spaces in them:
174
175 $ hg edspace -c 1
176 name <user@example.com> */extdiff.*/a.8a5febb7f867/a a.34eed99112ab/a (glob)
177 [1]
178
179 $ hg extdiff -p echo -o "name <user@example.com>" -c 1
180 name <user@example.com> */extdiff.*/a.8a5febb7f867/a a.34eed99112ab/a (glob)
181 [1]
182
171 183 Test with revsets:
172 184
173 185 $ hg extdif -p echo -c "rev(1)"
174 186 */extdiff.*/a.8a5febb7f867/a a.34eed99112ab/a (glob)
175 187 [1]
176 188
177 189 $ hg extdif -p echo -r "0::1"
178 190 */extdiff.*/a.8a5febb7f867/a a.34eed99112ab/a (glob)
179 191 [1]
180 192
181 193 $ cd ..
182 194
183 195 #endif
184 196
185 197 #if symlink
186 198
187 199 Test symlinks handling (issue1909)
188 200
189 201 $ hg init testsymlinks
190 202 $ cd testsymlinks
191 203 $ echo a > a
192 204 $ hg ci -Am adda
193 205 adding a
194 206 $ echo a >> a
195 207 $ ln -s missing linka
196 208 $ hg add linka
197 209 $ hg falabala -r 0 --traceback
198 210 diffing testsymlinks.07f494440405 testsymlinks
199 211 [1]
200 212 $ cd ..
201 213
202 214 #endif
General Comments 0
You need to be logged in to leave comments. Login now