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