def __init__(self, repo, revs, parent=None, outgoing=False, outgoingrevs=None): """Create EmailDialog for the given repo and revs :revs: List of revisions to be sent. :outgoing: Enable outgoing bundle support. You also need to set outgoing revisions to `revs`. :outgoingrevs: Target revision of outgoing bundle. (Passed as `hg email --bundle --rev {rev}`) """ super(EmailDialog, self).__init__(parent) self.setWindowFlags(Qt.Window) self._repo = repo self._outgoing = outgoing self._outgoingrevs = outgoingrevs or [] self._qui = Ui_EmailDialog() self._qui.setupUi(self) self._initchangesets(revs) self._initpreviewtab() self._initenvelopebox() self._qui.bundle_radio.toggled.connect(self._updateforms) self._qui.body_check.toggled.connect(self._body_mode_clicked) self._qui.attach_check.toggled.connect(self._attach_mode_clicked) self._qui.inline_check.toggled.connect(self._inline_mode_clicked) self._initintrobox() self._readhistory() self._filldefaults() self._updateforms() self._readsettings() QShortcut(QKeySequence('CTRL+Return'), self, self.accept) QShortcut(QKeySequence('Ctrl+Enter'), self, self.accept)
class EmailDialog(QDialog): """Dialog for sending patches via email""" def __init__(self, repo, revs, parent=None, outgoing=False, outgoingrevs=None): """Create EmailDialog for the given repo and revs :revs: List of revisions to be sent. :outgoing: Enable outgoing bundle support. You also need to set outgoing revisions to `revs`. :outgoingrevs: Target revision of outgoing bundle. (Passed as `hg email --bundle --rev {rev}`) """ super(EmailDialog, self).__init__(parent) self.setWindowFlags(Qt.Window) self._repo = repo self._outgoing = outgoing self._outgoingrevs = outgoingrevs or [] self._qui = Ui_EmailDialog() self._qui.setupUi(self) self._initchangesets(revs) self._initpreviewtab() self._initenvelopebox() self._qui.bundle_radio.toggled.connect(self._updateforms) self._qui.body_check.toggled.connect(self._body_mode_clicked) self._qui.attach_check.toggled.connect(self._attach_mode_clicked) self._qui.inline_check.toggled.connect(self._inline_mode_clicked) self._initintrobox() self._readhistory() self._filldefaults() self._updateforms() self._readsettings() QShortcut(QKeySequence('CTRL+Return'), self, self.accept) QShortcut(QKeySequence('Ctrl+Enter'), self, self.accept) def closeEvent(self, event): self._writesettings() super(EmailDialog, self).closeEvent(event) def _readsettings(self): s = QSettings() self.restoreGeometry(s.value('email/geom').toByteArray()) self._qui.intro_changesets_splitter.restoreState( s.value('email/intro_changesets_splitter').toByteArray()) def _writesettings(self): s = QSettings() s.setValue('email/geom', self.saveGeometry()) s.setValue('email/intro_changesets_splitter', self._qui.intro_changesets_splitter.saveState()) def _readhistory(self): s = QSettings() for k in ('to', 'cc', 'from', 'flag'): w = getattr(self._qui, '%s_edit' % k) w.addItems(s.value('email/%s_history' % k).toStringList()) w.setCurrentIndex(-1) # unselect def _writehistory(self): def itercombo(w): if w.currentText(): yield w.currentText() for i in xrange(w.count()): if w.itemText(i) != w.currentText(): yield w.itemText(i) s = QSettings() for k in ('to', 'cc', 'from', 'flag'): w = getattr(self._qui, '%s_edit' % k) s.setValue('email/%s_history' % k, list(itercombo(w))[:10]) def _initchangesets(self, revs): def purerevs(revs): return scmutil.revrange(self._repo, iter(str(e) for e in revs)) self._changesets = _ChangesetsModel(self._repo, # TODO: [':'] is inefficient revs=purerevs(revs or [':']), selectedrevs=purerevs(revs), parent=self) self._changesets.dataChanged.connect(self._updateforms) self._qui.changesets_view.setModel(self._changesets) @property def _ui(self): return self._repo.ui @property def _revs(self): """Returns list of revisions to be sent""" return self._changesets.selectedrevs def _filldefaults(self): """Fill form by default values""" def getfromaddr(ui): """Get sender address in the same manner as patchbomb""" addr = ui.config('email', 'from') or ui.config('patchbomb', 'from') if addr: return addr try: return ui.username() except error.Abort: return '' self._qui.to_edit.setEditText( hglib.tounicode(self._ui.config('email', 'to', ''))) self._qui.cc_edit.setEditText( hglib.tounicode(self._ui.config('email', 'cc', ''))) self._qui.from_edit.setEditText(hglib.tounicode(getfromaddr(self._ui))) self.setdiffformat(self._ui.configbool('diff', 'git') and 'git' or 'hg') def setdiffformat(self, format): """Set diff format, 'hg', 'git' or 'plain'""" try: radio = getattr(self._qui, '%spatch_radio' % format) except AttributeError: raise ValueError('unknown diff format: %r' % format) radio.setChecked(True) def getdiffformat(self): """Selected diff format""" for e in self._qui.patch_frame.children(): m = re.match(r'(\w+)patch_radio', str(e.objectName())) if m and e.isChecked(): return m.group(1) return 'hg' def getextraopts(self): """Dict of extra options""" opts = {} for e in self._qui.extra_frame.children(): m = re.match(r'(\w+)_check', str(e.objectName())) if m: opts[m.group(1)] = e.isChecked() return opts def _patchbombopts(self, **opts): """Generate opts for patchbomb by form values""" def headertext(s): # QLineEdit may contain newline character return re.sub(r'\s', ' ', hglib.fromunicode(s)) opts['to'] = [headertext(self._qui.to_edit.currentText())] opts['cc'] = [headertext(self._qui.cc_edit.currentText())] opts['from'] = headertext(self._qui.from_edit.currentText()) opts['in_reply_to'] = headertext(self._qui.inreplyto_edit.text()) opts['flag'] = [headertext(self._qui.flag_edit.currentText())] if self._qui.bundle_radio.isChecked(): assert self._outgoing # only outgoing bundle is supported opts['rev'] = map(str, self._outgoingrevs) opts['bundle'] = True else: opts['rev'] = map(str, self._revs) def diffformat(): n = self.getdiffformat() if n == 'hg': return {} else: return {n: True} opts.update(diffformat()) opts.update(self.getextraopts()) def writetempfile(s): fd, fname = tempfile.mkstemp(prefix='thg_emaildesc_') try: os.write(fd, s) return fname finally: os.close(fd) opts['intro'] = self._qui.writeintro_check.isChecked() if opts['intro']: opts['subject'] = headertext(self._qui.subject_edit.text()) opts['desc'] = writetempfile(hglib.fromunicode(self._qui.body_edit.toPlainText())) # TODO: change patchbomb not to use temporary file # Include the repo in the command so it can be found when thg is not # run from within a hg path opts['repository'] = self._repo.root return opts def _isvalid(self): """Filled all required values?""" for e in ('to_edit', 'from_edit'): if not getattr(self._qui, e).currentText(): return False if self._qui.writeintro_check.isChecked() and not self._qui.subject_edit.text(): return False if not self._revs: return False return True @pyqtSlot() def _updateforms(self): """Update availability of form widgets""" valid = self._isvalid() self._qui.send_button.setEnabled(valid) self._qui.main_tabs.setTabEnabled(self._previewtabindex(), valid) self._qui.writeintro_check.setEnabled(not self._introrequired()) self._qui.bundle_radio.setEnabled( self._outgoing and self._changesets.isselectedall()) self._changesets.setReadOnly(self._qui.bundle_radio.isChecked()) if self._qui.bundle_radio.isChecked(): # workaround to disable preview for outgoing bundle because it # may freeze main thread self._qui.main_tabs.setTabEnabled(self._previewtabindex(), False) if self._introrequired(): self._qui.writeintro_check.setChecked(True) def _body_mode_clicked(self): # Only allow a single attachment type to be active at a time sendattachment = self._qui.attach_check.isChecked() or self._qui.inline_check.isChecked() if not sendattachment: # If no attachment, ensure that the body mode is enabled self._qui.body_check.setChecked(True) def _attach_mode_clicked(self): sendattachment = self._qui.attach_check.isChecked() or self._qui.inline_check.isChecked() self._qui.body_check.setDisabled(not sendattachment) if not sendattachment: self._qui.body_check.setChecked(True) # Only allow a single attachment type to be active at a time if self._qui.attach_check.isChecked(): self._qui.inline_check.setChecked(False) def _inline_mode_clicked(self): sendattachment = self._qui.attach_check.isChecked() or self._qui.inline_check.isChecked() self._qui.body_check.setDisabled(not sendattachment) if not sendattachment: self._qui.body_check.setChecked(True) # Only allow a single attachment type to be active at a time if self._qui.inline_check.isChecked(): self._qui.attach_check.setChecked(False) def _initenvelopebox(self): for e in ('to_edit', 'from_edit'): getattr(self._qui, e).editTextChanged.connect(self._updateforms) def accept(self): # TODO: want to pass patchbombopts directly def cmdargs(opts): args = [] for k, v in opts.iteritems(): if isinstance(v, bool): if v: args.append('--%s' % k.replace('_', '-')) else: for e in isinstance(v, basestring) and [v] or v: args += ['--%s' % k.replace('_', '-'), e] return args hglib.loadextension(self._ui, 'patchbomb') opts = self._patchbombopts() try: cmd = cmdui.Dialog(['email'] + cmdargs(opts), parent=self) cmd.setWindowTitle(_('Sending Email')) cmd.setShowOutput(False) cmd.finished.connect(cmd.deleteLater) if cmd.exec_(): self._writehistory() finally: if 'desc' in opts: os.unlink(opts['desc']) # TODO: don't use tempfile def _initintrobox(self): self._qui.intro_box.hide() # hidden by default self._qui.subject_edit.textChanged.connect(self._updateforms) self._qui.writeintro_check.toggled.connect(self._updateforms) def _introrequired(self): """Is intro message required?""" return len(self._revs) > 1 or self._qui.bundle_radio.isChecked() def _initpreviewtab(self): def initqsci(w): w.setUtf8(True) w.setReadOnly(True) w.setMarginWidth(1, 0) # hide area for line numbers self.lexer = lex = lexers.get_diff_lexer(self) fh = qtlib.getfont('fontdiff') fh.changed.connect(self.forwardFont) lex.setFont(fh.font()) w.setLexer(lex) # TODO: better way to setup diff lexer initqsci(self._qui.preview_edit) self._qui.main_tabs.currentChanged.connect(self._refreshpreviewtab) self._refreshpreviewtab(self._qui.main_tabs.currentIndex()) def forwardFont(self, font): if self.lexer: self.lexer.setFont(font) @pyqtSlot(int) def _refreshpreviewtab(self, index): """Generate preview text if current tab is preview""" if self._previewtabindex() != index: return self._qui.preview_edit.setText(self._preview()) def _preview(self): """Generate preview text by running patchbomb""" def loadpatchbomb(): hglib.loadextension(self._ui, 'patchbomb') return extensions.find('patchbomb') def wrapui(ui): buf = StringIO() # TODO: common way to prepare pure ui newui = ui.copy() newui.setconfig('ui', 'interactive', False) newui.setconfig('diff', 'git', False) newui.write = lambda *args, **opts: buf.write(''.join(args)) newui.status = lambda *args, **opts: None return newui, buf def stripheadmsg(s): # TODO: skip until first Content-type: line ?? return '\n'.join(s.splitlines()[3:]) ui, buf = wrapui(self._ui) opts = self._patchbombopts(test=True) try: # TODO: fix hgext.patchbomb's implementation instead if 'PAGER' in os.environ: del os.environ['PAGER'] loadpatchbomb().patchbomb(ui, self._repo, **opts) return stripheadmsg(hglib.tounicode(buf.getvalue())) finally: if 'desc' in opts: os.unlink(opts['desc']) # TODO: don't use tempfile def _previewtabindex(self): """Index of preview tab""" return self._qui.main_tabs.indexOf(self._qui.preview_tab) @pyqtSlot() def on_settings_button_clicked(self): from tortoisehg.hgqt import settings if settings.SettingsDialog(parent=self, focus='email.from').exec_(): # not use repo.configChanged because it can clobber user input # accidentally. self._repo.invalidateui() # force reloading config immediately self._filldefaults() @pyqtSlot() def on_selectall_button_clicked(self): self._changesets.selectAll() @pyqtSlot() def on_selectnone_button_clicked(self): self._changesets.selectNone()
class EmailDialog(QDialog): """Dialog for sending patches via email""" def __init__(self, repoagent, revs, parent=None, outgoing=False, outgoingrevs=None): """Create EmailDialog for the given repo and revs :revs: List of revisions to be sent. :outgoing: Enable outgoing bundle support. You also need to set outgoing revisions to `revs`. :outgoingrevs: Target revision of outgoing bundle. (Passed as `hg email --bundle --rev {rev}`) """ super(EmailDialog, self).__init__(parent) self.setWindowFlags(Qt.Window) self._repoagent = repoagent self._cmdsession = cmdcore.nullCmdSession() self._outgoing = outgoing self._outgoingrevs = outgoingrevs or [] self._qui = Ui_EmailDialog() self._qui.setupUi(self) self._initchangesets(revs) self._initpreviewtab() self._initenvelopebox() self._qui.bundle_radio.toggled.connect(self._updateforms) self._qui.attach_check.toggled.connect(self._updateattachmodes) self._qui.inline_check.toggled.connect(self._updateattachmodes) self._initintrobox() self._readhistory() self._filldefaults() self._updateforms() self._updateattachmodes() self._readsettings() QShortcut(QKeySequence('CTRL+Return'), self, self.accept) QShortcut(QKeySequence('Ctrl+Enter'), self, self.accept) def closeEvent(self, event): self._writesettings() super(EmailDialog, self).closeEvent(event) def _readsettings(self): s = QSettings() self.restoreGeometry(s.value('email/geom').toByteArray()) self._qui.intro_changesets_splitter.restoreState( s.value('email/intro_changesets_splitter').toByteArray()) def _writesettings(self): s = QSettings() s.setValue('email/geom', self.saveGeometry()) s.setValue('email/intro_changesets_splitter', self._qui.intro_changesets_splitter.saveState()) def _readhistory(self): s = QSettings() for k in ('to', 'cc', 'from', 'flag', 'subject'): w = getattr(self._qui, '%s_edit' % k) w.addItems(s.value('email/%s_history' % k).toStringList()) w.setCurrentIndex(-1) # unselect for k in ('body', 'attach', 'inline', 'diffstat'): w = getattr(self._qui, '%s_check' % k) w.setChecked(s.value('email/%s' % k).toBool()) def _writehistory(self): def itercombo(w): if w.currentText(): yield w.currentText() for i in xrange(w.count()): if w.itemText(i) != w.currentText(): yield w.itemText(i) s = QSettings() for k in ('to', 'cc', 'from', 'flag', 'subject'): w = getattr(self._qui, '%s_edit' % k) s.setValue('email/%s_history' % k, list(itercombo(w))[:10]) for k in ('body', 'attach', 'inline', 'diffstat'): w = getattr(self._qui, '%s_check' % k) s.setValue('email/%s' % k, w.isChecked()) def _initchangesets(self, revs): self._changesets = _ChangesetsModel(self._repo, revs=revs or list(self._repo), selectedrevs=revs, parent=self) self._changesets.dataChanged.connect(self._updateforms) self._qui.changesets_view.setModel(self._changesets) @property def _repo(self): return self._repoagent.rawRepo() @property def _ui(self): return self._repo.ui @property def _revs(self): """Returns list of revisions to be sent""" return self._changesets.selectedrevs def _filldefaults(self): """Fill form by default values""" def getfromaddr(ui): """Get sender address in the same manner as patchbomb""" addr = ui.config('email', 'from') or ui.config('patchbomb', 'from') if addr: return addr try: return ui.username() except error.Abort: return '' self._qui.to_edit.setEditText( hglib.tounicode(self._ui.config('email', 'to', ''))) self._qui.cc_edit.setEditText( hglib.tounicode(self._ui.config('email', 'cc', ''))) self._qui.from_edit.setEditText(hglib.tounicode(getfromaddr(self._ui))) self.setdiffformat(self._ui.configbool('diff', 'git') and 'git' or 'hg') def setdiffformat(self, format): """Set diff format, 'hg', 'git' or 'plain'""" try: radio = getattr(self._qui, '%spatch_radio' % format) except AttributeError: raise ValueError('unknown diff format: %r' % format) radio.setChecked(True) def getdiffformat(self): """Selected diff format""" for e in self._qui.patch_frame.children(): m = re.match(r'(\w+)patch_radio', str(e.objectName())) if m and e.isChecked(): return m.group(1) return 'hg' def getextraopts(self): """Dict of extra options""" opts = {} for e in self._qui.extra_frame.children(): m = re.match(r'(\w+)_check', str(e.objectName())) if m: opts[m.group(1)] = e.isChecked() return opts def _patchbombopts(self, **opts): """Generate opts for patchbomb by form values""" def headertext(s): # QLineEdit may contain newline character return re.sub(r'\s', ' ', unicode(s)) opts['to'] = headertext(self._qui.to_edit.currentText()) opts['cc'] = headertext(self._qui.cc_edit.currentText()) opts['from'] = headertext(self._qui.from_edit.currentText()) opts['in_reply_to'] = headertext(self._qui.inreplyto_edit.text()) opts['flag'] = headertext(self._qui.flag_edit.currentText()) if self._qui.bundle_radio.isChecked(): assert self._outgoing # only outgoing bundle is supported opts['rev'] = hglib.compactrevs(self._outgoingrevs) opts['bundle'] = True else: opts['rev'] = hglib.compactrevs(self._revs) fmt = self.getdiffformat() if fmt != 'hg': opts[fmt] = True opts.update(self.getextraopts()) def writetempfile(s): fd, fname = tempfile.mkstemp(prefix='thg_emaildesc_', dir=qtlib.gettempdir()) try: os.write(fd, s) return hglib.tounicode(fname) finally: os.close(fd) opts['intro'] = self._qui.writeintro_check.isChecked() if opts['intro']: opts['subject'] = headertext(self._qui.subject_edit.currentText()) opts['desc'] = writetempfile( hglib.fromunicode(self._qui.body_edit.toPlainText())) # The email dialog is available no matter if patchbomb extension isn't # enabled. The extension name makes it unlikely first-time users # would discover that Mercurial ships with a functioning patch MTA. # Since patchbomb doesn't monkey patch any Mercurial code, it's safe # to enable it on demand. opts['config'] = 'extensions.patchbomb=' return opts def _isvalid(self): """Filled all required values?""" for e in ('to_edit', 'from_edit'): if not getattr(self._qui, e).currentText(): return False if (self._qui.writeintro_check.isChecked() and not self._qui.subject_edit.currentText()): return False if not self._revs: return False return True @pyqtSlot() def _updateforms(self): """Update availability of form widgets""" valid = self._isvalid() self._qui.send_button.setEnabled(valid) self._qui.main_tabs.setTabEnabled(self._previewtabindex(), valid) self._qui.writeintro_check.setEnabled(not self._introrequired()) self._qui.bundle_radio.setEnabled( self._outgoing and self._changesets.isselectedall()) self._changesets.setReadOnly(self._qui.bundle_radio.isChecked()) if self._qui.bundle_radio.isChecked(): # workaround to disable preview for outgoing bundle because it # may freeze main thread self._qui.main_tabs.setTabEnabled(self._previewtabindex(), False) if self._introrequired(): self._qui.writeintro_check.setChecked(True) @qtlib.senderSafeSlot() def _updateattachmodes(self): """Update checkboxes to select the embedding style of the patch""" attachmodes = [self._qui.attach_check, self._qui.inline_check] body = self._qui.body_check # --attach and --inline are exclusive if self.sender() in attachmodes and self.sender().isChecked(): for w in attachmodes: if w is not self.sender(): w.setChecked(False) # --body is mandatory if no attach modes are specified body.setEnabled(any(w.isChecked() for w in attachmodes)) if not body.isEnabled(): body.setChecked(True) def _initenvelopebox(self): for e in ('to_edit', 'from_edit'): getattr(self._qui, e).editTextChanged.connect(self._updateforms) def accept(self): opts = self._patchbombopts() cmdline = hglib.buildcmdargs('email', **opts) cmd = cmdui.CmdSessionDialog(self) cmd.setWindowTitle(_('Sending Email')) cmd.setLogVisible(False) uih = cmdui.PasswordUiHandler(cmd) # skip "intro" and "diffstat" prompt cmd.setSession(self._repoagent.runCommand(cmdline, uih)) if cmd.exec_() == 0: self._writehistory() def _initintrobox(self): self._qui.intro_box.hide() # hidden by default self._qui.subject_edit.editTextChanged.connect(self._updateforms) self._qui.writeintro_check.toggled.connect(self._updateforms) def _introrequired(self): """Is intro message required?""" return self._qui.bundle_radio.isChecked() def _initpreviewtab(self): def initqsci(w): w.setUtf8(True) w.setReadOnly(True) w.setMarginWidth(1, 0) # hide area for line numbers self.lexer = lex = lexers.difflexer(self) fh = qtlib.getfont('fontdiff') fh.changed.connect(self.forwardFont) lex.setFont(fh.font()) w.setLexer(lex) # TODO: better way to setup diff lexer initqsci(self._qui.preview_edit) self._qui.main_tabs.currentChanged.connect(self._refreshpreviewtab) self._refreshpreviewtab(self._qui.main_tabs.currentIndex()) def forwardFont(self, font): if self.lexer: self.lexer.setFont(font) @pyqtSlot(int) def _refreshpreviewtab(self, index): """Generate preview text if current tab is preview""" if self._previewtabindex() != index: return self._qui.preview_edit.clear() opts = self._patchbombopts(test=True) cmdline = hglib.buildcmdargs('email', **opts) self._cmdsession = sess = self._repoagent.runCommand(cmdline) sess.setCaptureOutput(True) sess.commandFinished.connect(self._updatepreview) @pyqtSlot() def _updatepreview(self): msg = hglib.tounicode(str(self._cmdsession.readAll())) self._qui.preview_edit.append(msg) def _previewtabindex(self): """Index of preview tab""" return self._qui.main_tabs.indexOf(self._qui.preview_tab) @pyqtSlot() def on_settings_button_clicked(self): from tortoisehg.hgqt import settings if settings.SettingsDialog(parent=self, focus='email.from').exec_(): # not use repo.configChanged because it can clobber user input # accidentally. self._repo.invalidateui() # force reloading config immediately self._filldefaults() @pyqtSlot() def on_selectall_button_clicked(self): self._changesets.selectAll() @pyqtSlot() def on_selectnone_button_clicked(self): self._changesets.selectNone()
class EmailDialog(QDialog): """Dialog for sending patches via email""" def __init__(self, repo, revs, parent=None, outgoing=False, outgoingrevs=None): """Create EmailDialog for the given repo and revs :revs: List of revisions to be sent. :outgoing: Enable outgoing bundle support. You also need to set outgoing revisions to `revs`. :outgoingrevs: Target revision of outgoing bundle. (Passed as `hg email --bundle --rev {rev}`) """ super(EmailDialog, self).__init__(parent) self.setWindowFlags(Qt.Window) self._repo = repo self._outgoing = outgoing self._outgoingrevs = outgoingrevs or [] self._qui = Ui_EmailDialog() self._qui.setupUi(self) self._initchangesets(revs) self._initpreviewtab() self._initenvelopebox() self._qui.bundle_radio.toggled.connect(self._updateforms) self._qui.body_check.toggled.connect(self._body_mode_clicked) self._qui.attach_check.toggled.connect(self._attach_mode_clicked) self._qui.inline_check.toggled.connect(self._inline_mode_clicked) self._initintrobox() self._readhistory() self._filldefaults() self._updateforms() self._readsettings() QShortcut(QKeySequence('CTRL+Return'), self, self.accept) QShortcut(QKeySequence('Ctrl+Enter'), self, self.accept) def closeEvent(self, event): self._writesettings() super(EmailDialog, self).closeEvent(event) def _readsettings(self): s = QSettings() self.restoreGeometry(s.value('email/geom').toByteArray()) self._qui.intro_changesets_splitter.restoreState( s.value('email/intro_changesets_splitter').toByteArray()) def _writesettings(self): s = QSettings() s.setValue('email/geom', self.saveGeometry()) s.setValue('email/intro_changesets_splitter', self._qui.intro_changesets_splitter.saveState()) def _readhistory(self): s = QSettings() for k in ('to', 'cc', 'from', 'flag'): w = getattr(self._qui, '%s_edit' % k) w.addItems(s.value('email/%s_history' % k).toStringList()) w.setCurrentIndex(-1) # unselect def _writehistory(self): def itercombo(w): if w.currentText(): yield w.currentText() for i in xrange(w.count()): if w.itemText(i) != w.currentText(): yield w.itemText(i) s = QSettings() for k in ('to', 'cc', 'from', 'flag'): w = getattr(self._qui, '%s_edit' % k) s.setValue('email/%s_history' % k, list(itercombo(w))[:10]) def _initchangesets(self, revs): def purerevs(revs): return scmutil.revrange(self._repo, iter(str(e) for e in revs)) self._changesets = _ChangesetsModel( self._repo, # TODO: [':'] is inefficient revs=purerevs(revs or [':']), selectedrevs=purerevs(revs), parent=self) self._changesets.dataChanged.connect(self._updateforms) self._qui.changesets_view.setModel(self._changesets) @property def _ui(self): return self._repo.ui @property def _revs(self): """Returns list of revisions to be sent""" return self._changesets.selectedrevs def _filldefaults(self): """Fill form by default values""" def getfromaddr(ui): """Get sender address in the same manner as patchbomb""" addr = ui.config('email', 'from') or ui.config('patchbomb', 'from') if addr: return addr try: return ui.username() except error.Abort: return '' self._qui.to_edit.setEditText( hglib.tounicode(self._ui.config('email', 'to', ''))) self._qui.cc_edit.setEditText( hglib.tounicode(self._ui.config('email', 'cc', ''))) self._qui.from_edit.setEditText(hglib.tounicode(getfromaddr(self._ui))) self.setdiffformat( self._ui.configbool('diff', 'git') and 'git' or 'hg') def setdiffformat(self, format): """Set diff format, 'hg', 'git' or 'plain'""" try: radio = getattr(self._qui, '%spatch_radio' % format) except AttributeError: raise ValueError('unknown diff format: %r' % format) radio.setChecked(True) def getdiffformat(self): """Selected diff format""" for e in self._qui.patch_frame.children(): m = re.match(r'(\w+)patch_radio', str(e.objectName())) if m and e.isChecked(): return m.group(1) return 'hg' def getextraopts(self): """Dict of extra options""" opts = {} for e in self._qui.extra_frame.children(): m = re.match(r'(\w+)_check', str(e.objectName())) if m: opts[m.group(1)] = e.isChecked() return opts def _patchbombopts(self, **opts): """Generate opts for patchbomb by form values""" def headertext(s): # QLineEdit may contain newline character return re.sub(r'\s', ' ', hglib.fromunicode(s)) opts['to'] = [headertext(self._qui.to_edit.currentText())] opts['cc'] = [headertext(self._qui.cc_edit.currentText())] opts['from'] = headertext(self._qui.from_edit.currentText()) opts['in_reply_to'] = headertext(self._qui.inreplyto_edit.text()) opts['flag'] = [headertext(self._qui.flag_edit.currentText())] if self._qui.bundle_radio.isChecked(): assert self._outgoing # only outgoing bundle is supported opts['rev'] = map(str, self._outgoingrevs) opts['bundle'] = True else: opts['rev'] = map(str, self._revs) def diffformat(): n = self.getdiffformat() if n == 'hg': return {} else: return {n: True} opts.update(diffformat()) opts.update(self.getextraopts()) def writetempfile(s): fd, fname = tempfile.mkstemp(prefix='thg_emaildesc_') try: os.write(fd, s) return fname finally: os.close(fd) opts['intro'] = self._qui.writeintro_check.isChecked() if opts['intro']: opts['subject'] = headertext(self._qui.subject_edit.text()) opts['desc'] = writetempfile( hglib.fromunicode(self._qui.body_edit.toPlainText())) # TODO: change patchbomb not to use temporary file # Include the repo in the command so it can be found when thg is not # run from within a hg path opts['repository'] = self._repo.root return opts def _isvalid(self): """Filled all required values?""" for e in ('to_edit', 'from_edit'): if not getattr(self._qui, e).currentText(): return False if self._qui.writeintro_check.isChecked( ) and not self._qui.subject_edit.text(): return False if not self._revs: return False return True @pyqtSlot() def _updateforms(self): """Update availability of form widgets""" valid = self._isvalid() self._qui.send_button.setEnabled(valid) self._qui.main_tabs.setTabEnabled(self._previewtabindex(), valid) self._qui.writeintro_check.setEnabled(not self._introrequired()) self._qui.bundle_radio.setEnabled(self._outgoing and self._changesets.isselectedall()) self._changesets.setReadOnly(self._qui.bundle_radio.isChecked()) if self._qui.bundle_radio.isChecked(): # workaround to disable preview for outgoing bundle because it # may freeze main thread self._qui.main_tabs.setTabEnabled(self._previewtabindex(), False) if self._introrequired(): self._qui.writeintro_check.setChecked(True) def _body_mode_clicked(self): # Only allow a single attachment type to be active at a time sendattachment = self._qui.attach_check.isChecked( ) or self._qui.inline_check.isChecked() if not sendattachment: # If no attachment, ensure that the body mode is enabled self._qui.body_check.setChecked(True) def _attach_mode_clicked(self): sendattachment = self._qui.attach_check.isChecked( ) or self._qui.inline_check.isChecked() self._qui.body_check.setDisabled(not sendattachment) if not sendattachment: self._qui.body_check.setChecked(True) # Only allow a single attachment type to be active at a time if self._qui.attach_check.isChecked(): self._qui.inline_check.setChecked(False) def _inline_mode_clicked(self): sendattachment = self._qui.attach_check.isChecked( ) or self._qui.inline_check.isChecked() self._qui.body_check.setDisabled(not sendattachment) if not sendattachment: self._qui.body_check.setChecked(True) # Only allow a single attachment type to be active at a time if self._qui.inline_check.isChecked(): self._qui.attach_check.setChecked(False) def _initenvelopebox(self): for e in ('to_edit', 'from_edit'): getattr(self._qui, e).editTextChanged.connect(self._updateforms) def accept(self): # TODO: want to pass patchbombopts directly def cmdargs(opts): args = [] for k, v in opts.iteritems(): if isinstance(v, bool): if v: args.append('--%s' % k.replace('_', '-')) else: for e in isinstance(v, basestring) and [v] or v: args += ['--%s' % k.replace('_', '-'), e] return args hglib.loadextension(self._ui, 'patchbomb') opts = self._patchbombopts() try: cmd = cmdui.Dialog(['email'] + cmdargs(opts), parent=self) cmd.setWindowTitle(_('Sending Email')) cmd.setShowOutput(False) cmd.finished.connect(cmd.deleteLater) if cmd.exec_(): self._writehistory() finally: if 'desc' in opts: os.unlink(opts['desc']) # TODO: don't use tempfile def _initintrobox(self): self._qui.intro_box.hide() # hidden by default self._qui.subject_edit.textChanged.connect(self._updateforms) self._qui.writeintro_check.toggled.connect(self._updateforms) def _introrequired(self): """Is intro message required?""" return len(self._revs) > 1 or self._qui.bundle_radio.isChecked() def _initpreviewtab(self): def initqsci(w): w.setUtf8(True) w.setReadOnly(True) w.setMarginWidth(1, 0) # hide area for line numbers self.lexer = lex = lexers.get_diff_lexer(self) fh = qtlib.getfont('fontdiff') fh.changed.connect(self.forwardFont) lex.setFont(fh.font()) w.setLexer(lex) # TODO: better way to setup diff lexer initqsci(self._qui.preview_edit) self._qui.main_tabs.currentChanged.connect(self._refreshpreviewtab) self._refreshpreviewtab(self._qui.main_tabs.currentIndex()) def forwardFont(self, font): if self.lexer: self.lexer.setFont(font) @pyqtSlot(int) def _refreshpreviewtab(self, index): """Generate preview text if current tab is preview""" if self._previewtabindex() != index: return self._qui.preview_edit.setText(self._preview()) def _preview(self): """Generate preview text by running patchbomb""" def loadpatchbomb(): hglib.loadextension(self._ui, 'patchbomb') return extensions.find('patchbomb') def wrapui(ui): buf = StringIO() # TODO: common way to prepare pure ui newui = ui.copy() newui.setconfig('ui', 'interactive', False) newui.setconfig('diff', 'git', False) newui.write = lambda *args, **opts: buf.write(''.join(args)) newui.status = lambda *args, **opts: None return newui, buf def stripheadmsg(s): # TODO: skip until first Content-type: line ?? return '\n'.join(s.splitlines()[3:]) ui, buf = wrapui(self._ui) opts = self._patchbombopts(test=True) try: # TODO: fix hgext.patchbomb's implementation instead if 'PAGER' in os.environ: del os.environ['PAGER'] loadpatchbomb().patchbomb(ui, self._repo, **opts) return stripheadmsg(hglib.tounicode(buf.getvalue())) finally: if 'desc' in opts: os.unlink(opts['desc']) # TODO: don't use tempfile def _previewtabindex(self): """Index of preview tab""" return self._qui.main_tabs.indexOf(self._qui.preview_tab) @pyqtSlot() def on_settings_button_clicked(self): from tortoisehg.hgqt import settings if settings.SettingsDialog(parent=self, focus='email.from').exec_(): # not use repo.configChanged because it can clobber user input # accidentally. self._repo.invalidateui() # force reloading config immediately self._filldefaults() @pyqtSlot() def on_selectall_button_clicked(self): self._changesets.selectAll() @pyqtSlot() def on_selectnone_button_clicked(self): self._changesets.selectNone()
class EmailDialog(QDialog): """Dialog for sending patches via email""" def __init__(self, repoagent, revs, parent=None, outgoing=False, outgoingrevs=None): """Create EmailDialog for the given repo and revs :revs: List of revisions to be sent. :outgoing: Enable outgoing bundle support. You also need to set outgoing revisions to `revs`. :outgoingrevs: Target revision of outgoing bundle. (Passed as `hg email --bundle --rev {rev}`) """ super(EmailDialog, self).__init__(parent) self.setWindowFlags(Qt.Window) self._repoagent = repoagent self._outgoing = outgoing self._outgoingrevs = outgoingrevs or [] self._qui = Ui_EmailDialog() self._qui.setupUi(self) self._initchangesets(revs) self._initpreviewtab() self._initenvelopebox() self._qui.bundle_radio.toggled.connect(self._updateforms) self._qui.attach_check.toggled.connect(self._updateattachmodes) self._qui.inline_check.toggled.connect(self._updateattachmodes) self._initintrobox() self._readhistory() self._filldefaults() self._updateforms() self._updateattachmodes() self._readsettings() QShortcut(QKeySequence("CTRL+Return"), self, self.accept) QShortcut(QKeySequence("Ctrl+Enter"), self, self.accept) def closeEvent(self, event): self._writesettings() super(EmailDialog, self).closeEvent(event) def _readsettings(self): s = QSettings() self.restoreGeometry(s.value("email/geom").toByteArray()) self._qui.intro_changesets_splitter.restoreState(s.value("email/intro_changesets_splitter").toByteArray()) def _writesettings(self): s = QSettings() s.setValue("email/geom", self.saveGeometry()) s.setValue("email/intro_changesets_splitter", self._qui.intro_changesets_splitter.saveState()) def _readhistory(self): s = QSettings() for k in ("to", "cc", "from", "flag", "subject"): w = getattr(self._qui, "%s_edit" % k) w.addItems(s.value("email/%s_history" % k).toStringList()) w.setCurrentIndex(-1) # unselect for k in ("body", "attach", "inline", "diffstat"): w = getattr(self._qui, "%s_check" % k) w.setChecked(s.value("email/%s" % k).toBool()) def _writehistory(self): def itercombo(w): if w.currentText(): yield w.currentText() for i in xrange(w.count()): if w.itemText(i) != w.currentText(): yield w.itemText(i) s = QSettings() for k in ("to", "cc", "from", "flag", "subject"): w = getattr(self._qui, "%s_edit" % k) s.setValue("email/%s_history" % k, list(itercombo(w))[:10]) for k in ("body", "attach", "inline", "diffstat"): w = getattr(self._qui, "%s_check" % k) s.setValue("email/%s" % k, w.isChecked()) def _initchangesets(self, revs): def purerevs(revs): return scmutil.revrange(self._repo, iter(str(e) for e in revs)) self._changesets = _ChangesetsModel( self._repo, # TODO: [':'] is inefficient revs=purerevs(revs or [":"]), selectedrevs=purerevs(revs), parent=self, ) self._changesets.dataChanged.connect(self._updateforms) self._qui.changesets_view.setModel(self._changesets) @property def _repo(self): return self._repoagent.rawRepo() @property def _ui(self): return self._repo.ui @property def _revs(self): """Returns list of revisions to be sent""" return self._changesets.selectedrevs def _filldefaults(self): """Fill form by default values""" def getfromaddr(ui): """Get sender address in the same manner as patchbomb""" addr = ui.config("email", "from") or ui.config("patchbomb", "from") if addr: return addr try: return ui.username() except error.Abort: return "" self._qui.to_edit.setEditText(hglib.tounicode(self._ui.config("email", "to", ""))) self._qui.cc_edit.setEditText(hglib.tounicode(self._ui.config("email", "cc", ""))) self._qui.from_edit.setEditText(hglib.tounicode(getfromaddr(self._ui))) self.setdiffformat(self._ui.configbool("diff", "git") and "git" or "hg") def setdiffformat(self, format): """Set diff format, 'hg', 'git' or 'plain'""" try: radio = getattr(self._qui, "%spatch_radio" % format) except AttributeError: raise ValueError("unknown diff format: %r" % format) radio.setChecked(True) def getdiffformat(self): """Selected diff format""" for e in self._qui.patch_frame.children(): m = re.match(r"(\w+)patch_radio", str(e.objectName())) if m and e.isChecked(): return m.group(1) return "hg" def getextraopts(self): """Dict of extra options""" opts = {} for e in self._qui.extra_frame.children(): m = re.match(r"(\w+)_check", str(e.objectName())) if m: opts[m.group(1)] = e.isChecked() return opts def _patchbombopts(self, **opts): """Generate opts for patchbomb by form values""" def headertext(s): # QLineEdit may contain newline character return re.sub(r"\s", " ", hglib.fromunicode(s)) opts["to"] = [headertext(self._qui.to_edit.currentText())] opts["cc"] = [headertext(self._qui.cc_edit.currentText())] opts["from"] = headertext(self._qui.from_edit.currentText()) opts["in_reply_to"] = headertext(self._qui.inreplyto_edit.text()) opts["flag"] = [headertext(self._qui.flag_edit.currentText())] if self._qui.bundle_radio.isChecked(): assert self._outgoing # only outgoing bundle is supported opts["rev"] = map(str, self._outgoingrevs) opts["bundle"] = True else: opts["rev"] = map(str, self._revs) def diffformat(): n = self.getdiffformat() if n == "hg": return {} else: return {n: True} opts.update(diffformat()) opts.update(self.getextraopts()) def writetempfile(s): fd, fname = tempfile.mkstemp(prefix="thg_emaildesc_") try: os.write(fd, s) return fname finally: os.close(fd) opts["intro"] = self._qui.writeintro_check.isChecked() if opts["intro"]: opts["subject"] = headertext(self._qui.subject_edit.currentText()) opts["desc"] = writetempfile(hglib.fromunicode(self._qui.body_edit.toPlainText())) # TODO: change patchbomb not to use temporary file # Include the repo in the command so it can be found when thg is not # run from within a hg path opts["repository"] = self._repo.root return opts def _isvalid(self): """Filled all required values?""" for e in ("to_edit", "from_edit"): if not getattr(self._qui, e).currentText(): return False if self._qui.writeintro_check.isChecked() and not self._qui.subject_edit.currentText(): return False if not self._revs: return False return True @pyqtSlot() def _updateforms(self): """Update availability of form widgets""" valid = self._isvalid() self._qui.send_button.setEnabled(valid) self._qui.main_tabs.setTabEnabled(self._previewtabindex(), valid) self._qui.writeintro_check.setEnabled(not self._introrequired()) self._qui.bundle_radio.setEnabled(self._outgoing and self._changesets.isselectedall()) self._changesets.setReadOnly(self._qui.bundle_radio.isChecked()) if self._qui.bundle_radio.isChecked(): # workaround to disable preview for outgoing bundle because it # may freeze main thread self._qui.main_tabs.setTabEnabled(self._previewtabindex(), False) if self._introrequired(): self._qui.writeintro_check.setChecked(True) # @pyqtSlot() def _updateattachmodes(self): """Update checkboxes to select the embedding style of the patch""" attachmodes = [self._qui.attach_check, self._qui.inline_check] body = self._qui.body_check # --attach and --inline are exclusive if self.sender() in attachmodes and self.sender().isChecked(): for w in attachmodes: if w is not self.sender(): w.setChecked(False) # --body is mandatory if no attach modes are specified body.setEnabled(util.any(w.isChecked() for w in attachmodes)) if not body.isEnabled(): body.setChecked(True) def _initenvelopebox(self): for e in ("to_edit", "from_edit"): getattr(self._qui, e).editTextChanged.connect(self._updateforms) def accept(self): hglib.loadextension(self._ui, "patchbomb") opts = self._patchbombopts() try: cmd = cmdui.Dialog(hglib.buildcmdargs("email", **opts), parent=self) cmd.setWindowTitle(_("Sending Email")) cmd.setShowOutput(False) cmd.finished.connect(cmd.deleteLater) if cmd.exec_(): self._writehistory() finally: if "desc" in opts: os.unlink(opts["desc"]) # TODO: don't use tempfile def _initintrobox(self): self._qui.intro_box.hide() # hidden by default self._qui.subject_edit.editTextChanged.connect(self._updateforms) self._qui.writeintro_check.toggled.connect(self._updateforms) def _introrequired(self): """Is intro message required?""" return len(self._revs) > 1 or self._qui.bundle_radio.isChecked() def _initpreviewtab(self): def initqsci(w): w.setUtf8(True) w.setReadOnly(True) w.setMarginWidth(1, 0) # hide area for line numbers self.lexer = lex = lexers.difflexer(self) fh = qtlib.getfont("fontdiff") fh.changed.connect(self.forwardFont) lex.setFont(fh.font()) w.setLexer(lex) # TODO: better way to setup diff lexer initqsci(self._qui.preview_edit) self._qui.main_tabs.currentChanged.connect(self._refreshpreviewtab) self._refreshpreviewtab(self._qui.main_tabs.currentIndex()) def forwardFont(self, font): if self.lexer: self.lexer.setFont(font) @pyqtSlot(int) def _refreshpreviewtab(self, index): """Generate preview text if current tab is preview""" if self._previewtabindex() != index: return self._qui.preview_edit.setText(self._preview()) def _preview(self): """Generate preview text by running patchbomb""" def loadpatchbomb(): hglib.loadextension(self._ui, "patchbomb") return extensions.find("patchbomb") def wrapui(ui): buf = StringIO() # TODO: common way to prepare pure ui newui = ui.copy() newui.setconfig("ui", "interactive", False) newui.setconfig("diff", "git", False) newui.write = lambda *args, **opts: buf.write("".join(args)) newui.status = lambda *args, **opts: None return newui, buf def stripheadmsg(s): # TODO: skip until first Content-type: line ?? return "\n".join(s.splitlines()[3:]) ui, buf = wrapui(self._ui) opts = self._patchbombopts(test=True) try: # TODO: fix hgext.patchbomb's implementation instead if "PAGER" in os.environ: del os.environ["PAGER"] loadpatchbomb().patchbomb(ui, self._repo, **opts) return stripheadmsg(hglib.tounicode(buf.getvalue())) finally: if "desc" in opts: os.unlink(opts["desc"]) # TODO: don't use tempfile def _previewtabindex(self): """Index of preview tab""" return self._qui.main_tabs.indexOf(self._qui.preview_tab) @pyqtSlot() def on_settings_button_clicked(self): from tortoisehg.hgqt import settings if settings.SettingsDialog(parent=self, focus="email.from").exec_(): # not use repo.configChanged because it can clobber user input # accidentally. self._repo.invalidateui() # force reloading config immediately self._filldefaults() @pyqtSlot() def on_selectall_button_clicked(self): self._changesets.selectAll() @pyqtSlot() def on_selectnone_button_clicked(self): self._changesets.selectNone()