/tortoisehg/hgqt/resolve.py

https://bitbucket.org/tortoisehg/hgtk/ · Python · 411 lines · 348 code · 57 blank · 6 comment · 49 complexity · 8af9b0654515a6a7aef9f5d4ef929cf0 MD5 · raw file

  1. # resolve.py - TortoiseHg merge conflict resolve
  2. #
  3. # Copyright 2010 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. from PyQt4.QtCore import *
  8. from PyQt4.QtGui import *
  9. import os
  10. from mercurial import merge as mergemod
  11. from tortoisehg.util import hglib
  12. from tortoisehg.hgqt.i18n import _
  13. from tortoisehg.hgqt import qtlib, cmdui, wctxactions, visdiff
  14. MARGINS = (8, 0, 0, 0)
  15. class ResolveDialog(QDialog):
  16. def __init__(self, repo, parent=None):
  17. super(ResolveDialog, self).__init__(parent)
  18. self.setWindowFlags(Qt.Window)
  19. self.setWindowTitle(_('Resolve conflicts - %s') % repo.displayname)
  20. self.setWindowIcon(qtlib.geticon('merge'))
  21. self.repo = repo
  22. s = QSettings()
  23. self.restoreGeometry(s.value('resolve/geom').toByteArray())
  24. box = QVBoxLayout()
  25. box.setSpacing(5)
  26. self.setLayout(box)
  27. self.stlabel = QLabel()
  28. box.addWidget(self.stlabel)
  29. unres = qtlib.LabeledSeparator(_('Unresolved conflicts'))
  30. self.layout().addWidget(unres)
  31. hbox = QHBoxLayout()
  32. hbox.setSpacing(0)
  33. hbox.setContentsMargins(*MARGINS)
  34. self.layout().addLayout(hbox)
  35. self.utree = PathsTree(self.repo, self)
  36. hbox.addWidget(self.utree)
  37. vbox = QVBoxLayout()
  38. vbox.setContentsMargins(*MARGINS)
  39. hbox.addLayout(vbox)
  40. auto = QPushButton(_('Auto Resolve'))
  41. auto.setToolTip(_('Attempt automatic merge'))
  42. auto.clicked.connect(lambda: self.merge('internal:merge'))
  43. manual = QPushButton(_('Manual Resolve'))
  44. manual.setToolTip(_('Merge with selected merge tool'))
  45. manual.clicked.connect(self.merge)
  46. local = QPushButton(_('Take Local'))
  47. local.setToolTip(_('Accept the local file version (yours)'))
  48. local.clicked.connect(lambda: self.merge('internal:local'))
  49. other = QPushButton(_('Take Other'))
  50. other.setToolTip(_('Accept the other file version (theirs)'))
  51. other.clicked.connect(lambda: self.merge('internal:other'))
  52. res = QPushButton(_('Mark as Resolved'))
  53. res.setToolTip(_('Mark this file as resolved'))
  54. res.clicked.connect(self.markresolved)
  55. vbox.addWidget(auto)
  56. vbox.addWidget(manual)
  57. vbox.addWidget(local)
  58. vbox.addWidget(other)
  59. vbox.addWidget(res)
  60. vbox.addStretch(1)
  61. self.ubuttons = (auto, manual, local, other, res)
  62. res = qtlib.LabeledSeparator(_('Resolved conflicts'))
  63. self.layout().addWidget(res)
  64. hbox = QHBoxLayout()
  65. hbox.setContentsMargins(*MARGINS)
  66. hbox.setSpacing(0)
  67. self.layout().addLayout(hbox)
  68. self.rtree = PathsTree(self.repo, self)
  69. hbox.addWidget(self.rtree)
  70. vbox = QVBoxLayout()
  71. vbox.setContentsMargins(*MARGINS)
  72. hbox.addLayout(vbox)
  73. edit = QPushButton(_('Edit File'))
  74. edit.setToolTip(_('Edit resolved file'))
  75. edit.clicked.connect(self.edit)
  76. v3way = QPushButton(_('3-Way Diff'))
  77. v3way.setToolTip(_('Visual three-way diff'))
  78. v3way.clicked.connect(self.v3way)
  79. vp0 = QPushButton(_('Diff to Local'))
  80. vp0.setToolTip(_('Visual diff between resolved file and first parent'))
  81. vp0.clicked.connect(self.vp0)
  82. vp1 = QPushButton(_('Diff to Other'))
  83. vp1.setToolTip(_('Visual diff between resolved file and second parent'))
  84. vp1.clicked.connect(self.vp1)
  85. ures = QPushButton(_('Mark as Unresolved'))
  86. ures.setToolTip(_('Mark this file as unresolved'))
  87. ures.clicked.connect(self.markunresolved)
  88. vbox.addWidget(edit)
  89. vbox.addWidget(v3way)
  90. vbox.addWidget(vp0)
  91. vbox.addWidget(vp1)
  92. vbox.addWidget(ures)
  93. vbox.addStretch(1)
  94. self.rbuttons = (edit, vp0, ures)
  95. self.rmbuttons = (vp1, v3way)
  96. hbox = QHBoxLayout()
  97. hbox.setContentsMargins(*MARGINS)
  98. hbox.setSpacing(4)
  99. self.layout().addLayout(hbox)
  100. self.tcombo = ToolsCombo(self.repo, self)
  101. hbox.addWidget(QLabel(_('Detected merge/diff tools:')))
  102. hbox.addWidget(self.tcombo)
  103. hbox.addStretch(1)
  104. out = qtlib.LabeledSeparator(_('Command output'))
  105. self.layout().addWidget(out)
  106. self.cmd = cmdui.Widget(True, self)
  107. self.cmd.commandFinished.connect(self.refresh)
  108. self.cmd.setShowOutput(True)
  109. self.layout().addWidget(self.cmd)
  110. BB = QDialogButtonBox
  111. bbox = QDialogButtonBox(BB.Ok|BB.Close)
  112. bbox.button(BB.Ok).setText('Refresh')
  113. bbox.accepted.connect(self.refresh)
  114. bbox.rejected.connect(self.reject)
  115. self.layout().addWidget(bbox)
  116. self.bbox = bbox
  117. self.refresh()
  118. self.utree.selectAll()
  119. self.utree.setFocus()
  120. repo.configChanged.connect(self.configChanged)
  121. repo.repositoryChanged.connect(self.repositoryChanged)
  122. def repositoryChanged(self):
  123. self.refresh()
  124. def getSelectedPaths(self, tree):
  125. paths = []
  126. repo = self.repo
  127. if not tree.selectionModel():
  128. return paths
  129. for idx in tree.selectionModel().selectedRows():
  130. path = hglib.fromunicode(idx.data().toString())
  131. paths.append(repo.wjoin(path))
  132. return paths
  133. def merge(self, tool=False):
  134. if not tool:
  135. tool = self.tcombo.readValue()
  136. cmd = ['resolve', '--repository', self.repo.root]
  137. if tool:
  138. cmd += ['--tool='+tool]
  139. paths = self.getSelectedPaths(self.utree)
  140. if paths:
  141. self.cmd.run(cmd + paths)
  142. def markresolved(self):
  143. paths = self.getSelectedPaths(self.utree)
  144. if paths:
  145. self.cmd.run(['resolve', '--repository', self.repo.root,
  146. '--mark'] + paths)
  147. def markunresolved(self):
  148. paths = self.getSelectedPaths(self.rtree)
  149. if paths:
  150. self.cmd.run(['resolve', '--repository', self.repo.root,
  151. '--unmark'] + paths)
  152. def edit(self):
  153. paths = self.getSelectedPaths(self.rtree)
  154. if paths:
  155. wctxactions.edit(self, self.repo.ui, self.repo, paths)
  156. def v3way(self):
  157. paths = self.getSelectedPaths(self.rtree)
  158. if paths:
  159. opts = {}
  160. opts['rev'] = []
  161. opts['tool'] = self.tcombo.readValue()
  162. dlg = visdiff.visualdiff(self.repo.ui, self.repo, paths, opts)
  163. if dlg:
  164. dlg.exec_()
  165. def vp0(self):
  166. paths = self.getSelectedPaths(self.rtree)
  167. if paths:
  168. opts = {}
  169. opts['rev'] = ["p1()"]
  170. opts['tool'] = self.tcombo.readValue()
  171. dlg = visdiff.visualdiff(self.repo.ui, self.repo, paths, opts)
  172. if dlg:
  173. dlg.exec_()
  174. def vp1(self):
  175. paths = self.getSelectedPaths(self.rtree)
  176. if paths:
  177. opts = {}
  178. opts['rev'] = ["p2()"]
  179. opts['tool'] = self.tcombo.readValue()
  180. dlg = visdiff.visualdiff(self.repo.ui, self.repo, paths, opts)
  181. if dlg:
  182. dlg.exec_()
  183. def configChanged(self):
  184. 'repository has detected a change to config files'
  185. self.tcombo.reset()
  186. def refresh(self):
  187. repo = self.repo
  188. def selpaths(tree):
  189. paths = []
  190. if not tree.selectionModel():
  191. return paths
  192. for idx in tree.selectionModel().selectedRows():
  193. path = hglib.fromunicode(idx.data().toString())
  194. paths.append(path)
  195. return paths
  196. ms = mergemod.mergestate(self.repo)
  197. u, r = [], []
  198. for path in ms:
  199. if ms[path] == 'u':
  200. u.append(path)
  201. else:
  202. r.append(path)
  203. paths = selpaths(self.utree)
  204. self.utree.setModel(PathsModel(u, self))
  205. self.utree.resizeColumnToContents(0)
  206. self.utree.resizeColumnToContents(1)
  207. model = self.utree.model()
  208. smodel = self.utree.selectionModel()
  209. sflags = QItemSelectionModel.Select | QItemSelectionModel.Columns
  210. for i, p in enumerate(u):
  211. if p in paths:
  212. smodel.select(model.index(i, 0), sflags)
  213. smodel.select(model.index(i, 1), sflags)
  214. @pyqtSlot(QItemSelection, QItemSelection)
  215. def uchanged(selected, deselected):
  216. enable = self.utree.selectionModel().hasSelection()
  217. for b in self.ubuttons:
  218. b.setEnabled(enable)
  219. smodel.selectionChanged.connect(uchanged)
  220. uchanged(None, None)
  221. paths = selpaths(self.rtree)
  222. self.rtree.setModel(PathsModel(r, self))
  223. self.rtree.resizeColumnToContents(0)
  224. self.rtree.resizeColumnToContents(1)
  225. model = self.rtree.model()
  226. smodel = self.rtree.selectionModel()
  227. for i, p in enumerate(r):
  228. if p in paths:
  229. smodel.select(model.index(i, 0), sflags)
  230. smodel.select(model.index(i, 1), sflags)
  231. @pyqtSlot(QItemSelection, QItemSelection)
  232. def rchanged(selected, deselected):
  233. enable = self.rtree.selectionModel().hasSelection()
  234. for b in self.rbuttons:
  235. b.setEnabled(enable)
  236. merge = len(self.repo.parents()) > 1
  237. for b in self.rmbuttons:
  238. b.setEnabled(enable and merge)
  239. smodel.selectionChanged.connect(rchanged)
  240. rchanged(None, None)
  241. if u:
  242. txt = _('There are merge <b>conflicts</b> to be resolved')
  243. elif r:
  244. txt = _('All conflicts are resolved.')
  245. else:
  246. txt = _('There are no conflicting file merges.')
  247. self.stlabel.setText(u'<h2>' + txt + u'</h2>')
  248. def reject(self):
  249. s = QSettings()
  250. s.setValue('resolve/geom', self.saveGeometry())
  251. if len(self.utree.model()):
  252. main = _('Quit without finishing resolve?')
  253. text = _('Unresolved conflicts remain. Are you sure?')
  254. labels = ((QMessageBox.Yes, _('&Quit')),
  255. (QMessageBox.No, _('Cancel')))
  256. if not qtlib.QuestionMsgBox(_('Confirm Exit'), main, text,
  257. labels=labels, parent=self):
  258. return
  259. super(ResolveDialog, self).reject()
  260. class PathsTree(QTreeView):
  261. def __init__(self, repo, parent):
  262. QTreeView.__init__(self, parent)
  263. self.repo = repo
  264. self.setSelectionMode(QTreeView.ExtendedSelection)
  265. self.setSortingEnabled(True)
  266. def dragObject(self):
  267. urls = []
  268. for index in self.selectionModel().selectedRows():
  269. index = index.sibling(index.row(), COL_PATH)
  270. path = index.data(Qt.DisplayRole).toString()
  271. u = QUrl()
  272. u.setPath('file://' + os.path.join(self.repo.root, path))
  273. urls.append(u)
  274. if urls:
  275. d = QDrag(self)
  276. m = QMimeData()
  277. m.setUrls(urls)
  278. d.setMimeData(m)
  279. d.start(Qt.CopyAction)
  280. def mousePressEvent(self, event):
  281. self.pressPos = event.pos()
  282. self.pressTime = QTime.currentTime()
  283. return QTreeView.mousePressEvent(self, event)
  284. def mouseMoveEvent(self, event):
  285. d = event.pos() - self.pressPos
  286. if d.manhattanLength() < QApplication.startDragDistance():
  287. return QTreeView.mouseMoveEvent(self, event)
  288. elapsed = self.pressTime.msecsTo(QTime.currentTime())
  289. if elapsed < QApplication.startDragTime():
  290. return QTreeView.mouseMoveEvent(self, event)
  291. self.dragObject()
  292. return QTreeView.mouseMoveEvent(self, event)
  293. class PathsModel(QAbstractTableModel):
  294. def __init__(self, pathlist, parent):
  295. QAbstractTableModel.__init__(self, parent)
  296. self.headers = (_('Path'), _('Extension'))
  297. self.rows = []
  298. for path in pathlist:
  299. name, ext = os.path.splitext(path)
  300. self.rows.append([path, ext])
  301. def __len__(self):
  302. return len(self.rows)
  303. def rowCount(self, parent):
  304. if parent.isValid():
  305. return 0 # no child
  306. return len(self.rows)
  307. def columnCount(self, parent):
  308. if parent.isValid():
  309. return 0 # no child
  310. return len(self.headers)
  311. def data(self, index, role):
  312. if not index.isValid():
  313. return QVariant()
  314. if role == Qt.DisplayRole:
  315. data = self.rows[index.row()][index.column()]
  316. return QVariant(hglib.tounicode(data))
  317. return QVariant()
  318. def headerData(self, col, orientation, role):
  319. if role != Qt.DisplayRole or orientation != Qt.Horizontal:
  320. return QVariant()
  321. else:
  322. return QVariant(self.headers[col])
  323. class ToolsCombo(QComboBox):
  324. def __init__(self, repo, parent):
  325. QComboBox.__init__(self, parent)
  326. self.setEditable(False)
  327. self.loaded = False
  328. self.default = _('<default>')
  329. self.addItem(self.default)
  330. self.repo = repo
  331. def reset(self):
  332. self.loaded = False
  333. self.clear()
  334. self.addItem(self.default)
  335. def showPopup(self):
  336. if not self.loaded:
  337. self.loaded = True
  338. self.clear()
  339. self.addItem(self.default)
  340. for t in self.repo.mergetools:
  341. self.addItem(hglib.tounicode(t))
  342. QComboBox.showPopup(self)
  343. def readValue(self):
  344. if self.loaded:
  345. text = self.currentText()
  346. if text != self.default:
  347. return hglib.fromunicode(text)
  348. else:
  349. return None
  350. def run(ui, *pats, **opts):
  351. from tortoisehg.util import paths
  352. from tortoisehg.hgqt import thgrepo
  353. repo = thgrepo.repository(ui, path=paths.find_root())
  354. return ResolveDialog(repo, None)