1# visdiff.py - launch external visual diff tools 2# 3# Copyright 2009 Steve Borho <steve@borho.org> 4# 5# This software may be used and distributed according to the terms of the 6# GNU General Public License version 2, incorporated herein by reference. 7 8from __future__ import absolute_import 9 10import os 11import re 12import stat 13import subprocess 14import threading 15 16from .qtcore import ( 17 QTimer, 18 pyqtSlot, 19) 20from .qtgui import ( 21 QComboBox, 22 QDialog, 23 QDialogButtonBox, 24 QHBoxLayout, 25 QKeySequence, 26 QLabel, 27 QListWidget, 28 QMessageBox, 29 QShortcut, 30 QVBoxLayout, 31) 32 33from mercurial import ( 34 copies, 35 error, 36 match, 37 pycompat, 38 scmutil, 39 util, 40) 41from mercurial.utils import ( 42 procutil, 43 stringutil, 44) 45 46from ..util import hglib 47from ..util.i18n import _ 48from . import qtlib 49 50if hglib.TYPE_CHECKING: 51 from typing import ( 52 Any, 53 Dict, 54 Iterable, 55 List, 56 Optional, 57 Sequence, 58 Set, 59 Text, 60 Tuple, 61 Union, 62 ) 63 from mercurial import ( 64 localrepo, 65 ui as uimod, 66 ) 67 from .qtgui import ( 68 QListWidgetItem, 69 ) 70 from ..util.typelib import ( 71 DiffTools, 72 HgContext, 73 ) 74 75 # Destination name, source name, dest modification time 76 FnsAndMtime = Tuple[bytes, bytes, float] 77 78 79# Match parent2 first, so 'parent1?' will match both parent1 and parent 80_regex = b'\$(parent2|parent1?|child|plabel1|plabel2|clabel|repo|phash1|phash2|chash)' 81 82_nonexistant = _('[non-existant]') 83 84# This global counter is incremented for each visual diff done in a session 85# It ensures that the names for snapshots created do not collide. 86_diffCount = 0 87 88def snapshotset(repo, ctxs, sa, sb, copies, copyworkingdir = False): 89 # type: (localrepo.localrepository, Sequence[HgContext], List[Set[bytes]], List[Set[bytes]], Dict[bytes, bytes], bool) -> Tuple[List[Optional[bytes]], List[bytes], List[List[FnsAndMtime]]] 90 '''snapshot files from parent-child set of revisions''' 91 ctx1a, ctx1b, ctx2 = ctxs 92 mod_a, add_a, rem_a = sa 93 mod_b, add_b, rem_b = sb 94 95 global _diffCount 96 _diffCount += 1 97 98 if copies: 99 sources = set(copies.values()) 100 else: 101 sources = set() 102 103 # Always make a copy of ctx1a 104 files1a = sources | mod_a | rem_a | ((mod_b | add_b) - add_a) 105 dir1a, fns_mtime1a = snapshot(repo, files1a, ctx1a) 106 label1a = b'@%d:%s' % (ctx1a.rev(), ctx1a) 107 108 # Make a copy of ctx1b if relevant 109 if ctx1b: 110 files1b = sources | mod_b | rem_b | ((mod_a | add_a) - add_b) 111 dir1b, fns_mtime1b = snapshot(repo, files1b, ctx1b) 112 label1b = b'@%d:%s' % (ctx1b.rev(), ctx1b) 113 else: 114 dir1b = None 115 fns_mtime1b = [] # type: List[FnsAndMtime] 116 label1b = b'' 117 118 # Either make a copy of ctx2, or use working dir directly if relevant. 119 files2 = mod_a | add_a | mod_b | add_b 120 if ctx2.rev() is None: 121 if copyworkingdir: 122 dir2, fns_mtime2 = snapshot(repo, files2, ctx2) 123 else: 124 dir2 = repo.root 125 fns_mtime2 = [] # type: List[FnsAndMtime] 126 # If ctx2 is working copy, use empty label. 127 label2 = b'' 128 else: 129 dir2, fns_mtime2 = snapshot(repo, files2, ctx2) 130 label2 = b'@%d:%s' % (ctx2.rev(), ctx2) 131 132 dirs = [dir1a, dir1b, dir2] 133 labels = [label1a, label1b, label2] 134 fns_and_mtimes = [fns_mtime1a, fns_mtime1b, fns_mtime2] 135 return dirs, labels, fns_and_mtimes 136 137def snapshot(repo, files, ctx): 138 # type: (localrepo.localrepository, Iterable[bytes], HgContext) -> Tuple[bytes, List[FnsAndMtime]] 139 '''snapshot repo files as of some revision, returning a tuple with the 140 created temporary snapshot dir and tuples of file info if using working 141 copy.''' 142 dirname = os.path.basename(repo.root) or b'root' 143 dirname += b'.%d' % _diffCount 144 if ctx.rev() is not None: 145 dirname += b'.%d' % ctx.rev() 146 base = os.path.join(qtlib.gettempdir(), dirname) 147 fns_and_mtime = [] 148 if not os.path.exists(base): 149 os.makedirs(base) 150 for fn in files: 151 assert isinstance(fn, bytes), repr(fn) 152 wfn = util.pconvert(fn) 153 if wfn not in ctx: 154 # File doesn't exist; could be a bogus modify 155 continue 156 dest = os.path.join(base, wfn) 157 if os.path.exists(dest): 158 # File has already been snapshot 159 continue 160 destdir = os.path.dirname(dest) 161 try: 162 if not os.path.isdir(destdir): 163 os.makedirs(destdir) 164 fctx = ctx[wfn] 165 data = repo.wwritedata(wfn, fctx.data()) 166 with open(dest, 'wb') as f: 167 f.write(data) 168 if b'x' in fctx.flags(): 169 util.setflags(dest, False, True) 170 if ctx.rev() is None: 171 fns_and_mtime.append((dest, repo.wjoin(fn), 172 os.lstat(dest).st_mtime)) 173 else: 174 # Make file read/only, to indicate it's static (archival) nature 175 os.chmod(dest, stat.S_IREAD) 176 except EnvironmentError: 177 pass 178 return base, fns_and_mtime 179 180def launchtool(cmd, opts, replace, block): 181 # type: (bytes, Sequence[bytes], Dict[Text, Union[bytes, Text]], bool) -> None 182 # TODO: fix up the bytes vs str in the replacement mapping 183 def quote(match): 184 key = pycompat.sysstr(match.group()[1:]) 185 return procutil.shellquote(replace[key]) 186 187 args = b' '.join(opts) 188 args = re.sub(_regex, quote, args) 189 cmdline = procutil.shellquote(cmd) + b' ' + args 190 try: 191 proc = subprocess.Popen(procutil.tonativestr(cmdline), shell=True, 192 creationflags=qtlib.openflags, 193 stderr=subprocess.PIPE, 194 stdout=subprocess.PIPE, 195 stdin=subprocess.PIPE) 196 if block: 197 proc.communicate() 198 except (OSError, EnvironmentError) as e: 199 QMessageBox.warning(None, 200 _('Tool launch failure'), 201 _('%s : %s') % (hglib.tounicode(cmd), hglib.tounicode(str(e)))) 202 203def filemerge(ui, fname, patchedfname): 204 # type: (uimod.ui, Text, Text) -> None 205 'Launch the preferred visual diff tool for two text files' 206 detectedtools = hglib.difftools(ui) 207 if not detectedtools: 208 QMessageBox.warning(None, 209 _('No diff tool found'), 210 _('No visual diff tools were detected')) 211 return None 212 preferred = besttool(ui, detectedtools) 213 diffcmd, diffopts, mergeopts = detectedtools[preferred] 214 replace = dict(parent=fname, parent1=fname, 215 plabel1=fname + _('[working copy]'), 216 repo='', phash1='', phash2='', chash='', 217 child=patchedfname, clabel=_('[original]')) 218 launchtool(diffcmd, diffopts, replace, True) 219 220 221def besttool(ui, tools, force=None): 222 # type: (uimod.ui, DiffTools, Optional[bytes]) -> bytes 223 'Select preferred or highest priority tool from dictionary' 224 preferred = force or ui.config(b'tortoisehg', b'vdiff') or \ 225 ui.config(b'ui', b'merge') 226 if preferred and preferred in tools: 227 return preferred 228 pris = [] 229 for t in tools.keys(): 230 try: 231 p = ui.configint(b'merge-tools', t + b'.priority') 232 except error.ConfigError as inst: 233 ui.warn(b'visdiff: %s\n' % stringutil.forcebytestr(inst)) 234 p = 0 235 assert p is not None # help pytype: default *.priority is 0 236 pris.append((-p, t)) 237 238 return sorted(pris)[0][1] 239 240 241def visualdiff(ui, repo, pats, opts): 242 # type: (uimod.ui, localrepo.localrepository, Sequence[bytes], Dict[Text, Any]) -> Optional[FileSelectionDialog] 243 revs = opts.get('rev', []) 244 change = opts.get('change') 245 246 try: 247 ctx1b = None 248 if change: 249 # TODO: figure out what's the expect type 250 if isinstance(change, pycompat.unicode): 251 change = hglib.fromunicode(change) 252 if isinstance(change, bytes): 253 ctx2 = hglib.revsymbol(repo, change) 254 else: 255 ctx2 = repo[change] 256 p = ctx2.parents() 257 if len(p) > 1: 258 ctx1a, ctx1b = p 259 else: 260 ctx1a = p[0] 261 else: 262 n1, n2 = scmutil.revpair(repo, [hglib.fromunicode(rev) 263 for rev in revs]) 264 ctx1a, ctx2 = repo[n1], repo[n2] 265 p = ctx2.parents() 266 if not revs and len(p) > 1: 267 ctx1b = p[1] 268 except (error.LookupError, error.RepoError): 269 QMessageBox.warning(None, 270 _('Unable to find changeset'), 271 _('You likely need to refresh this application')) 272 return None 273 274 return visual_diff(ui, repo, pats, ctx1a, ctx1b, ctx2, opts.get('tool'), 275 opts.get('mainapp'), revs) 276 277def visual_diff(ui, repo, pats, ctx1a, ctx1b, ctx2, tool, mainapp=False, 278 revs=None): 279 # type: (uimod.ui, localrepo.localrepository, Sequence[bytes], HgContext, Optional[HgContext], HgContext, bytes, bool, Optional[Sequence[int]]) -> Optional[FileSelectionDialog] 280 """Opens the visual diff tool on the given file patterns in the given 281 contexts. If a ``tool`` is provided, it is used, otherwise the diff tool 282 launched is determined by the configuration. For a 2-way diff, ``ctx1a`` is 283 the context for the first revision, ``ctxb1`` is None, and ``ctx2`` is the 284 context for the second revision. For a 3-way diff, ``ctx2`` is the wdir 285 context and ``ctx1a`` and ``ctx1b`` are the "local" and "other" contexts 286 respectively. 287 """ 288 # TODO: Figure out how to get rid of the `revs` argument 289 if revs is None: 290 revs = [] 291 pats = scmutil.expandpats(pats) 292 m = match.match(repo.root, b'', pats, None, None, b'relpath', ctx=ctx2) 293 n2 = ctx2.node() 294 295 def _status(ctx): 296 # type: (HgContext) -> Tuple[List[bytes], List[bytes], List[bytes]] 297 status = repo.status(ctx.node(), n2, m) 298 return status.modified, status.added, status.removed 299 300 mod_a, add_a, rem_a = pycompat.maplist(set, _status(ctx1a)) 301 if ctx1b: 302 mod_b, add_b, rem_b = pycompat.maplist(set, _status(ctx1b)) 303 cpy = copies.mergecopies(repo, ctx1a, ctx1b, ctx1a.ancestor(ctx1b))[0].copy 304 else: 305 cpy = copies.pathcopies(ctx1a, ctx2) 306 mod_b, add_b, rem_b = set(), set(), set() 307 308 cpy = { 309 dst: src for dst, src in cpy.items() if m(src) or m(dst) 310 } 311 312 MA = mod_a | add_a | mod_b | add_b 313 MAR = MA | rem_a | rem_b 314 if not MAR: 315 QMessageBox.information(None, 316 _('No file changes'), 317 _('There are no file changes to view')) 318 return None 319 320 detectedtools = hglib.difftools(repo.ui) 321 if not detectedtools: 322 QMessageBox.warning(None, 323 _('No diff tool found'), 324 _('No visual diff tools were detected')) 325 return None 326 327 preferred = besttool(repo.ui, detectedtools, tool) 328 329 # Build tool list based on diff-patterns matches 330 toollist = set() 331 patterns = repo.ui.configitems(b'diff-patterns') 332 patterns = [(p, t) for p,t in patterns if t in detectedtools] 333 for path in MAR: 334 for pat, tool in patterns: 335 mf = match.match(repo.root, b'', [pat]) 336 if mf(path): 337 toollist.add(tool) 338 break 339 else: 340 toollist.add(preferred) 341 342 cto = list(cpy.keys()) 343 for path in MAR: 344 if path in cto: 345 hascopies = True 346 break 347 else: 348 hascopies = False 349 force = repo.ui.configbool(b'tortoisehg', b'forcevdiffwin') 350 if len(toollist) > 1 or (hascopies and len(MAR) > 1) or force: 351 usewin = True 352 else: 353 preferred = toollist.pop() 354 dirdiff = repo.ui.configbool(b'merge-tools', preferred + b'.dirdiff') 355 dir3diff = repo.ui.configbool(b'merge-tools', preferred + b'.dir3diff') 356 usewin = repo.ui.configbool(b'merge-tools', preferred + b'.usewin') 357 if not usewin and len(MAR) > 1: 358 if ctx1b is not None: 359 usewin = not dir3diff 360 else: 361 usewin = not dirdiff 362 if usewin: 363 # Multiple required tools, or tool does not support directory diffs 364 sa = [mod_a, add_a, rem_a] 365 sb = [mod_b, add_b, rem_b] 366 dlg = FileSelectionDialog(repo, pats, ctx1a, sa, ctx1b, sb, ctx2, cpy) 367 return dlg 368 369 # We can directly use the selected tool, without a visual diff window 370 diffcmd, diffopts, mergeopts = detectedtools[preferred] 371 372 # Disable 3-way merge if there is only one parent or no tool support 373 do3way = False 374 if ctx1b: 375 if mergeopts: 376 do3way = True 377 args = mergeopts 378 else: 379 args = diffopts 380 if str(ctx1b.rev()) in revs: 381 ctx1a = ctx1b 382 else: 383 args = diffopts 384 385 def dodiff(): 386 assert not (hascopies and len(MAR) > 1), \ 387 'dodiff cannot handle copies when diffing dirs' 388 389 sa = [mod_a, add_a, rem_a] 390 sb = [mod_b, add_b, rem_b] 391 ctxs = [ctx1a, ctx1b, ctx2] 392 393 # If more than one file, diff on working dir copy. 394 copyworkingdir = len(MAR) > 1 395 dirs, labels, fns_and_mtimes = snapshotset(repo, ctxs, sa, sb, cpy, 396 copyworkingdir) 397 dir1a, dir1b, dir2 = dirs 398 label1a, label1b, label2 = labels 399 fns_and_mtime = fns_and_mtimes[2] 400 401 if len(MAR) > 1 and label2 == b'': 402 label2 = b'working files' 403 404 def getfile(fname, dir, label): 405 # type: (bytes, bytes, bytes) -> Tuple[bytes, bytes] 406 file = os.path.join(qtlib.gettempdir(), dir, fname) 407 if os.path.isfile(file): 408 return fname+label, file 409 nullfile = os.path.join(qtlib.gettempdir(), b'empty') 410 fp = open(nullfile, 'wb') 411 fp.close() 412 return (hglib.fromunicode(_nonexistant, 'replace') + label, 413 nullfile) 414 415 # If only one change, diff the files instead of the directories 416 # Handle bogus modifies correctly by checking if the files exist 417 if len(MAR) == 1: 418 file2 = MAR.pop() 419 file2local = util.localpath(file2) 420 if file2 in cto: 421 file1 = util.localpath(cpy[file2]) 422 else: 423 file1 = file2 424 label1a, dir1a = getfile(file1, dir1a, label1a) 425 if do3way: 426 label1b, dir1b = getfile(file1, dir1b, label1b) 427 label2, dir2 = getfile(file2local, dir2, label2) 428 if do3way: 429 label1a += b'[local]' 430 label1b += b'[other]' 431 label2 += b'[merged]' 432 433 repoagent = repo._pyqtobj # TODO 434 435 # TODO: sort out bytes vs str 436 replace = dict(parent=dir1a, parent1=dir1a, parent2=dir1b, 437 plabel1=label1a, plabel2=label1b, 438 phash1=str(ctx1a), phash2=str(ctx1b), 439 repo=hglib.fromunicode(repoagent.displayName()), 440 clabel=label2, child=dir2, chash=str(ctx2)) # type: Dict[Text, Union[bytes, Text]] 441 launchtool(diffcmd, args, replace, True) 442 443 # detect if changes were made to mirrored working files 444 for copy_fn, working_fn, mtime in fns_and_mtime: 445 try: 446 if os.lstat(copy_fn).st_mtime != mtime: 447 ui.debug(b'file changed while diffing. ' 448 b'Overwriting: %s (src: %s)\n' 449 % (working_fn, copy_fn)) 450 util.copyfile(copy_fn, working_fn) 451 except EnvironmentError: 452 pass # Ignore I/O errors or missing files 453 454 if mainapp: 455 dodiff() 456 else: 457 # We are not the main application, so this must be done in a 458 # background thread 459 thread = threading.Thread(target=dodiff, name='visualdiff') 460 thread.setDaemon(True) 461 thread.start() 462 463class FileSelectionDialog(QDialog): 464 'Dialog for selecting visual diff candidates' 465 def __init__(self, repo, pats, ctx1a, sa, ctx1b, sb, ctx2, cpy): 466 # type: (localrepo.localrepository, Sequence[bytes], HgContext, List[Set[bytes]], Optional[HgContext], List[Set[bytes]], HgContext, Dict[bytes, bytes]) -> None 467 'Initialize the Dialog' 468 QDialog.__init__(self) 469 470 self.setWindowIcon(qtlib.geticon('visualdiff')) 471 472 if ctx2.rev() is None: 473 title = _('working changes') 474 elif ctx1a == ctx2.parents()[0]: 475 title = _('changeset %d:%s') % (ctx2.rev(), ctx2) 476 else: 477 title = _('revisions %d:%s to %d:%s') \ 478 % (ctx1a.rev(), ctx1a, ctx2.rev(), ctx2) 479 title = _('Visual Diffs - ') + title 480 if pats: 481 title += _(' filtered') 482 self.setWindowTitle(title) 483 484 self.resize(650, 250) 485 repoagent = repo._pyqtobj # TODO 486 self.reponame = hglib.fromunicode(repoagent.displayName()) 487 488 self.ctxs = (ctx1a, ctx1b, ctx2) 489 self.filesets = (sa, sb) 490 self.copies = cpy 491 self.repo = repo 492 self.curFile = None # type: Optional[bytes] 493 494 layout = QVBoxLayout() 495 self.setLayout(layout) 496 497 lbl = QLabel(_('Temporary files are removed when this dialog ' 498 'is closed')) 499 layout.addWidget(lbl) 500 501 list = QListWidget() 502 layout.addWidget(list) 503 self.list = list 504 list.itemActivated.connect(self.itemActivated) 505 506 tools = hglib.difftools(repo.ui) 507 preferred = besttool(repo.ui, tools) 508 self.diffpath, self.diffopts, self.mergeopts = tools[preferred] 509 self.tools = tools 510 self.preferred = preferred 511 512 if len(tools) > 1: 513 hbox = QHBoxLayout() 514 combo = QComboBox() 515 lbl = QLabel(_('Select Tool:')) 516 lbl.setBuddy(combo) 517 hbox.addWidget(lbl) 518 hbox.addWidget(combo, 1) 519 layout.addLayout(hbox) 520 for i, name in enumerate(tools.keys()): 521 combo.addItem(hglib.tounicode(name)) 522 if name == preferred: 523 defrow = i 524 combo.setCurrentIndex(defrow) 525 526 list.currentRowChanged.connect(self.updateToolSelection) 527 combo.currentIndexChanged[str].connect(self.onToolSelected) 528 self.toolCombo = combo 529 530 BB = QDialogButtonBox 531 bb = BB() 532 layout.addWidget(bb) 533 534 if ctx2.rev() is None: 535 pass 536 # Do not offer directory diffs when the working directory 537 # is being referenced directly 538 elif ctx1b: 539 self.p1button = bb.addButton(_('Dir diff to p1'), BB.ActionRole) 540 self.p1button.pressed.connect(self.p1dirdiff) 541 self.p2button = bb.addButton(_('Dir diff to p2'), BB.ActionRole) 542 self.p2button.pressed.connect(self.p2dirdiff) 543 self.p3button = bb.addButton(_('3-way dir diff'), BB.ActionRole) 544 self.p3button.pressed.connect(self.threewaydirdiff) 545 else: 546 self.dbutton = bb.addButton(_('Directory diff'), BB.ActionRole) 547 self.dbutton.pressed.connect(self.p1dirdiff) 548 549 self.updateDiffButtons(preferred) 550 551 QShortcut(QKeySequence('CTRL+D'), self.list, self.activateCurrent) 552 QTimer.singleShot(0, self.fillmodel) 553 554 @pyqtSlot() 555 def fillmodel(self): 556 # type: () -> None 557 repo = self.repo 558 sa, sb = self.filesets 559 self.dirs, self.revs = snapshotset(repo, self.ctxs, sa, sb, self.copies)[:2] 560 561 def get_status(file, mod, add, rem): 562 # type: (bytes, Set[bytes], Set[bytes], Set[bytes]) -> Text 563 if file in mod: 564 return 'M' 565 if file in add: 566 return 'A' 567 if file in rem: 568 return 'R' 569 return ' ' 570 571 mod_a, add_a, rem_a = sa 572 for f in sorted(mod_a | add_a | rem_a): 573 status = get_status(f, mod_a, add_a, rem_a) 574 row = '%s %s' % (status, hglib.tounicode(f)) 575 self.list.addItem(row) 576 577 @pyqtSlot(str) 578 def onToolSelected(self, tool): 579 # type: (Text) -> None 580 'user selected a tool from the tool combo' 581 tool = hglib.fromunicode(tool) # pytype: disable=annotation-type-mismatch 582 assert tool in self.tools, tool 583 self.diffpath, self.diffopts, self.mergeopts = self.tools[tool] 584 self.updateDiffButtons(tool) 585 586 @pyqtSlot(int) 587 def updateToolSelection(self, row): 588 # type: (int) -> None 589 'user selected a file, pick an appropriate tool from combo' 590 if row == -1: 591 return 592 593 repo = self.repo 594 patterns = repo.ui.configitems(b'diff-patterns') 595 patterns = [(p, t) for p,t in patterns if t in self.tools] 596 597 fname = self.list.item(row).text()[2:] 598 fname = hglib.fromunicode(fname) 599 if self.curFile == fname: 600 return 601 self.curFile = fname 602 for pat, tool in patterns: 603 mf = match.match(repo.root, b'', [pat]) 604 if mf(fname): 605 selected = tool 606 break 607 else: 608 selected = self.preferred 609 for i, name in enumerate(self.tools.keys()): 610 if name == selected: 611 self.toolCombo.setCurrentIndex(i) 612 613 def activateCurrent(self): 614 # type: () -> None 615 'CTRL+D has been pressed' 616 row = self.list.currentRow() 617 if row >= 0: 618 self.launch(self.list.item(row).text()[2:]) 619 620 def itemActivated(self, item): 621 # type: (QListWidgetItem) -> None 622 'A QListWidgetItem has been activated' 623 self.launch(item.text()[2:]) 624 625 def updateDiffButtons(self, tool): 626 # type: (bytes) -> None 627 # hg>=4.4: configbool() may return None as the default is set to None 628 if hasattr(self, 'p1button'): 629 d2 = self.repo.ui.configbool(b'merge-tools', tool + b'.dirdiff') 630 d3 = self.repo.ui.configbool(b'merge-tools', tool + b'.dir3diff') 631 self.p1button.setEnabled(bool(d2)) 632 self.p2button.setEnabled(bool(d2)) 633 self.p3button.setEnabled(bool(d3)) 634 elif hasattr(self, 'dbutton'): 635 d2 = self.repo.ui.configbool(b'merge-tools', tool + b'.dirdiff') 636 self.dbutton.setEnabled(bool(d2)) 637 638 def launch(self, fname): 639 # type: (Text) -> None 640 fname = hglib.fromunicode(fname) # pytype: disable=annotation-type-mismatch 641 source = self.copies.get(fname, None) 642 dir1a, dir1b, dir2 = self.dirs 643 rev1a, rev1b, rev2 = self.revs 644 ctx1a, ctx1b, ctx2 = self.ctxs 645 646 # pytype: disable=redundant-function-type-comment 647 def getfile(ctx, dir, fname, source): 648 # type: (HgContext, bytes, bytes, Optional[bytes]) -> Tuple[bytes, bytes] 649 m = ctx.manifest() 650 if fname in m: 651 path = os.path.join(dir, util.localpath(fname)) 652 return fname, path 653 elif source and source in m: 654 path = os.path.join(dir, util.localpath(source)) 655 return source, path 656 else: 657 nullfile = os.path.join(qtlib.gettempdir(), b'empty') 658 fp = open(nullfile, 'w') 659 fp.close() 660 return hglib.fromunicode(_nonexistant, 'replace'), nullfile 661 # pytype: enable=redundant-function-type-comment 662 663 local, file1a = getfile(ctx1a, dir1a, fname, source) 664 if ctx1b: 665 other, file1b = getfile(ctx1b, dir1b, fname, source) 666 else: 667 other, file1b = fname, None 668 fname, file2 = getfile(ctx2, dir2, fname, None) # pytype: disable=annotation-type-mismatch 669 670 label1a = local+rev1a 671 label1b = other+rev1b 672 label2 = fname+rev2 673 if ctx1b: 674 label1a += b'[local]' 675 label1b += b'[other]' 676 label2 += b'[merged]' 677 678 # Function to quote file/dir names in the argument string 679 replace = dict(parent=file1a, parent1=file1a, plabel1=label1a, 680 parent2=file1b, plabel2=label1b, 681 repo=self.reponame, 682 phash1=str(ctx1a), phash2=str(ctx1b), chash=str(ctx2), 683 clabel=label2, child=file2) # type: Dict[Text, Union[bytes, Text]] 684 args = ctx1b and self.mergeopts or self.diffopts 685 launchtool(self.diffpath, args, replace, False) 686 687 def p1dirdiff(self): 688 # type: () -> None 689 dir1a, dir1b, dir2 = self.dirs 690 rev1a, rev1b, rev2 = self.revs 691 ctx1a, ctx1b, ctx2 = self.ctxs 692 693 replace = dict(parent=dir1a, parent1=dir1a, plabel1=rev1a, 694 repo=self.reponame, 695 phash1=str(ctx1a), phash2=str(ctx1b), chash=str(ctx2), 696 parent2='', plabel2='', clabel=rev2, child=dir2) # type: Dict[Text, Union[bytes, Text]] 697 launchtool(self.diffpath, self.diffopts, replace, False) 698 699 def p2dirdiff(self): 700 # type: () -> None 701 dir1a, dir1b, dir2 = self.dirs 702 rev1a, rev1b, rev2 = self.revs 703 ctx1a, ctx1b, ctx2 = self.ctxs 704 705 replace = dict(parent=dir1b, parent1=dir1b, plabel1=rev1b, 706 repo=self.reponame, 707 phash1=str(ctx1a), phash2=str(ctx1b), chash=str(ctx2), 708 parent2='', plabel2='', clabel=rev2, child=dir2) # type: Dict[Text, Union[bytes, Text]] 709 launchtool(self.diffpath, self.diffopts, replace, False) 710 711 def threewaydirdiff(self): 712 # type: () -> None 713 dir1a, dir1b, dir2 = self.dirs 714 rev1a, rev1b, rev2 = self.revs 715 ctx1a, ctx1b, ctx2 = self.ctxs 716 717 replace = dict(parent=dir1a, parent1=dir1a, plabel1=rev1a, 718 repo=self.reponame, 719 phash1=str(ctx1a), phash2=str(ctx1b), chash=str(ctx2), 720 parent2=dir1b, plabel2=rev1b, clabel=dir2, child=rev2) # type: Dict[Text, Union[bytes, Text]] 721 launchtool(self.diffpath, self.mergeopts, replace, False) 722