comparison hgext/extdiff.py @ 41487:fa471151d269

extdiff: add --per-file and --confirm options The new options lets the user control how the external program is run. By default, Mercurial passes the 2 snapshot directories as usual, but it can also run the program repeatedly on each file's snapshot pair, and optionally prompt the user each time.
author Ludovic Chabant <ludovic@chabant.com>
date Tue, 29 Jan 2019 22:59:15 -0800
parents 4f675c12d083
children a4cd77a425a3
comparison
equal deleted inserted replaced
41486:f9150901267c 41487:fa471151d269
78 short, 78 short,
79 ) 79 )
80 from mercurial import ( 80 from mercurial import (
81 archival, 81 archival,
82 cmdutil, 82 cmdutil,
83 encoding,
83 error, 84 error,
84 filemerge, 85 filemerge,
85 formatter, 86 formatter,
86 pycompat, 87 pycompat,
87 registrar, 88 registrar,
173 br'\$(parent2|parent1?|child|plabel1|plabel2|clabel|root)\1') 174 br'\$(parent2|parent1?|child|plabel1|plabel2|clabel|root)\1')
174 if not do3way and not re.search(regex, cmdline): 175 if not do3way and not re.search(regex, cmdline):
175 cmdline += ' $parent1 $child' 176 cmdline += ' $parent1 $child'
176 return re.sub(regex, quote, cmdline) 177 return re.sub(regex, quote, cmdline)
177 178
179 def _runperfilediff(cmdline, repo_root, ui, do3way, confirm,
180 commonfiles, tmproot, dir1a, dir1b,
181 dir2root, dir2,
182 rev1a, rev1b, rev2):
183 # Note that we need to sort the list of files because it was
184 # built in an "unstable" way and it's annoying to get files in a
185 # random order, especially when "confirm" mode is enabled.
186 totalfiles = len(commonfiles)
187 for idx, commonfile in enumerate(sorted(commonfiles)):
188 path1a = os.path.join(tmproot, dir1a, commonfile)
189 label1a = commonfile + rev1a
190 if not os.path.isfile(path1a):
191 path1a = os.devnull
192
193 path1b = ''
194 label1b = ''
195 if do3way:
196 path1b = os.path.join(tmproot, dir1b, commonfile)
197 label1b = commonfile + rev1b
198 if not os.path.isfile(path1b):
199 path1b = os.devnull
200
201 path2 = os.path.join(dir2root, dir2, commonfile)
202 label2 = commonfile + rev2
203
204 if confirm:
205 # Prompt before showing this diff
206 difffiles = _('diff %s (%d of %d)') % (commonfile, idx + 1,
207 totalfiles)
208 responses = _('[Yns?]'
209 '$$ &Yes, show diff'
210 '$$ &No, skip this diff'
211 '$$ &Skip remaining diffs'
212 '$$ &? (display help)')
213 r = ui.promptchoice('%s %s' % (difffiles, responses))
214 if r == 3: # ?
215 while r == 3:
216 for c, t in ui.extractchoices(responses)[1]:
217 ui.write('%s - %s\n' % (c, encoding.lower(t)))
218 r = ui.promptchoice('%s %s' % (difffiles, responses))
219 if r == 0: # yes
220 pass
221 elif r == 1: # no
222 continue
223 elif r == 2: # skip
224 break
225
226 curcmdline = formatcmdline(
227 cmdline, repo_root, do3way=do3way,
228 parent1=path1a, plabel1=label1a,
229 parent2=path1b, plabel2=label1b,
230 child=path2, clabel=label2)
231 ui.debug('running %r in %s\n' % (pycompat.bytestr(curcmdline),
232 tmproot))
233
234 # Run the comparison program and wait for it to exit
235 # before we show the next file.
236 ui.system(curcmdline, cwd=tmproot, blockedtag='extdiff')
237
178 def dodiff(ui, repo, cmdline, pats, opts): 238 def dodiff(ui, repo, cmdline, pats, opts):
179 '''Do the actual diff: 239 '''Do the actual diff:
180 240
181 - copy to a temp structure if diffing 2 internal revisions 241 - copy to a temp structure if diffing 2 internal revisions
182 - copy to a temp structure if diffing working revision with 242 - copy to a temp structure if diffing working revision with
199 if not revs: 259 if not revs:
200 ctx1b = repo[None].p2() 260 ctx1b = repo[None].p2()
201 else: 261 else:
202 ctx1b = repo[nullid] 262 ctx1b = repo[nullid]
203 263
264 perfile = opts.get('per_file')
265 confirm = opts.get('confirm')
266
204 node1a = ctx1a.node() 267 node1a = ctx1a.node()
205 node1b = ctx1b.node() 268 node1b = ctx1b.node()
206 node2 = ctx2.node() 269 node2 = ctx2.node()
207 270
208 # Disable 3-way merge if there is only one parent 271 # Disable 3-way merge if there is only one parent
215 matcher = scmutil.match(repo[node2], pats, opts) 278 matcher = scmutil.match(repo[node2], pats, opts)
216 279
217 if opts.get('patch'): 280 if opts.get('patch'):
218 if subrepos: 281 if subrepos:
219 raise error.Abort(_('--patch cannot be used with --subrepos')) 282 raise error.Abort(_('--patch cannot be used with --subrepos'))
283 if perfile:
284 raise error.Abort(_('--patch cannot be used with --per-file'))
220 if node2 is None: 285 if node2 is None:
221 raise error.Abort(_('--patch requires two revisions')) 286 raise error.Abort(_('--patch requires two revisions'))
222 else: 287 else:
223 mod_a, add_a, rem_a = map(set, repo.status(node1a, node2, matcher, 288 mod_a, add_a, rem_a = map(set, repo.status(node1a, node2, matcher,
224 listsubrepos=subrepos)[:3]) 289 listsubrepos=subrepos)[:3])
302 dir2 = repo.vfs.reljoin(tmproot, label2) 367 dir2 = repo.vfs.reljoin(tmproot, label2)
303 dir1b = None 368 dir1b = None
304 label1b = None 369 label1b = None
305 fnsandstat = [] 370 fnsandstat = []
306 371
307 # Run the external tool on the 2 temp directories or the patches 372 if not perfile:
308 cmdline = formatcmdline( 373 # Run the external tool on the 2 temp directories or the patches
309 cmdline, repo.root, do3way=do3way, 374 cmdline = formatcmdline(
310 parent1=dir1a, plabel1=label1a, 375 cmdline, repo.root, do3way=do3way,
311 parent2=dir1b, plabel2=label1b, 376 parent1=dir1a, plabel1=label1a,
312 child=dir2, clabel=label2) 377 parent2=dir1b, plabel2=label1b,
313 ui.debug('running %r in %s\n' % (pycompat.bytestr(cmdline), 378 child=dir2, clabel=label2)
314 tmproot)) 379 ui.debug('running %r in %s\n' % (pycompat.bytestr(cmdline),
315 ui.system(cmdline, cwd=tmproot, blockedtag='extdiff') 380 tmproot))
381 ui.system(cmdline, cwd=tmproot, blockedtag='extdiff')
382 else:
383 # Run the external tool once for each pair of files
384 _runperfilediff(
385 cmdline, repo.root, ui, do3way=do3way, confirm=confirm,
386 commonfiles=common, tmproot=tmproot, dir1a=dir1a, dir1b=dir1b,
387 dir2root=dir2root, dir2=dir2,
388 rev1a=rev1a, rev1b=rev1b, rev2=rev2)
316 389
317 for copy_fn, working_fn, st in fnsandstat: 390 for copy_fn, working_fn, st in fnsandstat:
318 cpstat = os.lstat(copy_fn) 391 cpstat = os.lstat(copy_fn)
319 # Some tools copy the file and attributes, so mtime may not detect 392 # Some tools copy the file and attributes, so mtime may not detect
320 # all changes. A size check will detect more cases, but not all. 393 # all changes. A size check will detect more cases, but not all.
338 extdiffopts = [ 411 extdiffopts = [
339 ('o', 'option', [], 412 ('o', 'option', [],
340 _('pass option to comparison program'), _('OPT')), 413 _('pass option to comparison program'), _('OPT')),
341 ('r', 'rev', [], _('revision'), _('REV')), 414 ('r', 'rev', [], _('revision'), _('REV')),
342 ('c', 'change', '', _('change made by revision'), _('REV')), 415 ('c', 'change', '', _('change made by revision'), _('REV')),
416 ('', 'per-file', False,
417 _('compare each file instead of revision snapshots')),
418 ('', 'confirm', False,
419 _('prompt user before each external program invocation')),
343 ('', 'patch', None, _('compare patches for two revisions')) 420 ('', 'patch', None, _('compare patches for two revisions'))
344 ] + cmdutil.walkopts + cmdutil.subrepoopts 421 ] + cmdutil.walkopts + cmdutil.subrepoopts
345 422
346 @command('extdiff', 423 @command('extdiff',
347 [('p', 'program', '', _('comparison program to run'), _('CMD')), 424 [('p', 'program', '', _('comparison program to run'), _('CMD')),
355 Show differences between revisions for the specified files, using 432 Show differences between revisions for the specified files, using
356 an external program. The default program used is diff, with 433 an external program. The default program used is diff, with
357 default options "-Npru". 434 default options "-Npru".
358 435
359 To select a different program, use the -p/--program option. The 436 To select a different program, use the -p/--program option. The
360 program will be passed the names of two directories to compare. To 437 program will be passed the names of two directories to compare,
361 pass additional options to the program, use -o/--option. These 438 unless the --per-file option is specified (see below). To pass
362 will be passed before the names of the directories to compare. 439 additional options to the program, use -o/--option. These will be
440 passed before the names of the directories or files to compare.
363 441
364 When two revision arguments are given, then changes are shown 442 When two revision arguments are given, then changes are shown
365 between those revisions. If only one revision is specified then 443 between those revisions. If only one revision is specified then
366 that revision is compared to the working directory, and, when no 444 that revision is compared to the working directory, and, when no
367 revisions are specified, the working directory files are compared 445 revisions are specified, the working directory files are compared
368 to its parent.''' 446 to its parent.
447
448 The --per-file option runs the external program repeatedly on each
449 file to diff, instead of once on two directories.
450
451 The --confirm option will prompt the user before each invocation of
452 the external program. It is ignored if --per-file isn't specified.
453 '''
369 opts = pycompat.byteskwargs(opts) 454 opts = pycompat.byteskwargs(opts)
370 program = opts.get('program') 455 program = opts.get('program')
371 option = opts.get('option') 456 option = opts.get('option')
372 if not program: 457 if not program:
373 program = 'diff' 458 program = 'diff'