PageRenderTime 66ms CodeModel.GetById 13ms app.highlight 48ms RepoModel.GetById 1ms app.codeStats 1ms

/tortoisehg/hgqt/resolve.py

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