Changeset faa2db22b83d…
Parent 64e8c7b2d82d…
by
Changes to 3 files · Browse files at faa2db22b83d Showing diff from parent 64e8c7b2d82d Diff from another changeset...
|
|
@@ -0,0 +1,295 @@ + # hgemail.py - TortoiseHg's dialog for sending patches via email
+#
+# Copyright 2007 TK Soh <teekaysoh@gmail.com>
+# Copyright 2007 Steve Borho <steve@borho.org>
+# Copyright 2010 Yuya Nishihara <yuya@tcha.org>
+#
+# This software may be used and distributed according to the terms of the
+# GNU General Public License version 2, incorporated herein by reference.
+
+import os, tempfile, re
+from StringIO import StringIO
+from PyQt4.QtCore import SIGNAL, Qt, QAbstractTableModel, QVariant, QModelIndex
+from PyQt4.QtGui import QDialog
+from mercurial import hg, error, extensions, util
+from tortoisehg.util import hglib, paths
+from tortoisehg.hgqt.i18n import _
+from tortoisehg.hgqt import cmdui
+
+try:
+ from tortoisehg.hgqt.ui_hgemail import Ui_EmailDialog
+except ImportError:
+ from PyQt4 import uic
+ Ui_EmailDialog = uic.loadUiType(os.path.join(os.path.dirname(__file__),
+ 'hgemail.ui'))[0]
+
+class EmailDialog(QDialog):
+ """Dialog for sending patches via email"""
+ def __init__(self, ui, repo, revs, parent=None):
+ super(EmailDialog, self).__init__(parent)
+ self._ui = ui
+ self._repo = repo
+ self._revs = revs
+
+ self._qui = Ui_EmailDialog()
+ self._qui.setupUi(self)
+ self._qui.bundle_radio.setEnabled(False) # TODO: bundle support
+ self._qui.settings_button.setEnabled(False) # TODO: open settings dialog
+
+ changesets = _ChangesetsModel(self._repo, self._revs, parent=self)
+ self._qui.changesets_view.setModel(changesets)
+
+ self._initpreviewtab()
+ self._initintrobox()
+ self._filldefaults()
+ self._connectvalidateform()
+ self._validateform()
+
+ def keyPressEvent(self, event):
+ # don't send email by just hitting enter
+ if event.key() in (Qt.Key_Return, Qt.Key_Enter):
+ if event.modifiers() == Qt.ControlModifier:
+ self.accept() # Ctrl+Enter
+
+ return
+
+ return super(EmailDialog, self).keyPressEvent(event)
+
+ 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 ''
+
+ # TODO: ui.config's encoding?
+ self._qui.to_edit.setText(self._ui.config('email', 'to', ''))
+ self._qui.cc_edit.setText(self._ui.config('email', 'cc', ''))
+ self._qui.from_edit.setText(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"""
+ opts['to'] = [hglib.fromunicode(self._qui.to_edit.text())]
+ opts['cc'] = [hglib.fromunicode(self._qui.cc_edit.text())]
+ opts['from'] = hglib.fromunicode(self._qui.from_edit.text())
+ opts['in_reply_to'] = hglib.fromunicode(self._qui.inreplyto_edit.text())
+ opts['flag'] = [hglib.fromunicode(self._qui.flag_edit.text())]
+
+ 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'] = hglib.fromunicode(self._qui.subject_edit.text())
+ opts['desc'] = writetempfile(hglib.fromunicode(self._qui.body_edit.toPlainText()))
+ # TODO: change patchbomb not to use temporary file
+
+ return opts
+
+ def _isvalid(self):
+ """Filled all required values?"""
+ req = ['to_edit', 'from_edit']
+ if self._qui.writeintro_check.isChecked():
+ req.append('subject_edit')
+
+ for e in req:
+ if not getattr(self._qui, e).text():
+ return False
+
+ # TODO: is it nice if we can choose revisions to send?
+ if not self._revs:
+ return False
+
+ return True
+
+ def _validateform(self):
+ """Check form values to update send/preview availability"""
+ valid = self._isvalid()
+ self._qui.send_button.setEnabled(valid)
+ self._qui.main_tabs.setTabEnabled(self._previewtabindex(), valid)
+
+ def _connectvalidateform(self):
+ # TODO: connect programmatically
+ for e in ('to_edit', 'from_edit', 'subject_edit'):
+ self.connect(getattr(self._qui, e),
+ SIGNAL('textChanged(QString)'),
+ self._validateform)
+
+ self.connect(self._qui.writeintro_check, SIGNAL('toggled(bool)'),
+ self._validateform)
+
+ 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) + list(self._revs),
+ parent=self)
+ cmd.setWindowTitle(_('Sending Email'))
+ cmd.exec_()
+ finally:
+ if 'desc' in opts:
+ os.unlink(opts['desc']) # TODO: don't use tempfile
+
+ super(EmailDialog, self).accept() # TODO: if success
+
+ def _initintrobox(self):
+ self._qui.intro_box.hide() # hidden by default
+ if self._introrequired():
+ self._qui.writeintro_check.setChecked(True)
+ self._qui.writeintro_check.setEnabled(False)
+
+ def _introrequired(self):
+ """Is intro message required?"""
+ return len(self._revs) > 1
+
+ def _initpreviewtab(self):
+ self.connect(self._qui.main_tabs, SIGNAL('currentChanged(int)'),
+ self._refreshpreviewtab)
+ self._refreshpreviewtab(self._qui.main_tabs.currentIndex())
+
+ 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, *self._revs,
+ **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)
+
+class _ChangesetsModel(QAbstractTableModel): # TODO: use component of log viewer?
+ _COLUMNS = [('rev', lambda ctx: '%d:%s' % (ctx.rev(), ctx)),
+ ('author', lambda ctx: hglib.username(ctx.user())),
+ ('date', lambda ctx: util.shortdate(ctx.date())),
+ ('description', lambda ctx: ctx.description().splitlines()[0])]
+
+ def __init__(self, repo, revs, parent=None):
+ super(_ChangesetsModel, self).__init__(parent)
+ self._repo = repo
+ self._revs = revs
+
+ def data(self, index, role):
+ if (not index.isValid()) or role != Qt.DisplayRole:
+ return QVariant()
+
+ coldata = self._COLUMNS[index.column()][1]
+ rev = self._revs[index.row()]
+ return QVariant(hglib.tounicode(coldata(self._repo[rev])))
+
+ def rowCount(self, parent=QModelIndex()):
+ return len(self._revs)
+
+ def columnCount(self, parent=QModelIndex()):
+ return len(self._COLUMNS)
+
+ def headerData(self, section, orientation, role):
+ if role != Qt.DisplayRole or orientation != Qt.Horizontal:
+ return QVariant()
+
+ return QVariant(self._COLUMNS[section][0].capitalize())
+
+def run(ui, *revs, **opts):
+ # TODO: same options as patchbomb
+ # TODO: repo should be specified as an argument?
+ # TODO: if no revs specified?
+ repo = hg.repository(ui, paths.find_root())
+ return EmailDialog(repo.ui, repo, revs)
|
|
|
@@ -0,0 +1,419 @@ + <?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>EmailDialog</class>
+ <widget class="QDialog" name="EmailDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>660</width>
+ <height>506</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Email</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_5">
+ <item>
+ <widget class="QTabWidget" name="main_tabs">
+ <property name="currentIndex">
+ <number>0</number>
+ </property>
+ <property name="documentMode">
+ <bool>false</bool>
+ </property>
+ <property name="tabsClosable">
+ <bool>false</bool>
+ </property>
+ <property name="movable">
+ <bool>false</bool>
+ </property>
+ <widget class="QWidget" name="edit_tab">
+ <attribute name="title">
+ <string>Edit</string>
+ </attribute>
+ <layout class="QGridLayout" name="gridLayout">
+ <item row="0" column="0">
+ <widget class="QGroupBox" name="envelope_box">
+ <property name="title">
+ <string/>
+ </property>
+ <layout class="QFormLayout" name="formLayout">
+ <item row="0" column="0">
+ <widget class="QLabel" name="to_label">
+ <property name="text">
+ <string>To:</string>
+ </property>
+ <property name="buddy">
+ <cstring>to_edit</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QLineEdit" name="to_edit"/>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLabel" name="cc_label">
+ <property name="text">
+ <string>Cc:</string>
+ </property>
+ <property name="buddy">
+ <cstring>cc_edit</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QLineEdit" name="cc_edit"/>
+ </item>
+ <item row="2" column="0">
+ <widget class="QLabel" name="from_label">
+ <property name="text">
+ <string>From:</string>
+ </property>
+ <property name="buddy">
+ <cstring>from_edit</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="1">
+ <widget class="QLineEdit" name="from_edit"/>
+ </item>
+ <item row="3" column="0">
+ <widget class="QLabel" name="inreplyto_label">
+ <property name="text">
+ <string>In-Reply-To:</string>
+ </property>
+ <property name="buddy">
+ <cstring>inreplyto_edit</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="1">
+ <widget class="QLineEdit" name="inreplyto_edit"/>
+ </item>
+ <item row="4" column="0">
+ <widget class="QLabel" name="flag_label">
+ <property name="text">
+ <string>Flag:</string>
+ </property>
+ <property name="buddy">
+ <cstring>flag_edit</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="4" column="1">
+ <widget class="QLineEdit" name="flag_edit"/>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QGroupBox" name="options_edit">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Maximum" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="title">
+ <string/>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_4">
+ <item>
+ <widget class="QFrame" name="patch_frame">
+ <property name="frameShape">
+ <enum>QFrame::NoFrame</enum>
+ </property>
+ <property name="frameShadow">
+ <enum>QFrame::Raised</enum>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QRadioButton" name="hgpatch_radio">
+ <property name="text">
+ <string>Send changesets as Hg patches</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QRadioButton" name="gitpatch_radio">
+ <property name="text">
+ <string>Use extended (git) patch format</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QRadioButton" name="plainpatch_radio">
+ <property name="text">
+ <string>Plain, do not prepend Hg header</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QRadioButton" name="bundle_radio">
+ <property name="text">
+ <string>Send single binary bundle, not patches</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QFrame" name="extra_frame">
+ <property name="frameShape">
+ <enum>QFrame::NoFrame</enum>
+ </property>
+ <property name="frameShadow">
+ <enum>QFrame::Raised</enum>
+ </property>
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <item>
+ <widget class="QCheckBox" name="attach_check">
+ <property name="text">
+ <string>attach</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="inline_check">
+ <property name="text">
+ <string>inline</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="diffstat_check">
+ <property name="text">
+ <string>diffstat</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="extra_spacer">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item row="1" column="0" colspan="2">
+ <widget class="QCheckBox" name="writeintro_check">
+ <property name="text">
+ <string>Write patch series (bundle) description</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="0" colspan="2">
+ <widget class="QSplitter" name="intro_changesets_splitter">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <widget class="QGroupBox" name="intro_box">
+ <property name="title">
+ <string/>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <item>
+ <layout class="QHBoxLayout" name="subject_layout">
+ <item>
+ <widget class="QLabel" name="subject_label">
+ <property name="text">
+ <string>Subject:</string>
+ </property>
+ <property name="buddy">
+ <cstring>subject_edit</cstring>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLineEdit" name="subject_edit"/>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QPlainTextEdit" name="body_edit">
+ <property name="font">
+ <font>
+ <family>Monospace</family>
+ </font>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QGroupBox" name="changesets_box">
+ <property name="title">
+ <string>Changesets</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_3">
+ <item>
+ <widget class="QTreeView" name="changesets_view">
+ <property name="indentation">
+ <number>0</number>
+ </property>
+ <property name="rootIsDecorated">
+ <bool>false</bool>
+ </property>
+ <property name="itemsExpandable">
+ <bool>false</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="preview_tab">
+ <attribute name="title">
+ <string>Preview</string>
+ </attribute>
+ <layout class="QGridLayout" name="gridLayout_2">
+ <item row="0" column="0">
+ <widget class="QTextEdit" name="preview_edit">
+ <property name="font">
+ <font>
+ <family>Monospace</family>
+ </font>
+ </property>
+ <property name="readOnly">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="dialogbuttons_layout">
+ <item>
+ <widget class="QPushButton" name="settings_button">
+ <property name="toolTip">
+ <string extracomment="Configure email settings"/>
+ </property>
+ <property name="text">
+ <string>Settings</string>
+ </property>
+ <property name="autoDefault">
+ <bool>false</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>25</width>
+ <height>19</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QPushButton" name="send_button">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="text">
+ <string>Send</string>
+ </property>
+ <property name="default">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ <tabstops>
+ <tabstop>main_tabs</tabstop>
+ <tabstop>to_edit</tabstop>
+ <tabstop>cc_edit</tabstop>
+ <tabstop>from_edit</tabstop>
+ <tabstop>inreplyto_edit</tabstop>
+ <tabstop>flag_edit</tabstop>
+ <tabstop>hgpatch_radio</tabstop>
+ <tabstop>gitpatch_radio</tabstop>
+ <tabstop>plainpatch_radio</tabstop>
+ <tabstop>bundle_radio</tabstop>
+ <tabstop>attach_check</tabstop>
+ <tabstop>inline_check</tabstop>
+ <tabstop>diffstat_check</tabstop>
+ <tabstop>writeintro_check</tabstop>
+ <tabstop>subject_edit</tabstop>
+ <tabstop>body_edit</tabstop>
+ <tabstop>changesets_view</tabstop>
+ <tabstop>send_button</tabstop>
+ <tabstop>preview_edit</tabstop>
+ <tabstop>settings_button</tabstop>
+ </tabstops>
+ <resources/>
+ <connections>
+ <connection>
+ <sender>writeintro_check</sender>
+ <signal>toggled(bool)</signal>
+ <receiver>intro_box</receiver>
+ <slot>setVisible(bool)</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>129</x>
+ <y>222</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>133</x>
+ <y>252</y>
+ </hint>
+ </hints>
+ </connection>
+ <connection>
+ <sender>send_button</sender>
+ <signal>clicked()</signal>
+ <receiver>EmailDialog</receiver>
+ <slot>accept()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>641</x>
+ <y>501</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>528</x>
+ <y>506</y>
+ </hint>
+ </hints>
+ </connection>
+ <connection>
+ <sender>writeintro_check</sender>
+ <signal>toggled(bool)</signal>
+ <receiver>subject_edit</receiver>
+ <slot>setFocus()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>86</x>
+ <y>214</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>177</x>
+ <y>244</y>
+ </hint>
+ </hints>
+ </connection>
+ </connections>
+</ui>
|
@@ -337,6 +337,11 @@ from tortoisehg.hgqt.clone import run
qtrun(run, ui, *pats, **opts)
+def email(ui, *pats, **opts):
+ """send changesets by email"""
+ from tortoisehg.hgqt.hgemail import run
+ qtrun(run, ui, *pats, **opts)
+
def test(ui, *pats, **opts):
"""test arbitrary widgets"""
from tortoisehg.hgqt.status import run
@@ -594,6 +599,7 @@ _('use uncompressed transfer (fast over LAN)')),],
_('thg clone [OPTION]... SOURCE [DEST]')),
"bug": (bug, [], _('thg bug [MESSAGE]')),
+ "email": (email, [], _('thg email [REVS]')),
"^log|history|explorer": (log,
[('l', 'limit', '', _('limit number of changes displayed'))],
_('thg log [OPTIONS] [FILE]')),
|
Loading...