/tortoisehg/hgqt/update.py

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