Mercurial > hg
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' |