/tortoisehg/hgtk/visdiff.py

https://bitbucket.org/tortoisehg/hgtk/ · Python · 638 lines · 528 code · 81 blank · 29 comment · 123 complexity · b3520572d15c9380f6b8024b8c4100ab MD5 · raw file

  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. import gtk
  8. import gobject
  9. import os
  10. import subprocess
  11. import stat
  12. import shutil
  13. import threading
  14. import tempfile
  15. import re
  16. from mercurial import hg, ui, cmdutil, util, error, match, copies
  17. from tortoisehg.util.i18n import _
  18. from tortoisehg.util import hglib, settings, paths
  19. from tortoisehg.hgtk import gdialog, gtklib
  20. try:
  21. import win32con
  22. openflags = win32con.CREATE_NO_WINDOW
  23. except ImportError:
  24. openflags = 0
  25. # Match parent2 first, so 'parent1?' will match both parent1 and parent
  26. _regex = '\$(parent2|parent1?|child|plabel1|plabel2|clabel|repo|phash1|phash2|chash)'
  27. _nonexistant = _('[non-existant]')
  28. def snapshotset(repo, ctxs, sa, sb, copies, tmproot, copyworkingdir = False):
  29. '''snapshot files from parent-child set of revisions'''
  30. ctx1a, ctx1b, ctx2 = ctxs
  31. mod_a, add_a, rem_a = sa
  32. mod_b, add_b, rem_b = sb
  33. if copies:
  34. sources = set(copies.values())
  35. else:
  36. sources = set()
  37. # Always make a copy of ctx1a
  38. files1a = sources | mod_a | rem_a | ((mod_b | add_b) - add_a)
  39. dir1a, fns_mtime1a = snapshot(repo, files1a, ctx1a, tmproot)
  40. label1a = '@%d' % ctx1a.rev()
  41. # Make a copy of ctx1b if relevant
  42. if ctx1b:
  43. files1b = sources | mod_b | rem_b | ((mod_a | add_a) - add_b)
  44. dir1b, fns_mtime1b = snapshot(repo, files1b, ctx1b, tmproot)
  45. label1b = '@%d' % ctx1b.rev()
  46. else:
  47. dir1b = None
  48. fns_mtime1b = []
  49. label1b = ''
  50. # Either make a copy of ctx2, or use working dir directly if relevant.
  51. files2 = mod_a | add_a | mod_b | add_b
  52. if ctx2.rev() is None:
  53. if copyworkingdir:
  54. dir2, fns_mtime2 = snapshot(repo, files2, ctx2, tmproot)
  55. else:
  56. dir2 = repo.root
  57. fns_mtime2 = []
  58. # If ctx2 is working copy, use empty label.
  59. label2 = ''
  60. else:
  61. dir2, fns_mtime2 = snapshot(repo, files2, ctx2, tmproot)
  62. label2 = '@%d' % ctx2.rev()
  63. dirs = [dir1a, dir1b, dir2]
  64. labels = [label1a, label1b, label2]
  65. fns_and_mtimes = [fns_mtime1a, fns_mtime1b, fns_mtime2]
  66. return dirs, labels, fns_and_mtimes
  67. def snapshot(repo, files, ctx, tmproot):
  68. '''snapshot files as of some revision'''
  69. dirname = os.path.basename(repo.root) or 'root'
  70. if ctx.rev() is not None:
  71. dirname = '%s.%s' % (dirname, str(ctx))
  72. base = os.path.join(tmproot, dirname)
  73. os.mkdir(base)
  74. fns_and_mtime = []
  75. for fn in files:
  76. wfn = util.pconvert(fn)
  77. if not wfn in ctx:
  78. # File doesn't exist; could be a bogus modify
  79. continue
  80. dest = os.path.join(base, wfn)
  81. destdir = os.path.dirname(dest)
  82. if not os.path.isdir(destdir):
  83. os.makedirs(destdir)
  84. data = repo.wwritedata(wfn, ctx[wfn].data())
  85. f = open(dest, 'wb')
  86. f.write(data)
  87. f.close()
  88. if ctx.rev() is None:
  89. fns_and_mtime.append((dest, repo.wjoin(fn), os.path.getmtime(dest)))
  90. elif os.name != 'nt':
  91. # Make file read/only, to indicate it's static (archival) nature
  92. os.chmod(dest, stat.S_IREAD)
  93. return base, fns_and_mtime
  94. def launchtool(cmd, opts, replace, block):
  95. def quote(match):
  96. key = match.group()[1:]
  97. return util.shellquote(replace[key])
  98. args = ' '.join(opts)
  99. args = re.sub(_regex, quote, args)
  100. cmdline = util.shellquote(cmd) + ' ' + args
  101. cmdline = util.quotecommand(cmdline)
  102. try:
  103. proc = subprocess.Popen(cmdline, shell=True,
  104. creationflags=openflags,
  105. stderr=subprocess.PIPE,
  106. stdout=subprocess.PIPE,
  107. stdin=subprocess.PIPE)
  108. if block:
  109. proc.communicate()
  110. except (OSError, EnvironmentError), e:
  111. gdialog.Prompt(_('Tool launch failure'),
  112. _('%s : %s') % (cmd, str(e)), None).run()
  113. def filemerge(ui, fname, patchedfname):
  114. 'Launch the preferred visual diff tool for two text files'
  115. detectedtools = hglib.difftools(ui)
  116. if not detectedtools:
  117. gdialog.Prompt(_('No diff tool found'),
  118. _('No visual diff tools were detected'), None).run()
  119. return None
  120. preferred = besttool(ui, detectedtools)
  121. diffcmd, diffopts, mergeopts = detectedtools[preferred]
  122. replace = dict(parent=fname, parent1=fname,
  123. plabel1=fname + _('[working copy]'),
  124. repo='', phash1='', phash2='', chash='',
  125. child=patchedfname, clabel=_('[original]'))
  126. launchtool(diffcmd, diffopts, replace, True)
  127. def besttool(ui, tools):
  128. 'Select preferred or highest priority tool from dictionary'
  129. preferred = ui.config('tortoisehg', 'vdiff') or ui.config('ui', 'merge')
  130. if preferred and preferred in tools:
  131. return preferred
  132. pris = []
  133. for t in tools.keys():
  134. p = int(ui.config('merge-tools', t + '.priority', 0))
  135. pris.append((-p, t))
  136. tools = sorted(pris)
  137. return tools[0][1]
  138. def visualdiff(ui, repo, pats, opts):
  139. revs = opts.get('rev')
  140. change = opts.get('change')
  141. try:
  142. ctx1b = None
  143. if change:
  144. ctx2 = repo[change]
  145. p = ctx2.parents()
  146. if len(p) > 1:
  147. ctx1a, ctx1b = p
  148. else:
  149. ctx1a = p[0]
  150. else:
  151. n1, n2 = cmdutil.revpair(repo, revs)
  152. ctx1a, ctx2 = repo[n1], repo[n2]
  153. p = ctx2.parents()
  154. if not revs and len(p) > 1:
  155. ctx1b = p[1]
  156. except (error.LookupError, error.RepoError):
  157. gdialog.Prompt(_('Unable to find changeset'),
  158. _('You likely need to refresh this application'),
  159. None).run()
  160. return None
  161. pats = cmdutil.expandpats(pats)
  162. m = match.match(repo.root, '', pats, None, None, 'relpath')
  163. n2 = ctx2.node()
  164. mod_a, add_a, rem_a = map(set, repo.status(ctx1a.node(), n2, m)[:3])
  165. if ctx1b:
  166. mod_b, add_b, rem_b = map(set, repo.status(ctx1b.node(), n2, m)[:3])
  167. cpy = copies.copies(repo, ctx1a, ctx1b, ctx1a.ancestor(ctx1b))[0]
  168. else:
  169. cpy = copies.copies(repo, ctx1a, ctx2, repo[-1])[0]
  170. mod_b, add_b, rem_b = set(), set(), set()
  171. MA = mod_a | add_a | mod_b | add_b
  172. MAR = MA | rem_a | rem_b
  173. if not MAR:
  174. gdialog.Prompt(_('No file changes'),
  175. _('There are no file changes to view'), None).run()
  176. return None
  177. detectedtools = hglib.difftools(repo.ui)
  178. if not detectedtools:
  179. gdialog.Prompt(_('No diff tool found'),
  180. _('No visual diff tools were detected'), None).run()
  181. return None
  182. preferred = besttool(repo.ui, detectedtools)
  183. # Build tool list based on diff-patterns matches
  184. toollist = set()
  185. patterns = repo.ui.configitems('diff-patterns')
  186. patterns = [(p, t) for p,t in patterns if t in detectedtools]
  187. for path in MAR:
  188. for pat, tool in patterns:
  189. mf = match.match(repo.root, '', [pat])
  190. if mf(path):
  191. toollist.add(tool)
  192. break
  193. else:
  194. toollist.add(preferred)
  195. cto = cpy.keys()
  196. for path in MAR:
  197. if path in cto:
  198. hascopies = True
  199. break
  200. else:
  201. hascopies = False
  202. force = repo.ui.configbool('tortoisehg', 'forcevdiffwin')
  203. if len(toollist) > 1 or (hascopies and len(MAR) > 1) or force:
  204. usewin = True
  205. else:
  206. preferred = toollist.pop()
  207. dirdiff = repo.ui.configbool('merge-tools', preferred + '.dirdiff')
  208. dir3diff = repo.ui.configbool('merge-tools', preferred + '.dir3diff')
  209. usewin = repo.ui.configbool('merge-tools', preferred + '.usewin')
  210. if not usewin and len(MAR) > 1:
  211. if ctx1b is not None:
  212. usewin = not dir3diff
  213. else:
  214. usewin = not dirdiff
  215. if usewin:
  216. # Multiple required tools, or tool does not support directory diffs
  217. sa = [mod_a, add_a, rem_a]
  218. sb = [mod_b, add_b, rem_b]
  219. dlg = FileSelectionDialog(repo, pats, ctx1a, sa, ctx1b, sb, ctx2, cpy)
  220. return dlg
  221. # We can directly use the selected tool, without a visual diff window
  222. diffcmd, diffopts, mergeopts = detectedtools[preferred]
  223. # Disable 3-way merge if there is only one parent or no tool support
  224. do3way = False
  225. if ctx1b:
  226. if mergeopts:
  227. do3way = True
  228. args = mergeopts
  229. else:
  230. args = diffopts
  231. if str(ctx1b.rev()) in revs:
  232. ctx1a = ctx1b
  233. else:
  234. args = diffopts
  235. def dodiff(tmproot):
  236. assert not (hascopies and len(MAR) > 1), \
  237. 'dodiff cannot handle copies when diffing dirs'
  238. sa = [mod_a, add_a, rem_a]
  239. sb = [mod_b, add_b, rem_b]
  240. ctxs = [ctx1a, ctx1b, ctx2]
  241. # If more than one file, diff on working dir copy.
  242. copyworkingdir = len(MAR) > 1
  243. dirs, labels, fns_and_mtimes = snapshotset(repo, ctxs, sa, sb, cpy,
  244. tmproot, copyworkingdir)
  245. dir1a, dir1b, dir2 = dirs
  246. label1a, label1b, label2 = labels
  247. fns_and_mtime = fns_and_mtimes[2]
  248. if len(MAR) > 1 and label2 == '':
  249. label2 = 'working files'
  250. def getfile(fname, dir, label):
  251. file = os.path.join(tmproot, dir, fname)
  252. if os.path.isfile(file):
  253. return fname+label, file
  254. nullfile = os.path.join(tmproot, 'empty')
  255. fp = open(nullfile, 'w')
  256. fp.close()
  257. return _nonexistant+label, nullfile
  258. # If only one change, diff the files instead of the directories
  259. # Handle bogus modifies correctly by checking if the files exist
  260. if len(MAR) == 1:
  261. file2 = util.localpath(MAR.pop())
  262. if file2 in cto:
  263. file1 = util.localpath(cpy[file2])
  264. else:
  265. file1 = file2
  266. label1a, dir1a = getfile(file1, dir1a, label1a)
  267. if do3way:
  268. label1b, dir1b = getfile(file1, dir1b, label1b)
  269. label2, dir2 = getfile(file2, dir2, label2)
  270. if do3way:
  271. label1a += '[local]'
  272. label1b += '[other]'
  273. label2 += '[merged]'
  274. replace = dict(parent=dir1a, parent1=dir1a, parent2=dir1b,
  275. plabel1=label1a, plabel2=label1b,
  276. phash1=str(ctx1a), phash2=str(ctx1b),
  277. repo=hglib.get_reponame(repo),
  278. clabel=label2, child=dir2, chash=str(ctx2))
  279. launchtool(diffcmd, args, replace, True)
  280. # detect if changes were made to mirrored working files
  281. for copy_fn, working_fn, mtime in fns_and_mtime:
  282. if os.path.getmtime(copy_fn) != mtime:
  283. ui.debug('file changed while diffing. '
  284. 'Overwriting: %s (src: %s)\n' % (working_fn, copy_fn))
  285. util.copyfile(copy_fn, working_fn)
  286. def dodiffwrapper():
  287. try:
  288. dodiff(tmproot)
  289. finally:
  290. ui.note(_('cleaning up temp directory\n'))
  291. try:
  292. shutil.rmtree(tmproot)
  293. except (IOError, OSError), e:
  294. # Leaking temporary files, fix your diff tool config
  295. ui.note(_('unable to clean temp directory: %s\n'), str(e))
  296. tmproot = tempfile.mkdtemp(prefix='visualdiff.')
  297. if opts.get('mainapp'):
  298. dodiffwrapper()
  299. else:
  300. # We are not the main application, so this must be done in a
  301. # background thread
  302. thread = threading.Thread(target=dodiffwrapper, name='visualdiff')
  303. thread.setDaemon(True)
  304. thread.start()
  305. class FileSelectionDialog(gtk.Dialog):
  306. 'Dialog for selecting visual diff candidates'
  307. def __init__(self, repo, pats, ctx1a, sa, ctx1b, sb, ctx2, cpy):
  308. 'Initialize the Dialog'
  309. gtk.Dialog.__init__(self, title=_('Visual Diffs'))
  310. gtklib.set_tortoise_icon(self, 'menushowchanged.ico')
  311. gtklib.set_tortoise_keys(self)
  312. if ctx2.rev() is None:
  313. title = _('working changes')
  314. elif ctx1a == ctx2.parents()[0]:
  315. title = _('changeset ') + str(ctx2.rev())
  316. else:
  317. title = _('revisions %d to %d') % (ctx1a.rev(), ctx2.rev())
  318. title = _('Visual Diffs - ') + title
  319. if pats:
  320. title += _(' filtered')
  321. self.set_title(title)
  322. self.set_default_size(400, 250)
  323. self.set_has_separator(False)
  324. self.reponame=hglib.get_reponame(repo)
  325. self.ctxs = (ctx1a, ctx1b, ctx2)
  326. self.copies = cpy
  327. self.ui = repo.ui
  328. lbl = gtk.Label(_('Temporary files are removed when this dialog'
  329. ' is closed'))
  330. self.vbox.pack_start(lbl, False, False, 2)
  331. scroller = gtk.ScrolledWindow()
  332. scroller.set_shadow_type(gtk.SHADOW_IN)
  333. scroller.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
  334. treeview = gtk.TreeView()
  335. self.treeview = treeview
  336. treeview.get_selection().set_mode(gtk.SELECTION_SINGLE)
  337. treeview.set_search_equal_func(self.search_filelist)
  338. scroller.add(treeview)
  339. self.vbox.pack_start(scroller, True, True, 2)
  340. treeview.connect('row-activated', self.rowactivated)
  341. treeview.set_headers_visible(False)
  342. treeview.set_property('enable-grid-lines', True)
  343. treeview.set_enable_search(False)
  344. accelgroup = gtk.AccelGroup()
  345. self.add_accel_group(accelgroup)
  346. mod = gtklib.get_thg_modifier()
  347. key, modifier = gtk.accelerator_parse(mod+'d')
  348. treeview.add_accelerator('thg-diff', accelgroup, key,
  349. modifier, gtk.ACCEL_VISIBLE)
  350. treeview.connect('thg-diff', self.rowactivated)
  351. cell = gtk.CellRendererText()
  352. stcol = gtk.TreeViewColumn('Status', cell)
  353. stcol.set_resizable(True)
  354. stcol.add_attribute(cell, 'text', 0)
  355. treeview.append_column(stcol)
  356. cell = gtk.CellRendererText()
  357. fcol = gtk.TreeViewColumn('Filename', cell)
  358. fcol.set_resizable(True)
  359. fcol.add_attribute(cell, 'text', 1)
  360. treeview.append_column(fcol)
  361. model = gtk.ListStore(str, str)
  362. treeview.set_model(model)
  363. tools = hglib.difftools(repo.ui)
  364. preferred = besttool(repo.ui, tools)
  365. self.diffpath, self.diffopts, self.mergeopts = tools[preferred]
  366. hbox = gtk.HBox()
  367. self.vbox.pack_start(hbox, False, False, 2)
  368. if ctx2.rev() is None:
  369. pass
  370. # Do not offer directory diffs when the working directory
  371. # is being referenced directly
  372. elif ctx1b:
  373. self.p1button = gtk.Button(_('Dir diff to p1'))
  374. self.p1button.connect('pressed', self.p1dirdiff)
  375. self.p2button = gtk.Button(_('Dir diff to p2'))
  376. self.p2button.connect('pressed', self.p2dirdiff)
  377. self.p3button = gtk.Button(_('3-way dir diff'))
  378. self.p3button.connect('pressed', self.threewaydirdiff)
  379. hbox.pack_end(self.p3button, False, False)
  380. hbox.pack_end(self.p2button, False, False)
  381. hbox.pack_end(self.p1button, False, False)
  382. else:
  383. self.dbutton = gtk.Button(_('Directory diff'))
  384. self.dbutton.connect('pressed', self.p1dirdiff)
  385. hbox.pack_end(self.dbutton, False, False)
  386. self.update_diff_buttons(preferred)
  387. if len(tools) > 1:
  388. combo = gtk.combo_box_new_text()
  389. for i, name in enumerate(tools.iterkeys()):
  390. combo.append_text(name)
  391. if name == preferred:
  392. defrow = i
  393. combo.set_active(defrow)
  394. combo.connect('changed', self.toolselect, tools)
  395. hbox.pack_start(combo, False, False, 2)
  396. patterns = repo.ui.configitems('diff-patterns')
  397. patterns = [(p, t) for p,t in patterns if t in tools]
  398. filesel = treeview.get_selection()
  399. filesel.connect('changed', self.fileselect, repo, combo, tools,
  400. patterns, preferred)
  401. gobject.idle_add(self.fillmodel, repo, model, sa, sb)
  402. def fillmodel(self, repo, model, sa, sb):
  403. tmproot = tempfile.mkdtemp(prefix='visualdiff.')
  404. self.tmproot = tmproot
  405. self.dirs, self.revs = snapshotset(repo, self.ctxs, sa, sb, self.copies, tmproot)[:2]
  406. def get_status(file, mod, add, rem):
  407. if file in mod:
  408. return 'M'
  409. if file in add:
  410. return 'A'
  411. if file in rem:
  412. return 'R'
  413. return ' '
  414. mod_a, add_a, rem_a = sa
  415. for f in sorted(mod_a | add_a | rem_a):
  416. model.append([get_status(f, mod_a, add_a, rem_a), hglib.toutf(f)])
  417. self.connect('response', self.response)
  418. def search_filelist(self, model, column, key, iter):
  419. 'case insensitive filename search'
  420. key = key.lower()
  421. if key in model.get_value(iter, 1).lower():
  422. return False
  423. return True
  424. def toolselect(self, combo, tools):
  425. 'user selected a tool from the tool combo'
  426. sel = combo.get_active_text()
  427. if sel in tools:
  428. self.diffpath, self.diffopts, self.mergeopts = tools[sel]
  429. self.update_diff_buttons(sel)
  430. def update_diff_buttons(self, tool):
  431. if hasattr(self, 'p1button'):
  432. d2 = self.ui.configbool('merge-tools', tool + '.dirdiff')
  433. d3 = self.ui.configbool('merge-tools', tool + '.dir3diff')
  434. self.p1button.set_sensitive(d2)
  435. self.p2button.set_sensitive(d2)
  436. self.p3button.set_sensitive(d3)
  437. elif hasattr(self, 'dbutton'):
  438. d2 = self.ui.configbool('merge-tools', tool + '.dirdiff')
  439. self.dbutton.set_sensitive(d2)
  440. def fileselect(self, selection, repo, combo, tools, patterns, preferred):
  441. 'user selected a file, pick an appropriate tool from combo'
  442. model, path = selection.get_selected()
  443. if not path:
  444. return
  445. row = model[path]
  446. fname = row[-1]
  447. for pat, tool in patterns:
  448. mf = match.match(repo.root, '', [pat])
  449. if mf(fname):
  450. selected = tool
  451. break
  452. else:
  453. selected = preferred
  454. for i, name in enumerate(tools.iterkeys()):
  455. if name == selected:
  456. combo.set_active(i)
  457. def response(self, window, resp):
  458. self.should_live()
  459. def should_live(self):
  460. while self.tmproot:
  461. try:
  462. shutil.rmtree(self.tmproot)
  463. return False
  464. except (IOError, OSError), e:
  465. resp = gdialog.CustomPrompt(_('Unable to delete temp files'),
  466. _('Close diff tools and try again, or quit to leak files?'),
  467. self, (_('Try &Again'), _('&Quit')), 1).run()
  468. if resp == 0:
  469. continue
  470. else:
  471. return False
  472. return False
  473. def rowactivated(self, tree, *args):
  474. selection = tree.get_selection()
  475. if selection.count_selected_rows() != 1:
  476. return False
  477. model, paths = selection.get_selected_rows()
  478. self.launch(*model[paths[0]])
  479. def launch(self, st, fname):
  480. fname = hglib.fromutf(fname)
  481. source = self.copies.get(fname, None)
  482. dir1a, dir1b, dir2 = self.dirs
  483. rev1a, rev1b, rev2 = self.revs
  484. ctx1a, ctx1b, ctx2 = self.ctxs
  485. def getfile(ctx, dir, fname, source):
  486. m = ctx.manifest()
  487. if fname in m:
  488. path = os.path.join(dir, util.localpath(fname))
  489. return fname, path
  490. elif source and source in m:
  491. path = os.path.join(dir, util.localpath(source))
  492. return source, path
  493. else:
  494. nullfile = os.path.join(self.tmproot, 'empty')
  495. fp = open(nullfile, 'w')
  496. fp.close()
  497. return _nonexistant, nullfile
  498. local, file1a = getfile(ctx1a, dir1a, fname, source)
  499. if ctx1b:
  500. other, file1b = getfile(ctx1b, dir1b, fname, source)
  501. else:
  502. other = fname
  503. file1b = None
  504. fname, file2 = getfile(ctx2, dir2, fname, None)
  505. label1a = local+rev1a
  506. label1b = other+rev1b
  507. label2 = fname+rev2
  508. if ctx1b:
  509. label1a += '[local]'
  510. label1b += '[other]'
  511. label2 += '[merged]'
  512. # Function to quote file/dir names in the argument string
  513. replace = dict(parent=file1a, parent1=file1a, plabel1=label1a,
  514. parent2=file1b, plabel2=label1b,
  515. repo=self.reponame,
  516. phash1=str(ctx1a), phash2=str(ctx1b), chash=str(ctx2),
  517. clabel=label2, child=file2)
  518. args = ctx1b and self.mergeopts or self.diffopts
  519. launchtool(self.diffpath, args, replace, False)
  520. def p1dirdiff(self, button):
  521. dir1a, dir1b, dir2 = self.dirs
  522. rev1a, rev1b, rev2 = self.revs
  523. ctx1a, ctx1b, ctx2 = self.ctxs
  524. replace = dict(parent=dir1a, parent1=dir1a, plabel1=rev1a,
  525. repo=self.reponame,
  526. phash1=str(ctx1a), phash2=str(ctx1b), chash=str(ctx2),
  527. parent2='', plabel2='', clabel=rev2, child=dir2)
  528. launchtool(self.diffpath, self.diffopts, replace, False)
  529. def p2dirdiff(self, button):
  530. dir1a, dir1b, dir2 = self.dirs
  531. rev1a, rev1b, rev2 = self.revs
  532. ctx1a, ctx1b, ctx2 = self.ctxs
  533. replace = dict(parent=dir1b, parent1=dir1b, plabel1=rev1b,
  534. repo=self.reponame,
  535. phash1=str(ctx1a), phash2=str(ctx1b), chash=str(ctx2),
  536. parent2='', plabel2='', clabel=rev2, child=dir2)
  537. launchtool(self.diffpath, self.diffopts, replace, False)
  538. def threewaydirdiff(self, button):
  539. dir1a, dir1b, dir2 = self.dirs
  540. rev1a, rev1b, rev2 = self.revs
  541. ctx1a, ctx1b, ctx2 = self.ctxs
  542. replace = dict(parent=dir1a, parent1=dir1a, plabel1=rev1a,
  543. repo=self.reponame,
  544. phash1=str(ctx1a), phash2=str(ctx1b), chash=str(ctx2),
  545. parent2=dir1b, plabel2=rev1b, clabel=dir2, child=rev2)
  546. launchtool(self.diffpath, self.mergeopts, replace, False)
  547. def run(ui, *pats, **opts):
  548. try:
  549. path = opts.get('bundle') or paths.find_root()
  550. repo = hg.repository(ui, path=path)
  551. except error.RepoError:
  552. ui.warn(_('No repository found here') + '\n')
  553. return None
  554. pats = hglib.canonpaths(pats)
  555. if opts.get('canonpats'):
  556. pats = list(pats) + opts['canonpats']
  557. return visualdiff(ui, repo, pats, opts)