PageRenderTime 71ms CodeModel.GetById 20ms app.highlight 46ms RepoModel.GetById 2ms app.codeStats 0ms

/tortoisehg/hgqt/update.py

https://bitbucket.org/tortoisehg/hgtk/
Python | 325 lines | 258 code | 40 blank | 27 comment | 45 complexity | 05d41df83cc633e47e5d5c7d87a2b29b MD5 | raw file
  1# update.py - Update dialog for TortoiseHg
  2#
  3# Copyright 2007 TK Soh <teekaysoh@gmail.com>
  4# Copyright 2007 Steve Borho <steve@borho.org>
  5# Copyright 2010 Yuki KODAMA <endflow.net@gmail.com>
  6#
  7# This software may be used and distributed according to the terms of the
  8# GNU General Public License version 2, incorporated herein by reference.
  9
 10from PyQt4.QtCore import *
 11from PyQt4.QtGui import *
 12
 13from mercurial import error, merge as mergemod
 14
 15from tortoisehg.util import hglib, paths
 16from tortoisehg.hgqt.i18n import _
 17from tortoisehg.hgqt import cmdui, csinfo, qtlib, thgrepo, resolve
 18
 19class UpdateDialog(QDialog):
 20
 21    output = pyqtSignal(QString, QString)
 22    progress = pyqtSignal(QString, object, QString, QString, object)
 23    makeLogVisible = pyqtSignal(bool)
 24
 25    def __init__(self, repo, rev=None, parent=None, opts={}):
 26        super(UpdateDialog, self).__init__(parent)
 27        self.setWindowFlags(self.windowFlags() & \
 28                            ~Qt.WindowContextHelpButtonHint)
 29
 30        self._finished = False
 31        self.repo = repo
 32
 33        # base layout box
 34        box = QVBoxLayout()
 35        box.setSpacing(6)
 36
 37        ## main layout grid
 38        grid = QGridLayout()
 39        grid.setSpacing(6)
 40        box.addLayout(grid)
 41
 42        ### target revision combo
 43        self.rev_combo = combo = QComboBox()
 44        combo.setEditable(True)
 45        grid.addWidget(QLabel(_('Update to:')), 0, 0)
 46        grid.addWidget(combo, 0, 1)
 47
 48        if rev is None:
 49            rev = self.repo.dirstate.branch()
 50        else:
 51            rev = str(rev)
 52        combo.addItem(hglib.tounicode(rev))
 53        combo.setCurrentIndex(0)
 54        for name in hglib.getlivebranch(self.repo):
 55            combo.addItem(hglib.tounicode(name))
 56
 57        tags = list(self.repo.tags())
 58        tags.sort()
 59        tags.reverse()
 60        for tag in tags:
 61            combo.addItem(hglib.tounicode(tag))
 62
 63        ### target revision info
 64        items = ('%(rev)s', ' %(branch)s', ' %(tags)s', '<br />%(summary)s')
 65        style = csinfo.labelstyle(contents=items, width=350, selectable=True)
 66        factory = csinfo.factory(self.repo, style=style)
 67        self.target_info = factory()
 68        grid.addWidget(QLabel(_('Target:')), 1, 0, Qt.AlignLeft | Qt.AlignTop)
 69        grid.addWidget(self.target_info, 1, 1)
 70
 71        ### parent revision info
 72        self.ctxs = self.repo[None].parents()
 73        if len(self.ctxs) == 2:
 74            self.p1_info = factory()
 75            grid.addWidget(QLabel(_('Parent 1:')), 2, 0, Qt.AlignLeft | Qt.AlignTop)
 76            grid.addWidget(self.p1_info, 2, 1)
 77            self.p2_info = factory()
 78            grid.addWidget(QLabel(_('Parent 2:')), 3, 0, Qt.AlignLeft | Qt.AlignTop)
 79            grid.addWidget(self.p2_info, 3, 1)
 80        else:
 81            self.p1_info = factory()
 82            grid.addWidget(QLabel(_('Parent:')), 2, 0, Qt.AlignLeft | Qt.AlignTop)
 83            grid.addWidget(self.p1_info, 2, 1)
 84
 85        ### options
 86        optbox = QVBoxLayout()
 87        optbox.setSpacing(6)
 88        expander = qtlib.ExpanderLabel(_('Options:'), False)
 89        expander.expanded.connect(self.show_options)
 90        row = grid.rowCount()
 91        grid.addWidget(expander, row, 0, Qt.AlignLeft | Qt.AlignTop)
 92        grid.addLayout(optbox, row, 1)
 93
 94        self.discard_chk = QCheckBox(_('Discard local changes, no backup'
 95                                       ' (-C/--clean)'))
 96        self.merge_chk = QCheckBox(_('Always merge (when possible)'))
 97        self.autoresolve_chk = QCheckBox(_('Automatically resolve merge conflicts '
 98                                           'where possible'))
 99        self.showlog_chk = QCheckBox(_('Always show command log'))
100        optbox.addWidget(self.discard_chk)
101        optbox.addWidget(self.merge_chk)
102        optbox.addWidget(self.autoresolve_chk)
103        optbox.addWidget(self.showlog_chk)
104
105        self.discard_chk.setChecked(bool(opts.get('clean')))
106        self.autoresolve_chk.setChecked(
107            repo.ui.configbool('tortoisehg', 'autoresolve', False))
108
109        ## command widget
110        self.cmd = cmdui.Widget()
111        self.cmd.commandStarted.connect(self.command_started)
112        self.cmd.commandFinished.connect(self.command_finished)
113        self.cmd.commandCanceling.connect(self.command_canceling)
114        self.cmd.output.connect(self.output)
115        self.cmd.makeLogVisible.connect(self.makeLogVisible)
116        self.cmd.progress.connect(self.progress)
117        box.addWidget(self.cmd)
118
119        ## bottom buttons
120        buttons = QDialogButtonBox()
121        self.cancel_btn = buttons.addButton(QDialogButtonBox.Cancel)
122        self.cancel_btn.clicked.connect(self.cancel_clicked)
123        self.close_btn = buttons.addButton(QDialogButtonBox.Close)
124        self.close_btn.clicked.connect(self.reject)
125        self.close_btn.setAutoDefault(False)
126        self.update_btn = buttons.addButton(_('&Update'),
127                                            QDialogButtonBox.ActionRole)
128        self.update_btn.clicked.connect(self.update)
129        self.detail_btn = buttons.addButton(_('Detail'),
130                                            QDialogButtonBox.ResetRole)
131        self.detail_btn.setAutoDefault(False)
132        self.detail_btn.setCheckable(True)
133        self.detail_btn.toggled.connect(self.detail_toggled)
134        box.addWidget(buttons)
135
136        # signal handlers
137        self.rev_combo.editTextChanged.connect(lambda *a: self.update_info())
138        self.discard_chk.toggled.connect(lambda *a: self.update_info())
139
140        # dialog setting
141        self.setLayout(box)
142        self.layout().setSizeConstraint(QLayout.SetFixedSize)
143        self.setWindowTitle(_('Update - %s') % self.repo.displayname)
144        self.setWindowIcon(qtlib.geticon('update'))
145
146        # prepare to show
147        self.rev_combo.lineEdit().selectAll()
148        self.cmd.setHidden(True)
149        self.cancel_btn.setHidden(True)
150        self.detail_btn.setHidden(True)
151        self.merge_chk.setHidden(True)
152        self.autoresolve_chk.setHidden(True)
153        self.showlog_chk.setHidden(True)
154        self.update_info()
155
156    ### Private Methods ###
157
158    def update_info(self):
159        self.p1_info.update(self.ctxs[0].node())
160        merge = len(self.ctxs) == 2
161        if merge:
162            self.p2_info.update(self.ctxs[1])
163        new_rev = hglib.fromunicode(self.rev_combo.currentText())
164        if new_rev.lower() == 'null':
165            self.update_btn.setEnabled(True)
166            return
167        try:
168            new_ctx = self.repo[new_rev]
169            if not merge and new_ctx.rev() == self.ctxs[0].rev():
170                self.target_info.setText(_('(same as parent)'))
171                clean = self.discard_chk.isChecked()
172                self.update_btn.setEnabled(clean)
173            else:
174                self.target_info.update(self.repo[new_rev])
175                self.update_btn.setEnabled(True)
176        except (error.LookupError, error.RepoLookupError, error.RepoError):
177            self.target_info.setText(_('unknown revision!'))
178            self.update_btn.setDisabled(True)
179
180    def update(self):
181        cmdline = ['update', '--repository', self.repo.root, '--verbose']
182        cmdline += ['--config', 'ui.merge=internal:' +
183                    (self.autoresolve_chk.isChecked() and 'merge' or 'fail')]
184        rev = hglib.fromunicode(self.rev_combo.currentText())
185        cmdline.append('--rev')
186        cmdline.append(rev)
187
188        if self.discard_chk.isChecked():
189            cmdline.append('--clean')
190        else:
191            cur = self.repo['.']
192            try:
193                node = self.repo[rev]
194            except (error.LookupError, error.RepoLookupError, error.RepoError):
195                return
196            def isclean():
197                '''whether WD is changed'''
198                wc = self.repo[None]
199                return not (wc.modified() or wc.added() or wc.removed())
200            def ismergedchange():
201                '''whether the local changes are merged (have 2 parents)'''
202                wc = self.repo[None]
203                return len(wc.parents()) == 2
204            def iscrossbranch(p1, p2):
205                '''whether p1 -> p2 crosses branch'''
206                pa = p1.ancestor(p2)
207                return p1.branch() != p2.branch() or (p1 != pa and p2 != pa)
208            def islocalmerge(p1, p2, clean=None):
209                if clean is None:
210                    clean = isclean()
211                pa = p1.ancestor(p2)
212                return not clean and (p1 == pa or p2 == pa)
213            def confirmupdate(clean=None):
214                if clean is None:
215                    clean = isclean()
216
217                msg = _('Detected uncommitted local changes in working tree.\n'
218                        'Please select to continue:\n\n')
219                data = {'discard': (_('&Discard'),
220                                    _('Discard - discard local changes, no backup')),
221                        'shelve': (_('&Shelve'),
222                                  _('Shelve - move local changes to a patch')),
223                        'merge': (_('&Merge'),
224                                  _('Merge - allow to merge with local changes')),}
225
226                opts = [data['discard']]
227                if not ismergedchange():
228                    opts.append(data['shelve'])
229                if islocalmerge(cur, node, clean):
230                    opts.append(data['merge'])
231
232                msg += '\n'.join([desc for label, desc in opts if desc])
233                dlg = QMessageBox(QMessageBox.Question, _('Confirm Update'),
234                                  msg, QMessageBox.Cancel, self)
235                buttons = {}
236                for name in ('discard', 'shelve', 'merge'):
237                    label, desc = data[name]
238                    buttons[name] = dlg.addButton(label, QMessageBox.ActionRole)
239                dlg.exec_()
240                return buttons, dlg.clickedButton()
241
242            # If merge-by-default, we want to merge whenever possible,
243            # without prompting user (similar to command-line behavior)
244            defaultmerge = self.merge_chk.isChecked()
245            clean = isclean()
246            if clean:
247                cmdline.append('--check')
248            elif not (defaultmerge and islocalmerge(cur, node, clean)):
249                buttons, clicked = confirmupdate(clean)
250                if buttons['discard'] == clicked:
251                    cmdline.append('--clean')
252                elif buttons['shelve'] == clicked:
253                    from tortoisehg.hgqt import shelve
254                    dlg = shelve.ShelveDialog(self.repo, self)
255                    dlg.finished.connect(dlg.deleteLater)
256                    dlg.exec_()
257                    return
258                elif buttons['merge'] == clicked:
259                    pass # no args
260                else:
261                    return
262
263        # start updating
264        self.repo.incrementBusyCount()
265        self.cmd.run(cmdline)
266
267    ### Signal Handlers ###
268
269    def cancel_clicked(self):
270        self.cmd.cancel()
271        self.reject()
272
273    def detail_toggled(self, checked):
274        self.cmd.setShowOutput(checked)
275
276    def show_options(self, visible):
277        self.merge_chk.setShown(visible)
278        self.autoresolve_chk.setShown(visible)
279        self.showlog_chk.setShown(visible)
280
281    def command_started(self):
282        self.cmd.setShown(True)
283        if self.showlog_chk.isChecked():
284            self.detail_btn.setChecked(True)
285        self.update_btn.setHidden(True)
286        self.close_btn.setHidden(True)
287        self.cancel_btn.setShown(True)
288        self.detail_btn.setShown(True)
289
290    def command_finished(self, ret):
291        self.repo.decrementBusyCount()
292        if ret not in (0, 1) or self.cmd.outputShown():
293            self.detail_btn.setChecked(True)
294            self.close_btn.setShown(True)
295            self.close_btn.setAutoDefault(True)
296            self.close_btn.setFocus()
297            self.cancel_btn.setHidden(True)
298        else:
299            self.accept()
300
301    def accept(self):
302        ms = mergemod.mergestate(self.repo)
303        for path in ms:
304            if ms[path] == 'u':
305                qtlib.InfoMsgBox(_('Merge caused file conflicts'),
306                                 _('File conflicts need to be resolved'))
307                dlg = resolve.ResolveDialog(self.repo, self)
308                dlg.finished.connect(dlg.deleteLater)
309                dlg.exec_()
310                break
311        super(UpdateDialog, self).accept()
312
313    def command_canceling(self):
314        self.cancel_btn.setDisabled(True)
315
316def run(ui, *pats, **opts):
317    from tortoisehg.util import paths
318    from tortoisehg.hgqt import thgrepo
319    repo = thgrepo.repository(ui, path=paths.find_root())
320    rev = None
321    if opts.get('rev'):
322        rev = opts.get('rev')
323    elif len(pats) == 1:
324        rev = pats[0]
325    return UpdateDialog(repo, rev, None, opts)