/tortoisehg/hgqt/commit.py

https://bitbucket.org/tortoisehg/hgtk/ · Python · 975 lines · 826 code · 119 blank · 30 comment · 161 complexity · 247a839cfe2d8366b5c5190f44b6d906 MD5 · raw file

  1. # commit.py - TortoiseHg's commit widget and standalone dialog
  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. import os
  8. from mercurial import ui, util, error
  9. from PyQt4.QtCore import *
  10. from PyQt4.QtGui import *
  11. from PyQt4.Qsci import QsciScintilla, QsciAPIs, QsciLexerMakefile
  12. from tortoisehg.hgqt.i18n import _
  13. from tortoisehg.util import hglib, shlib, wconfig
  14. from tortoisehg.hgqt import qtlib, qscilib, status, cmdui, branchop, revpanel
  15. from tortoisehg.hgqt.sync import loadIniFile
  16. # Technical Debt for CommitWidget
  17. # disable commit button while no message is entered or no files are selected
  18. # qtlib decode failure dialog (ask for retry locale, suggest HGENCODING)
  19. # spell check / tab completion
  20. # in-memory patching / committing chunk selected files
  21. class MessageEntry(qscilib.Scintilla):
  22. def __init__(self, parent=None):
  23. super(MessageEntry, self).__init__(parent)
  24. self.setEdgeColor(QColor('LightSalmon'))
  25. self.setEdgeMode(QsciScintilla.EdgeLine)
  26. self.setReadOnly(False)
  27. self.setMarginWidth(1, 0)
  28. self.setFont(qtlib.getfont('fontcomment').font())
  29. self.setCaretWidth(10)
  30. self.setCaretLineBackgroundColor(QColor("#e6fff0"))
  31. self.setCaretLineVisible(True)
  32. self.setAutoIndent(True)
  33. self.setAutoCompletionThreshold(2)
  34. self.setAutoCompletionSource(QsciScintilla.AcsAPIs)
  35. self.setAutoCompletionFillupsEnabled(True)
  36. self.setLexer(QsciLexerMakefile(self))
  37. self.lexer().setFont(qtlib.getfont('fontcomment').font())
  38. self.lexer().setColor(QColor(Qt.red), QsciLexerMakefile.Error)
  39. self.setMatchedBraceBackgroundColor(Qt.yellow)
  40. self.setIndentationsUseTabs(False)
  41. self.setBraceMatching(QsciScintilla.SloppyBraceMatch)
  42. #self.setIndentationGuidesBackgroundColor(QColor("#e6e6de"))
  43. #self.setFolding(QsciScintilla.BoxedFoldStyle)
  44. # http://www.riverbankcomputing.com/pipermail/qscintilla/2009-February/000461.html
  45. self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
  46. self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
  47. def refresh(self, repo):
  48. self.setEdgeColumn(repo.summarylen)
  49. self.setIndentationWidth(repo.tabwidth)
  50. self.setTabWidth(repo.tabwidth)
  51. self.summarylen = repo.summarylen
  52. def reflowBlock(self, line):
  53. lines = self.text().split('\n', QString.KeepEmptyParts)
  54. if line >= len(lines):
  55. return None
  56. if not len(lines[line]) > 1:
  57. return line+1
  58. # find boundaries (empty lines or bounds)
  59. b = line
  60. while b and len(lines[b-1]) > 1:
  61. b = b - 1
  62. e = line
  63. while e+1 < len(lines) and len(lines[e+1]) > 1:
  64. e = e + 1
  65. group = QStringList([lines[l].simplified() for l in xrange(b, e+1)])
  66. sentence = group.join(' ')
  67. parts = sentence.split(' ', QString.SkipEmptyParts)
  68. outlines = QStringList()
  69. line = QStringList()
  70. partslen = 0
  71. for part in parts:
  72. if partslen + len(line) + len(part) + 1 > self.summarylen:
  73. if line:
  74. outlines.append(line.join(' '))
  75. line, partslen = QStringList(), 0
  76. line.append(part)
  77. partslen += len(part)
  78. if line:
  79. outlines.append(line.join(' '))
  80. self.beginUndoAction()
  81. self.setSelection(b, 0, e+1, 0)
  82. self.removeSelectedText()
  83. self.insertAt(outlines.join('\n')+'\n', b, 0)
  84. self.endUndoAction()
  85. self.setCursorPosition(b, 0)
  86. return b + len(outlines) + 1
  87. def keyPressEvent(self, event):
  88. if event.modifiers() == Qt.ControlModifier and event.key() == Qt.Key_E:
  89. line, col = self.getCursorPosition()
  90. self.reflowBlock(line)
  91. super(MessageEntry, self).keyPressEvent(event)
  92. class CommitWidget(QWidget):
  93. 'A widget that encompasses a StatusWidget and commit extras'
  94. commitButtonEnable = pyqtSignal(bool)
  95. linkActivated = pyqtSignal(QString)
  96. showMessage = pyqtSignal(unicode)
  97. commitComplete = pyqtSignal()
  98. progress = pyqtSignal(QString, object, QString, QString, object)
  99. output = pyqtSignal(QString, QString)
  100. makeLogVisible = pyqtSignal(bool)
  101. def __init__(self, pats, opts, root=None, embedded=False, parent=None):
  102. QWidget.__init__(self, parent=parent)
  103. self.opts = opts # user, date
  104. self.stwidget = status.StatusWidget(pats, opts, root, self)
  105. self.stwidget.showMessage.connect(self.showMessage)
  106. self.stwidget.progress.connect(self.progress)
  107. self.stwidget.linkActivated.connect(self.linkActivated)
  108. self.stwidget.fileDisplayed.connect(self.fileDisplayed)
  109. self.msghistory = []
  110. self.repo = repo = self.stwidget.repo
  111. self.runner = cmdui.Runner(_('Commit'), not embedded, self)
  112. self.runner.output.connect(self.output)
  113. self.runner.progress.connect(self.progress)
  114. self.runner.makeLogVisible.connect(self.makeLogVisible)
  115. self.runner.commandFinished.connect(self.commandFinished)
  116. repo.configChanged.connect(self.configChanged)
  117. repo.repositoryChanged.connect(self.repositoryChanged)
  118. repo.workingBranchChanged.connect(self.workingBranchChanged)
  119. self.opts['pushafter'] = repo.ui.config('tortoisehg', 'cipushafter', '')
  120. self.opts['autoinc'] = repo.ui.config('tortoisehg', 'autoinc', '')
  121. layout = QVBoxLayout()
  122. layout.setContentsMargins(2, 2, 2, 2)
  123. layout.setSpacing(0)
  124. layout.addWidget(self.stwidget)
  125. self.setLayout(layout)
  126. vbox = QVBoxLayout()
  127. vbox.setMargin(0)
  128. vbox.setSpacing(0)
  129. vbox.setContentsMargins(*(0,)*4)
  130. hbox = QHBoxLayout()
  131. hbox.setMargin(0)
  132. hbox.setContentsMargins(*(0,)*4)
  133. msgcombo = MessageHistoryCombo()
  134. msgcombo.activated.connect(self.msgSelected)
  135. hbox.addWidget(msgcombo, 1)
  136. branchbutton = QPushButton(_('Branch: '))
  137. branchbutton.pressed.connect(self.branchOp)
  138. self.branchbutton = branchbutton
  139. self.branchop = None
  140. hbox.addWidget(branchbutton)
  141. self.detailsbutton = QPushButton(_('Options'))
  142. self.detailsbutton.pressed.connect(self.details)
  143. hbox.addWidget(self.detailsbutton)
  144. vbox.addLayout(hbox, 0)
  145. self.buttonHBox = hbox
  146. self.pcsinfo = revpanel.ParentWidget(repo)
  147. vbox.addWidget(self.pcsinfo, 0)
  148. msgte = MessageEntry(self)
  149. msgte.setContextMenuPolicy(Qt.CustomContextMenu)
  150. msgte.customContextMenuRequested.connect(self.menuRequested)
  151. msgte.installEventFilter(qscilib.KeyPressInterceptor(self))
  152. vbox.addWidget(msgte, 1)
  153. upperframe = QFrame()
  154. SP = QSizePolicy
  155. sp = SP(SP.Expanding, SP.Expanding)
  156. sp.setHorizontalStretch(1)
  157. upperframe.setSizePolicy(sp)
  158. upperframe.setLayout(vbox)
  159. self.split = QSplitter(Qt.Vertical)
  160. sp = SP(SP.Expanding, SP.Expanding)
  161. sp.setHorizontalStretch(1)
  162. sp.setVerticalStretch(0)
  163. self.split.setSizePolicy(sp)
  164. # Add our widgets to the top of our splitter
  165. self.split.addWidget(upperframe)
  166. # Add status widget document frame below our splitter
  167. # this reparents the docf from the status splitter
  168. self.split.addWidget(self.stwidget.docf)
  169. # add our splitter where the docf used to be
  170. self.stwidget.split.addWidget(self.split)
  171. self.msgte = msgte
  172. self.msgcombo = msgcombo
  173. @pyqtSlot(QString, QString)
  174. def fileDisplayed(self, wfile, contents):
  175. 'Status widget is displaying a new file'
  176. if not (wfile and contents):
  177. return
  178. wfile = unicode(wfile)
  179. self._apis = QsciAPIs(self.msgte.lexer())
  180. tokens = set()
  181. for e in self.stwidget.getChecked():
  182. tokens.add(e)
  183. tokens.add(os.path.basename(e))
  184. try:
  185. from pygments.lexers import guess_lexer_for_filename
  186. from pygments.token import Token
  187. from pygments.util import ClassNotFound
  188. try:
  189. contents = unicode(contents)
  190. lexer = guess_lexer_for_filename(wfile, contents)
  191. for tokentype, value in lexer.get_tokens(contents):
  192. if tokentype in Token.Name and len(value) > 4:
  193. tokens.add(value)
  194. except ClassNotFound:
  195. pass
  196. except ImportError:
  197. pass
  198. for n in sorted(list(tokens)):
  199. self._apis.add(n)
  200. self._apis.apiPreparationFinished.connect(self.apiPrepFinished)
  201. self._apis.prepare()
  202. def apiPrepFinished(self):
  203. 'QsciAPIs has finished parsing displayed file'
  204. self.msgte.lexer().setAPIs(self._apis)
  205. def details(self):
  206. dlg = DetailsDialog(self.opts, self.userhist, self)
  207. dlg.finished.connect(dlg.deleteLater)
  208. dlg.setWindowFlags(Qt.Sheet)
  209. dlg.setWindowModality(Qt.WindowModal)
  210. if dlg.exec_() == QDialog.Accepted:
  211. self.opts.update(dlg.outopts)
  212. self.refresh()
  213. def workingBranchChanged(self):
  214. 'Repository has detected a change in .hg/branch'
  215. self.refresh()
  216. def repositoryChanged(self):
  217. 'Repository has detected a changelog / dirstate change'
  218. self.refresh()
  219. def configChanged(self):
  220. 'Repository is reporting its config files have changed'
  221. self.refresh()
  222. @pyqtSlot()
  223. def reload(self):
  224. 'User has requested a reload'
  225. self.repo.thginvalidate()
  226. self.refresh()
  227. self.stwidget.refreshWctx() # Trigger reload of working context
  228. def refresh(self):
  229. ispatch = self.repo.changectx('.').thgmqappliedpatch()
  230. self.commitButtonEnable.emit(not ispatch)
  231. self.msgte.refresh(self.repo)
  232. # Update message list
  233. self.msgcombo.reset(self.msghistory)
  234. # Update branch operation button
  235. branchu = hglib.tounicode(self.repo[None].branch())
  236. if self.branchop is None:
  237. title = _('Branch: ') + branchu
  238. elif self.branchop == False:
  239. title = _('Close Branch: ') + branchu
  240. else:
  241. title = _('New Branch: ') + self.branchop
  242. self.branchbutton.setText(title)
  243. # Update parent csinfo widget
  244. self.pcsinfo.set_revision(None)
  245. self.pcsinfo.update()
  246. def menuRequested(self, point):
  247. line = self.msgte.lineAt(point)
  248. point = self.msgte.mapToGlobal(point)
  249. def apply():
  250. line = 0
  251. while True:
  252. line = self.msgte.reflowBlock(line)
  253. if line is None:
  254. break;
  255. def paste():
  256. files = self.stwidget.getChecked()
  257. self.msgte.insert(', '.join(files))
  258. def settings():
  259. from tortoisehg.hgqt.settings import SettingsDialog
  260. dlg = SettingsDialog(True, focus='tortoisehg.summarylen')
  261. dlg.exec_()
  262. menu = self.msgte.createStandardContextMenu()
  263. menu.addSeparator()
  264. for name, func in [(_('Paste &Filenames'), paste),
  265. (_('App&ly Format'), apply),
  266. (_('C&onfigure Format'), settings)]:
  267. def add(name, func):
  268. action = menu.addAction(name)
  269. action.triggered.connect(lambda: func())
  270. add(name, func)
  271. return menu.exec_(point)
  272. def branchOp(self):
  273. d = branchop.BranchOpDialog(self.repo, self.branchop, self)
  274. d.setWindowFlags(Qt.Sheet)
  275. d.setWindowModality(Qt.WindowModal)
  276. if d.exec_() == QDialog.Accepted:
  277. self.branchop = d.branchop
  278. self.refresh()
  279. def canUndo(self):
  280. 'Returns undo description or None if not valid'
  281. if os.path.exists(self.repo.sjoin('undo')):
  282. try:
  283. args = self.repo.opener('undo.desc', 'r').read().splitlines()
  284. if args[1] != 'commit':
  285. return None
  286. return _('Rollback commit to revision %d') % (int(args[0]) - 1)
  287. except (IOError, IndexError, ValueError):
  288. pass
  289. return None
  290. def rollback(self):
  291. msg = self.canUndo()
  292. if not msg:
  293. return
  294. d = QMessageBox.question(self, _('Confirm Undo'), msg,
  295. QMessageBox.Ok | QMessageBox.Cancel)
  296. if d != QMessageBox.Ok:
  297. return
  298. self.repo.incrementBusyCount()
  299. self.repo.rollback()
  300. self.repo.decrementBusyCount()
  301. self.reload()
  302. QTimer.singleShot(500, lambda: shlib.shell_notify([self.repo.root]))
  303. def getMessage(self):
  304. text = self.msgte.text()
  305. try:
  306. text = hglib.fromunicode(text, 'strict')
  307. except UnicodeEncodeError:
  308. pass # TODO
  309. return text
  310. def msgSelected(self, index):
  311. if self.msgte.text() and self.msgte.isModified():
  312. d = QMessageBox.question(self, _('Confirm Discard Message'),
  313. _('Discard current commit message?'),
  314. QMessageBox.Ok | QMessageBox.Cancel)
  315. if d != QMessageBox.Ok:
  316. return
  317. self.setMessage(self.msghistory[index])
  318. self.msgte.setFocus()
  319. def setMessage(self, msg):
  320. self.msgte.setText(msg)
  321. lines = self.msgte.lines()
  322. if lines:
  323. lines -= 1
  324. pos = self.msgte.lineLength(lines)
  325. self.msgte.setCursorPosition(lines, pos)
  326. self.msgte.ensureLineVisible(lines)
  327. hs = self.msgte.horizontalScrollBar()
  328. hs.setSliderPosition(0)
  329. self.msgte.setModified(False)
  330. def canExit(self):
  331. # Usually safe to exit, since we're saving messages implicitly
  332. # We'll ask the user for confirmation later, if they have any
  333. # files partially selected.
  334. return not self.runner.core.running()
  335. def loadSettings(self, s, prefix):
  336. 'Load history, etc, from QSettings instance'
  337. repoid = str(self.repo[0])
  338. lpref = prefix + '/commit/' # local settings (splitter, etc)
  339. gpref = 'commit/' # global settings (history, etc)
  340. # message history is stored in unicode
  341. self.split.restoreState(s.value(lpref+'split').toByteArray())
  342. self.msgte.loadSettings(s, lpref+'msgte')
  343. self.stwidget.loadSettings(s, lpref+'status')
  344. self.msghistory = list(s.value(gpref+'history-'+repoid).toStringList())
  345. self.msghistory = [m for m in self.msghistory if m]
  346. self.msgcombo.reset(self.msghistory)
  347. self.userhist = s.value(gpref+'userhist').toStringList()
  348. self.userhist = [u for u in self.userhist if u]
  349. try:
  350. curmsg = self.repo.opener('cur-message.txt').read()
  351. self.setMessage(hglib.tounicode(curmsg))
  352. except EnvironmentError:
  353. pass
  354. try:
  355. curmsg = self.repo.opener('last-message.txt').read()
  356. if curmsg:
  357. self.addMessageToHistory(hglib.tounicode(curmsg))
  358. except EnvironmentError:
  359. pass
  360. def saveSettings(self, s, prefix):
  361. 'Save history, etc, in QSettings instance'
  362. repoid = str(self.repo[0])
  363. lpref = prefix + '/commit/'
  364. gpref = 'commit/'
  365. s.setValue(lpref+'split', self.split.saveState())
  366. self.msgte.saveSettings(s, lpref+'msgte')
  367. self.stwidget.saveSettings(s, lpref+'status')
  368. s.setValue(gpref+'history-'+repoid, self.msghistory)
  369. s.setValue(gpref+'userhist', self.userhist)
  370. try:
  371. msg = self.getMessage()
  372. self.repo.opener('cur-message.txt', 'w').write(msg)
  373. except EnvironmentError:
  374. pass
  375. def addMessageToHistory(self, umsg):
  376. if umsg in self.msghistory:
  377. self.msghistory.remove(umsg)
  378. self.msghistory.insert(0, umsg)
  379. self.msghistory = self.msghistory[:10]
  380. def addUsernameToHistory(self, user):
  381. if user in self.userhist:
  382. self.userhist.remove(user)
  383. self.userhist.insert(0, user)
  384. self.userhist = self.userhist[:10]
  385. def getCurrentUsername(self):
  386. # 1. Override has highest priority
  387. user = self.opts.get('user')
  388. if user:
  389. return user
  390. # 2. Read from repository
  391. try:
  392. return self.repo.ui.username()
  393. except error.Abort:
  394. pass
  395. # 3. Get a username from the user
  396. QMessageBox.information(self, _('Please enter a username'),
  397. _('You must identify yourself to Mercurial'),
  398. QMessageBox.Ok)
  399. from tortoisehg.hgqt.settings import SettingsDialog
  400. dlg = SettingsDialog(False, focus='ui.username')
  401. dlg.exec_()
  402. self.repo.invalidateui()
  403. try:
  404. return self.repo.ui.username()
  405. except error.Abort:
  406. return None
  407. def commit(self):
  408. repo = self.repo
  409. msg = self.getMessage()
  410. if not msg:
  411. qtlib.WarningMsgBox(_('Nothing Commited'),
  412. _('Please enter commit message'),
  413. parent=self)
  414. self.msgte.setFocus()
  415. return
  416. commandlines = []
  417. brcmd = []
  418. newbranch = False
  419. if self.branchop is None:
  420. newbranch = repo[None].branch() != repo['.'].branch()
  421. elif self.branchop == False:
  422. brcmd = ['--close-branch']
  423. else:
  424. branch = hglib.fromunicode(self.branchop)
  425. if branch in repo.branchtags():
  426. # response: 0=Yes, 1=No, 2=Cancel
  427. if branch in [p.branch() for p in repo.parents()]:
  428. resp = 0
  429. else:
  430. rev = repo[branch].rev()
  431. resp = qtlib.CustomPrompt(_('Confirm Branch Change'),
  432. _('Named branch "%s" already exists, '
  433. 'last used in revision %d\n'
  434. 'Yes\t- Make commit restarting this named branch\n'
  435. 'No\t- Make commit without changing branch\n'
  436. 'Cancel\t- Cancel this commit') % (self.branchop, rev),
  437. self, (_('&Yes'), _('&No'), _('Cancel')), 2, 2).run()
  438. else:
  439. resp = qtlib.CustomPrompt(_('Confirm New Branch'),
  440. _('Create new named branch "%s" with this commit?\n'
  441. 'Yes\t- Start new branch with this commit\n'
  442. 'No\t- Make commit without branch change\n'
  443. 'Cancel\t- Cancel this commit') % self.branchop,
  444. self, (_('&Yes'), _('&No'), _('Cancel')), 2, 2).run()
  445. if resp == 0:
  446. newbranch = True
  447. commandlines.append(['branch', '--repository', repo.root,
  448. '--force', branch])
  449. elif resp == 2:
  450. return
  451. files = self.stwidget.getChecked('MAR?!S')
  452. if not (files or brcmd or newbranch):
  453. qtlib.WarningMsgBox(_('No files checked'),
  454. _('No modified files checkmarked for commit'),
  455. parent=self)
  456. self.stwidget.tv.setFocus()
  457. return
  458. if len(repo.parents()) > 1:
  459. files = []
  460. user = self.getCurrentUsername()
  461. if not user:
  462. return
  463. self.addUsernameToHistory(user)
  464. checkedUnknowns = self.stwidget.getChecked('?I')
  465. if checkedUnknowns:
  466. res = qtlib.CustomPrompt(
  467. _('Confirm Add'),
  468. _('Add checked untracked files?'), self,
  469. (_('&OK'), _('Cancel')), 0, 1,
  470. checkedUnknowns).run()
  471. if res == 0:
  472. cmd = ['add', '--repository', repo.root] + \
  473. [repo.wjoin(f) for f in checkedUnknowns]
  474. commandlines.append(cmd)
  475. else:
  476. return
  477. checkedMissing = self.stwidget.getChecked('!')
  478. if checkedMissing:
  479. res = qtlib.CustomPrompt(
  480. _('Confirm Remove'),
  481. _('Remove checked deleted files?'), self,
  482. (_('&OK'), _('Cancel')), 0, 1,
  483. checkedMissing).run()
  484. if res == 0:
  485. cmd = ['remove', '--repository', repo.root] + \
  486. [repo.wjoin(f) for f in checkedMissing]
  487. commandlines.append(cmd)
  488. else:
  489. return
  490. try:
  491. date = self.opts.get('date')
  492. if date:
  493. util.parsedate(date)
  494. dcmd = ['--date', date]
  495. else:
  496. dcmd = []
  497. except error.Abort, e:
  498. if e.hint:
  499. err = _('%s (hint: %s)') % (hglib.tounicode(str(e)),
  500. hglib.tounicode(e.hint))
  501. else:
  502. err = hglib.tounicode(str(e))
  503. self.showMessage.emit(err)
  504. dcmd = []
  505. cmdline = ['commit', '--repository', repo.root, '--verbose',
  506. '--user', user, '--message='+msg]
  507. cmdline += dcmd + brcmd + [repo.wjoin(f) for f in files]
  508. for fname in self.opts.get('autoinc', '').split(','):
  509. fname = fname.strip()
  510. if fname:
  511. cmdline.extend(['--include', fname])
  512. commandlines.append(cmdline)
  513. if self.opts.get('pushafter'):
  514. cmd = ['push', '--repository', repo.root, self.opts['pushafter']]
  515. commandlines.append(cmd)
  516. repo.incrementBusyCount()
  517. self.runner.run(*commandlines)
  518. def commandFinished(self, ret):
  519. self.repo.decrementBusyCount()
  520. if ret == 0:
  521. umsg = self.msgte.text()
  522. if umsg:
  523. self.addMessageToHistory(umsg)
  524. self.msgte.clear()
  525. self.msgte.setModified(False)
  526. self.commitComplete.emit()
  527. self.stwidget.refreshWctx()
  528. def keyPressEvent(self, event):
  529. if event.key() in (Qt.Key_Return, Qt.Key_Enter):
  530. if event.modifiers() == Qt.ControlModifier:
  531. self.commit()
  532. return
  533. return super(CommitWidget, self).keyPressEvent(event)
  534. class MessageHistoryCombo(QComboBox):
  535. def __init__(self, parent=None):
  536. QComboBox.__init__(self, parent)
  537. self.reset([])
  538. def reset(self, msgs):
  539. self.clear()
  540. self.addItem(_('Recent commit messages...'))
  541. self.loaded = False
  542. self.msgs = msgs
  543. def showPopup(self):
  544. if not self.loaded:
  545. self.clear()
  546. for s in self.msgs:
  547. self.addItem(s.split('\n', 1)[0][:70])
  548. self.loaded = True
  549. QComboBox.showPopup(self)
  550. class DetailsDialog(QDialog):
  551. 'Utility dialog for configuring uncommon settings'
  552. def __init__(self, opts, userhistory, parent):
  553. QDialog.__init__(self, parent)
  554. self.setWindowTitle('%s - commit options' % parent.repo.displayname)
  555. self.repo = parent.repo
  556. layout = QVBoxLayout()
  557. self.setLayout(layout)
  558. hbox = QHBoxLayout()
  559. self.usercb = QCheckBox(_('Set username:'))
  560. usercombo = QComboBox()
  561. usercombo.setEditable(True)
  562. usercombo.setEnabled(False)
  563. self.usercb.toggled.connect(usercombo.setEnabled)
  564. self.usercb.toggled.connect(lambda s: s and usercombo.setFocus())
  565. l = []
  566. if opts.get('user'):
  567. val = hglib.tounicode(opts['user'])
  568. self.usercb.setChecked(True)
  569. l.append(val)
  570. try:
  571. val = hglib.tounicode(self.repo.ui.username())
  572. l.append(val)
  573. except util.Abort:
  574. pass
  575. for name in userhistory:
  576. if name not in l:
  577. l.append(name)
  578. for name in l:
  579. usercombo.addItem(name)
  580. self.usercombo = usercombo
  581. usersaverepo = QPushButton(_('Save in Repo'))
  582. usersaverepo.clicked.connect(self.saveInRepo)
  583. usersaverepo.setEnabled(False)
  584. self.usercb.toggled.connect(usersaverepo.setEnabled)
  585. usersaveglobal = QPushButton(_('Save Global'))
  586. usersaveglobal.clicked.connect(self.saveGlobal)
  587. usersaveglobal.setEnabled(False)
  588. self.usercb.toggled.connect(usersaveglobal.setEnabled)
  589. hbox.addWidget(self.usercb)
  590. hbox.addWidget(self.usercombo)
  591. hbox.addWidget(usersaverepo)
  592. hbox.addWidget(usersaveglobal)
  593. layout.addLayout(hbox)
  594. hbox = QHBoxLayout()
  595. self.datecb = QCheckBox(_('Set Date:'))
  596. self.datele = QLineEdit()
  597. self.datele.setEnabled(False)
  598. self.datecb.toggled.connect(self.datele.setEnabled)
  599. curdate = QPushButton(_('Update'))
  600. curdate.setEnabled(False)
  601. self.datecb.toggled.connect(curdate.setEnabled)
  602. self.datecb.toggled.connect(lambda s: s and curdate.setFocus())
  603. curdate.clicked.connect( lambda: self.datele.setText(
  604. hglib.tounicode(hglib.displaytime(util.makedate()))))
  605. if opts.get('date'):
  606. self.datele.setText(opts['date'])
  607. self.datecb.setChecked(True)
  608. else:
  609. self.datecb.setChecked(False)
  610. curdate.clicked.emit(True)
  611. hbox.addWidget(self.datecb)
  612. hbox.addWidget(self.datele)
  613. hbox.addWidget(curdate)
  614. layout.addLayout(hbox)
  615. hbox = QHBoxLayout()
  616. self.pushaftercb = QCheckBox(_('Push After Commit:'))
  617. self.pushafterle = QLineEdit()
  618. self.pushafterle.setEnabled(False)
  619. self.pushaftercb.toggled.connect(self.pushafterle.setEnabled)
  620. self.pushaftercb.toggled.connect(lambda s:
  621. s and self.pushafterle.setFocus())
  622. pushaftersave = QPushButton(_('Save in Repo'))
  623. pushaftersave.clicked.connect(self.savePushAfter)
  624. pushaftersave.setEnabled(False)
  625. self.pushaftercb.toggled.connect(pushaftersave.setEnabled)
  626. if opts.get('pushafter'):
  627. val = hglib.tounicode(opts['pushafter'])
  628. self.pushafterle.setText(val)
  629. self.pushaftercb.setChecked(True)
  630. hbox.addWidget(self.pushaftercb)
  631. hbox.addWidget(self.pushafterle)
  632. hbox.addWidget(pushaftersave)
  633. layout.addLayout(hbox)
  634. hbox = QHBoxLayout()
  635. self.autoinccb = QCheckBox(_('Auto Includes:'))
  636. self.autoincle = QLineEdit()
  637. self.autoincle.setEnabled(False)
  638. self.autoinccb.toggled.connect(self.autoincle.setEnabled)
  639. self.autoinccb.toggled.connect(lambda s:
  640. s and self.autoincle.setFocus())
  641. autoincsave = QPushButton(_('Save in Repo'))
  642. autoincsave.clicked.connect(self.saveAutoInc)
  643. autoincsave.setEnabled(False)
  644. self.autoinccb.toggled.connect(autoincsave.setEnabled)
  645. if opts.get('autoinc'):
  646. val = hglib.tounicode(opts['autoinc'])
  647. self.autoincle.setText(val)
  648. self.autoinccb.setChecked(True)
  649. hbox.addWidget(self.autoinccb)
  650. hbox.addWidget(self.autoincle)
  651. hbox.addWidget(autoincsave)
  652. layout.addLayout(hbox)
  653. BB = QDialogButtonBox
  654. bb = QDialogButtonBox(BB.Ok|BB.Cancel)
  655. bb.accepted.connect(self.accept)
  656. bb.rejected.connect(self.reject)
  657. self.bb = bb
  658. layout.addWidget(bb)
  659. def saveInRepo(self):
  660. fn = os.path.join(self.repo.root, '.hg', 'hgrc')
  661. self.saveToPath([fn])
  662. def saveGlobal(self):
  663. self.saveToPath(util.user_rcpath())
  664. def saveToPath(self, path):
  665. fn, cfg = loadIniFile(path, self)
  666. if not hasattr(cfg, 'write'):
  667. qtlib.WarningMsgBox(_('Unable to save username'),
  668. _('Iniparse must be installed.'), parent=self)
  669. return
  670. if fn is None:
  671. return
  672. try:
  673. user = hglib.fromunicode(self.usercombo.currentText())
  674. if user:
  675. cfg.set('ui', 'username', user)
  676. else:
  677. try:
  678. del cfg['ui']['username']
  679. except KeyError:
  680. pass
  681. wconfig.writefile(cfg, fn)
  682. except IOError, e:
  683. qtlib.WarningMsgBox(_('Unable to write configuration file'),
  684. hglib.tounicode(e), parent=self)
  685. def savePushAfter(self):
  686. path = os.path.join(self.repo.root, '.hg', 'hgrc')
  687. fn, cfg = loadIniFile([path], self)
  688. if not hasattr(cfg, 'write'):
  689. qtlib.WarningMsgBox(_('Unable to save after commit push'),
  690. _('Iniparse must be installed.'), parent=self)
  691. return
  692. if fn is None:
  693. return
  694. try:
  695. remote = hglib.fromunicode(self.pushafterle.text())
  696. if remote:
  697. cfg.set('tortoisehg', 'cipushafter', remote)
  698. else:
  699. try:
  700. del cfg['tortoisehg']['cipushafter']
  701. except KeyError:
  702. pass
  703. wconfig.writefile(cfg, fn)
  704. except IOError, e:
  705. qtlib.WarningMsgBox(_('Unable to write configuration file'),
  706. hglib.tounicode(e), parent=self)
  707. def saveAutoInc(self):
  708. path = os.path.join(self.repo.root, '.hg', 'hgrc')
  709. fn, cfg = loadIniFile([path], self)
  710. if not hasattr(cfg, 'write'):
  711. qtlib.WarningMsgBox(_('Unable to save auto include list'),
  712. _('Iniparse must be installed.'), parent=self)
  713. return
  714. if fn is None:
  715. return
  716. try:
  717. list = hglib.fromunicode(self.autoincle.text())
  718. if list:
  719. cfg.set('tortoisehg', 'autoinc', list)
  720. else:
  721. try:
  722. del cfg['tortoisehg']['autoinc']
  723. except KeyError:
  724. pass
  725. wconfig.writefile(cfg, fn)
  726. except IOError, e:
  727. qtlib.WarningMsgBox(_('Unable to write configuration file'),
  728. hglib.tounicode(e), parent=self)
  729. def accept(self):
  730. outopts = {}
  731. if self.datecb.isChecked():
  732. date = hglib.fromunicode(self.datele.text())
  733. try:
  734. util.parsedate(date)
  735. except error.Abort, e:
  736. if e.hint:
  737. err = _('%s (hint: %s)') % (hglib.tounicode(str(e)),
  738. hglib.tounicode(e.hint))
  739. else:
  740. err = hglib.tounicode(str(e))
  741. qtlib.WarningMsgBox(_('Invalid date format'), err, parent=self)
  742. return
  743. outopts['date'] = date
  744. else:
  745. outopts['date'] = ''
  746. if self.usercb.isChecked():
  747. user = hglib.fromunicode(self.usercombo.currentText())
  748. else:
  749. user = ''
  750. outopts['user'] = user
  751. if not user:
  752. try:
  753. self.repo.ui.username()
  754. except util.Abort, e:
  755. if e.hint:
  756. err = _('%s (hint: %s)') % (hglib.tounicode(str(e)),
  757. hglib.tounicode(e.hint))
  758. else:
  759. err = hglib.tounicode(str(e))
  760. qtlib.WarningMsgBox(_('No username configured'),
  761. err, parent=self)
  762. return
  763. if self.pushaftercb.isChecked():
  764. remote = hglib.fromunicode(self.pushafterle.text())
  765. outopts['pushafter'] = remote
  766. else:
  767. outopts['pushafter'] = ''
  768. self.outopts = outopts
  769. QDialog.accept(self)
  770. class CommitDialog(QDialog):
  771. 'Standalone commit tool, a wrapper for CommitWidget'
  772. def __init__(self, pats, opts, parent=None):
  773. QDialog.__init__(self, parent)
  774. self.setWindowFlags(Qt.Window)
  775. self.pats = pats
  776. self.opts = opts
  777. layout = QVBoxLayout()
  778. layout.setContentsMargins(2, 2, 2, 2)
  779. layout.setMargin(0)
  780. self.setLayout(layout)
  781. commit = CommitWidget(pats, opts, opts.get('root'), False, self)
  782. layout.addWidget(commit, 1)
  783. self.statusbar = cmdui.ThgStatusBar(self)
  784. self.statusbar.setSizeGripEnabled(False)
  785. commit.showMessage.connect(self.statusbar.showMessage)
  786. commit.progress.connect(self.statusbar.progress)
  787. commit.linkActivated.connect(self.linkActivated)
  788. BB = QDialogButtonBox
  789. bb = QDialogButtonBox(BB.Ok|BB.Cancel|BB.Discard)
  790. bb.accepted.connect(self.accept)
  791. bb.rejected.connect(self.reject)
  792. bb.button(BB.Discard).setText('Undo')
  793. bb.button(BB.Discard).clicked.connect(commit.rollback)
  794. bb.button(BB.Cancel).setDefault(False)
  795. bb.button(BB.Discard).setDefault(False)
  796. bb.button(BB.Ok).setDefault(True)
  797. self.commitButton = bb.button(BB.Ok)
  798. self.commitButton.setText(_('Commit'))
  799. self.bb = bb
  800. hbox = QHBoxLayout()
  801. hbox.setMargin(0)
  802. hbox.setContentsMargins(*(0,)*4)
  803. hbox.addWidget(self.statusbar)
  804. hbox.addWidget(self.bb)
  805. layout.addLayout(hbox)
  806. s = QSettings()
  807. self.restoreGeometry(s.value('commit/geom').toByteArray())
  808. commit.loadSettings(s, 'committool')
  809. commit.repo.repositoryChanged.connect(self.updateUndo)
  810. commit.commitComplete.connect(self.postcommit)
  811. commit.commitButtonEnable.connect(self.commitButton.setEnabled)
  812. self.setWindowTitle('%s - commit' % commit.repo.displayname)
  813. self.commit = commit
  814. self.commit.reload()
  815. self.updateUndo()
  816. self.commit.msgte.setFocus()
  817. QShortcut(QKeySequence.Refresh, self, self.refresh)
  818. def linkActivated(self, link):
  819. link = hglib.fromunicode(link)
  820. if link.startswith('subrepo:'):
  821. from tortoisehg.hgqt.run import qtrun
  822. qtrun(run, ui.ui(), root=link[8:])
  823. if link.startswith('shelve:'):
  824. repo = self.commit.repo
  825. from tortoisehg.hgqt import shelve
  826. dlg = shelve.ShelveDialog(repo, self)
  827. dlg.finished.connect(dlg.deleteLater)
  828. dlg.exec_()
  829. self.refresh()
  830. def updateUndo(self):
  831. BB = QDialogButtonBox
  832. undomsg = self.commit.canUndo()
  833. if undomsg:
  834. self.bb.button(BB.Discard).setEnabled(True)
  835. self.bb.button(BB.Discard).setToolTip(undomsg)
  836. else:
  837. self.bb.button(BB.Discard).setEnabled(False)
  838. self.bb.button(BB.Discard).setToolTip('')
  839. def refresh(self):
  840. self.updateUndo()
  841. self.commit.reload()
  842. def postcommit(self):
  843. repo = self.commit.stwidget.repo
  844. if repo.ui.configbool('tortoisehg', 'closeci'):
  845. self.reject()
  846. return
  847. def accept(self):
  848. self.commit.commit()
  849. def reject(self):
  850. if self.commit.canExit():
  851. s = QSettings()
  852. s.setValue('commit/geom', self.saveGeometry())
  853. self.commit.saveSettings(s, 'committool')
  854. QDialog.reject(self)
  855. def run(ui, *pats, **opts):
  856. return CommitDialog(hglib.canonpaths(pats), opts)