# HG changeset patch # User Chris Cannam # Date 1309266942 -3600 # Node ID b616c9c6cfd25bc42738c101cbbf9bc573590654 # Parent 0d779f3cb4bc6427ac915102d5b7d0fa80acc2e1 Bring new-look easyhg.py into action diff -r 0d779f3cb4bc -r b616c9c6cfd2 easyhg.py --- a/easyhg.py Tue Jun 28 14:15:08 2011 +0100 +++ b/easyhg.py Tue Jun 28 14:15:42 2011 +0100 @@ -13,9 +13,14 @@ # License, or (at your option) any later version. See the file # COPYING included with this distribution for more information. -import sys -from mercurial import ui, getpass, util +import sys, os, stat, urllib, urllib2, urlparse + from mercurial.i18n import _ +from mercurial import ui, util, error +try: + from mercurial.url import passwordmgr +except: + from mercurial.httprepo import passwordmgr # The value assigned here may be modified during installation, by # replacing its default value with another one. We can't compare @@ -26,7 +31,7 @@ if not easyhg_import_path.startswith('NO_'): # We have an installation path: append it twice, once with # the Python version suffixed - version_suffix = "Py" + str(sys.version_info[0]) + "." + str(sys.version_info[1]); + version_suffix = 'Py%d.%d' % (sys.version_info[0], sys.version_info[1]) sys.path.append(easyhg_import_path + "/" + version_suffix) sys.path.append(easyhg_import_path) @@ -39,64 +44,215 @@ # easyhg_pyqt_ok = True try: - from PyQt4 import QtGui + from PyQt4 import Qt, QtGui except ImportError: easyhg_pyqt_ok = False +easyhg_qtapp = None -easyhg_qtapp = None +# These imports are optional, we just can't use the authfile (i.e. +# "remember this password") feature without them +# +easyhg_authfile_imports_ok = True +try: + from Crypto.Cipher import AES + import ConfigParser # Mercurial version won't write files + import base64 +except ImportError: + easyhg_authfile_imports_ok = False + + +def encrypt_salted(text, key): + salt = os.urandom(8) + text = '%d.%s.%s' % (len(text), base64.b64encode(salt), text) + text += (16 - len(text) % 16) * ' ' + cipher = AES.new(key) + return base64.b64encode(cipher.encrypt(text)) + +def decrypt_salted(ctext, key): + cipher = AES.new(key) + text = cipher.decrypt(base64.b64decode(ctext)) + (tlen, d, text) = text.partition('.') + (salt, d, text) = text.partition('.') + return text[0:int(tlen)] + +# from mercurial_keyring by Marcin Kasperski +def canonical_url(authuri): + parsed_url = urlparse.urlparse(authuri) + return "%s://%s%s" % (parsed_url.scheme, parsed_url.netloc, + parsed_url.path) + +def load_config(pcfg, pfile): + fp = None + try: + fp = open(pfile) + except: + return + pcfg.readfp(fp) + fp.close() + +def save_config(pcfg, pfile): + ofp = None + try: + ofp = open(pfile, 'w') + except: + self.ui.write("failed to open authfile %s for writing\n" % pfile) + raise + try: + #!!! Windows equivalent? + os.fchmod(ofp.fileno(), stat.S_IRUSR | stat.S_IWUSR) + except: + ofp.close() + self.ui.write("failed to set permissions on authfile %s\n" % pfile) + raise + pcfg.write(ofp) + ofp.close() + +def get_from_config(pcfg, sect, key): + data = None + try: + data = pcfg.get(sect, key) + except (ConfigParser.NoOptionError, ConfigParser.NoSectionError): + pass + return data + +def get_boolean_from_config(pcfg, sect, key, deflt): + data = deflt + try: + data = pcfg.getboolean(sect, key) + except (ConfigParser.NoOptionError, ConfigParser.NoSectionError): + pass + return data + +def set_to_config(pcfg, sect, key, data): + if not pcfg.has_section(sect): + pcfg.add_section(sect) + pcfg.set(sect, key, data) + +def remote_key(uri, user): + # generate a "safe-for-config-file" key representing uri+user + # tuple (n.b. trailing = on base64 is not safe) + return base64.b64encode('%s@@%s' % (uri, user)).replace('=', '_') + def uisetup(ui): if not easyhg_pyqt_ok: raise util.Abort(_('Failed to load PyQt4 module required by easyhg.py')) - ui.__class__.prompt = easyhg_prompt - ui.__class__.getpass = easyhg_getpass global easyhg_qtapp easyhg_qtapp = QtGui.QApplication([]) -def easyhg_prompt(self, msg, default="y"): - if not self.interactive(): - self.write(msg, ' ', default, "\n") - return default - isusername = False - if msg == _('user:'): - msg = _('Username for remote repository:') - isusername = True - d = QtGui.QInputDialog() - d.setInputMode(QtGui.QInputDialog.TextInput) - d.setTextEchoMode(QtGui.QLineEdit.Normal) - d.setLabelText(msg) - d.setWindowTitle(_('EasyMercurial: Information')) - d.show() - d.raise_() - ok = d.exec_() - r = d.textValue() - if not ok: - if isusername: - raise util.Abort(_('username entry cancelled')) - else: - raise util.Abort(_('information entry cancelled')) - if not r: - return default - return r +def monkeypatch_method(cls): + def decorator(func): + setattr(cls, func.__name__, func) + return func + return decorator -def easyhg_getpass(self, prompt=None, default=None): - if not self.interactive(): - return default - if not prompt or prompt == _('password:'): - prompt = _('Password for remote repository:'); - d = QtGui.QInputDialog() - d.setInputMode(QtGui.QInputDialog.TextInput) - d.setTextEchoMode(QtGui.QLineEdit.Password) - d.setLabelText(prompt) - d.setWindowTitle(_('EasyMercurial: Password')) - d.show() - d.raise_() - ok = d.exec_() - r = d.textValue() +orig_find = passwordmgr.find_user_password + +@monkeypatch_method(passwordmgr) +def find_user_password(self, realm, authuri): + + if not self.ui.interactive(): + return orig_find(self, realm, authuri) + if not easyhg_pyqt_ok: + return orig_find(self, realm, authuri) + + authinfo = urllib2.HTTPPasswordMgrWithDefaultRealm.find_user_password( + self, realm, authuri) + user, passwd = authinfo + + if user and passwd: + return orig_find(self, realm, authuri) + +# self.ui.write("want username and/or password for %s\n" % authuri) + + short_uri = canonical_url(authuri) + + authkey = self.ui.config('easyhg', 'authkey') + authfile = self.ui.config('easyhg', 'authfile') + use_authfile = (easyhg_authfile_imports_ok and authkey and authfile) + if authfile: + authfile = os.path.expanduser(authfile) + authdata = None + + dialog = QtGui.QDialog() + layout = QtGui.QGridLayout() + dialog.setLayout(layout) + + layout.addWidget(QtGui.QLabel(_('
Please provide your login details for the repository at%s
:') % short_uri), 0, 0, 1, 2)
+
+ user_field = QtGui.QLineEdit()
+ if user:
+ user_field.setText(user)
+ layout.addWidget(QtGui.QLabel(_('User:')), 1, 0)
+ layout.addWidget(user_field, 1, 1)
+
+ passwd_field = QtGui.QLineEdit()
+ passwd_field.setEchoMode(QtGui.QLineEdit.Password)
+ if passwd:
+ passwd_field.setText(passwd)
+ layout.addWidget(QtGui.QLabel(_('Password:')), 2, 0)
+ layout.addWidget(passwd_field, 2, 1)
+
+ user_field.connect(user_field, Qt.SIGNAL("textChanged(QString)"),
+ passwd_field, Qt.SLOT("clear()"))
+
+ remember_field = None
+ remember = False
+ authconfig = None
+
+ if use_authfile:
+ authconfig = ConfigParser.RawConfigParser()
+ load_config(authconfig, authfile)
+ remember = get_boolean_from_config(authconfig, 'preferences',
+ 'remember', False)
+ authdata = get_from_config(authconfig, 'auth',
+ remote_key(short_uri, user))
+ if authdata:
+ cachedpwd = decrypt_salted(authdata, authkey)
+ passwd_field.setText(cachedpwd)
+ remember_field = QtGui.QCheckBox()
+ remember_field.setChecked(remember)
+ remember_field.setText(_('Remember this password until EasyMercurial exits'))
+ layout.addWidget(remember_field, 3, 1)
+
+ bb = QtGui.QDialogButtonBox()
+ ok = bb.addButton(bb.Ok)
+ cancel = bb.addButton(bb.Cancel)
+ cancel.setDefault(False)
+ cancel.setAutoDefault(False)
+ ok.setDefault(True)
+ bb.connect(ok, Qt.SIGNAL("clicked()"), dialog, Qt.SLOT("accept()"))
+ bb.connect(cancel, Qt.SIGNAL("clicked()"), dialog, Qt.SLOT("reject()"))
+ layout.addWidget(bb, 4, 0, 1, 2)
+
+ dialog.setWindowTitle(_('EasyMercurial: Login'))
+ dialog.show()
+
+ if not user:
+ user_field.setFocus(True)
+ elif not passwd:
+ passwd_field.setFocus(True)
+
+ dialog.raise_()
+ ok = dialog.exec_()
if not ok:
raise util.Abort(_('password entry cancelled'))
- if not r:
- return default
- return r
-
+ user = user_field.text()
+ passwd = passwd_field.text()
+
+ if use_authfile:
+ remember = remember_field.isChecked()
+ set_to_config(authconfig, 'preferences', 'remember', remember)
+ if user:
+ if passwd and remember:
+ authdata = encrypt_salted(passwd, authkey)
+ set_to_config(authconfig, 'auth', remote_key(short_uri, user), authdata)
+ else:
+ set_to_config(authconfig, 'auth', remote_key(short_uri, user), '')
+ save_config(authconfig, authfile)
+
+ self.add_password(realm, authuri, user, passwd)
+ return (user, passwd)
+
+
diff -r 0d779f3cb4bc -r b616c9c6cfd2 easyhg1.py
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/easyhg1.py Tue Jun 28 14:15:42 2011 +0100
@@ -0,0 +1,102 @@
+# -*- coding: utf-8 -*-
+#
+# EasyMercurial
+#
+# Based on hgExplorer by Jari Korhonen
+# Copyright (c) 2010 Jari Korhonen
+# Copyright (c) 2010 Chris Cannam
+# Copyright (c) 2010 Queen Mary, University of London
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; either version 2 of the
+# License, or (at your option) any later version. See the file
+# COPYING included with this distribution for more information.
+
+import sys
+from mercurial import ui, getpass, util
+from mercurial.i18n import _
+
+# The value assigned here may be modified during installation, by
+# replacing its default value with another one. We can't compare
+# against its default value, because then the comparison text would
+# get modified as well. So, compare using prefix only.
+#
+easyhg_import_path = 'NO_EASYHG_IMPORT_PATH'
+if not easyhg_import_path.startswith('NO_'):
+ # We have an installation path: append it twice, once with
+ # the Python version suffixed
+ version_suffix = "Py" + str(sys.version_info[0]) + "." + str(sys.version_info[1]);
+ sys.path.append(easyhg_import_path + "/" + version_suffix)
+ sys.path.append(easyhg_import_path)
+
+# Try to load the PyQt4 module that we need. If this fails, we should
+# bail out later (in uisetup), because if we bail out now, Mercurial
+# will just continue without us and report success. The invoking
+# application needs to be able to discover whether the module load
+# succeeded or not, so we need to ensure that Mercurial itself returns
+# failure if it didn't.
+#
+easyhg_pyqt_ok = True
+try:
+ from PyQt4 import QtGui
+except ImportError:
+ easyhg_pyqt_ok = False
+
+easyhg_qtapp = None
+
+def uisetup(ui):
+ if not easyhg_pyqt_ok:
+ raise util.Abort(_('Failed to load PyQt4 module required by easyhg.py'))
+ ui.__class__.prompt = easyhg_prompt
+ ui.__class__.getpass = easyhg_getpass
+ global easyhg_qtapp
+ easyhg_qtapp = QtGui.QApplication([])
+
+def easyhg_prompt(self, msg, default="y"):
+ if not self.interactive():
+ self.write(msg, ' ', default, "\n")
+ return default
+ isusername = False
+ if msg == _('user:'):
+ msg = _('Username for remote repository:')
+ isusername = True
+ d = QtGui.QInputDialog()
+ d.setInputMode(QtGui.QInputDialog.TextInput)
+ d.setTextEchoMode(QtGui.QLineEdit.Normal)
+ d.setLabelText(msg)
+ d.setWindowTitle(_('EasyMercurial: Information'))
+ d.show()
+ d.raise_()
+ ok = d.exec_()
+ r = d.textValue()
+ if not ok:
+ if isusername:
+ raise util.Abort(_('username entry cancelled'))
+ else:
+ raise util.Abort(_('information entry cancelled'))
+ if not r:
+ return default
+ return r
+
+def easyhg_getpass(self, prompt=None, default=None):
+ if not self.interactive():
+ return default
+ if not prompt or prompt == _('password:'):
+ prompt = _('Password for remote repository:');
+ d = QtGui.QInputDialog()
+ d.setInputMode(QtGui.QInputDialog.TextInput)
+ d.setTextEchoMode(QtGui.QLineEdit.Password)
+ d.setLabelText(prompt)
+ d.setWindowTitle(_('EasyMercurial: Password'))
+ d.show()
+ d.raise_()
+ ok = d.exec_()
+ r = d.textValue()
+ if not ok:
+ raise util.Abort(_('password entry cancelled'))
+ if not r:
+ return default
+ return r
+
+
diff -r 0d779f3cb4bc -r b616c9c6cfd2 easyhg2.py
--- a/easyhg2.py Tue Jun 28 14:15:08 2011 +0100
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,258 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# EasyMercurial
-#
-# Based on hgExplorer by Jari Korhonen
-# Copyright (c) 2010 Jari Korhonen
-# Copyright (c) 2010 Chris Cannam
-# Copyright (c) 2010 Queen Mary, University of London
-#
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of the GNU General Public License as
-# published by the Free Software Foundation; either version 2 of the
-# License, or (at your option) any later version. See the file
-# COPYING included with this distribution for more information.
-
-import sys, os, stat, urllib, urllib2, urlparse
-
-from mercurial.i18n import _
-from mercurial import ui, util, error
-try:
- from mercurial.url import passwordmgr
-except:
- from mercurial.httprepo import passwordmgr
-
-# The value assigned here may be modified during installation, by
-# replacing its default value with another one. We can't compare
-# against its default value, because then the comparison text would
-# get modified as well. So, compare using prefix only.
-#
-easyhg_import_path = 'NO_EASYHG_IMPORT_PATH'
-if not easyhg_import_path.startswith('NO_'):
- # We have an installation path: append it twice, once with
- # the Python version suffixed
- version_suffix = 'Py%d.%d' % (sys.version_info[0], sys.version_info[1])
- sys.path.append(easyhg_import_path + "/" + version_suffix)
- sys.path.append(easyhg_import_path)
-
-# Try to load the PyQt4 module that we need. If this fails, we should
-# bail out later (in uisetup), because if we bail out now, Mercurial
-# will just continue without us and report success. The invoking
-# application needs to be able to discover whether the module load
-# succeeded or not, so we need to ensure that Mercurial itself returns
-# failure if it didn't.
-#
-easyhg_pyqt_ok = True
-try:
- from PyQt4 import Qt, QtGui
-except ImportError:
- easyhg_pyqt_ok = False
-easyhg_qtapp = None
-
-# These imports are optional, we just can't use the authfile (i.e.
-# "remember this password") feature without them
-#
-easyhg_authfile_imports_ok = True
-try:
- from Crypto.Cipher import AES
- import ConfigParser # Mercurial version won't write files
- import base64
-except ImportError:
- easyhg_authfile_imports_ok = False
-
-
-def encrypt_salted(text, key):
- salt = os.urandom(8)
- text = '%d.%s.%s' % (len(text), base64.b64encode(salt), text)
- text += (16 - len(text) % 16) * ' '
- cipher = AES.new(key)
- return base64.b64encode(cipher.encrypt(text))
-
-def decrypt_salted(ctext, key):
- cipher = AES.new(key)
- text = cipher.decrypt(base64.b64decode(ctext))
- (tlen, d, text) = text.partition('.')
- (salt, d, text) = text.partition('.')
- return text[0:int(tlen)]
-
-# from mercurial_keyring by Marcin Kasperski
-def canonical_url(authuri):
- parsed_url = urlparse.urlparse(authuri)
- return "%s://%s%s" % (parsed_url.scheme, parsed_url.netloc,
- parsed_url.path)
-
-def load_config(pcfg, pfile):
- fp = None
- try:
- fp = open(pfile)
- except:
- return
- pcfg.readfp(fp)
- fp.close()
-
-def save_config(pcfg, pfile):
- ofp = None
- try:
- ofp = open(pfile, 'w')
- except:
- self.ui.write("failed to open authfile %s for writing\n" % pfile)
- raise
- try:
- #!!! Windows equivalent?
- os.fchmod(ofp.fileno(), stat.S_IRUSR | stat.S_IWUSR)
- except:
- ofp.close()
- self.ui.write("failed to set permissions on authfile %s\n" % pfile)
- raise
- pcfg.write(ofp)
- ofp.close()
-
-def get_from_config(pcfg, sect, key):
- data = None
- try:
- data = pcfg.get(sect, key)
- except (ConfigParser.NoOptionError, ConfigParser.NoSectionError):
- pass
- return data
-
-def get_boolean_from_config(pcfg, sect, key, deflt):
- data = deflt
- try:
- data = pcfg.getboolean(sect, key)
- except (ConfigParser.NoOptionError, ConfigParser.NoSectionError):
- pass
- return data
-
-def set_to_config(pcfg, sect, key, data):
- if not pcfg.has_section(sect):
- pcfg.add_section(sect)
- pcfg.set(sect, key, data)
-
-def remote_key(uri, user):
- # generate a "safe-for-config-file" key representing uri+user
- # tuple (n.b. trailing = on base64 is not safe)
- return base64.b64encode('%s@@%s' % (uri, user)).replace('=', '_')
-
-
-def uisetup(ui):
- if not easyhg_pyqt_ok:
- raise util.Abort(_('Failed to load PyQt4 module required by easyhg.py'))
- global easyhg_qtapp
- easyhg_qtapp = QtGui.QApplication([])
-
-def monkeypatch_method(cls):
- def decorator(func):
- setattr(cls, func.__name__, func)
- return func
- return decorator
-
-orig_find = passwordmgr.find_user_password
-
-@monkeypatch_method(passwordmgr)
-def find_user_password(self, realm, authuri):
-
- if not self.ui.interactive():
- return orig_find(self, realm, authuri)
- if not easyhg_pyqt_ok:
- return orig_find(self, realm, authuri)
-
- authinfo = urllib2.HTTPPasswordMgrWithDefaultRealm.find_user_password(
- self, realm, authuri)
- user, passwd = authinfo
-
- if user and passwd:
- return orig_find(self, realm, authuri)
-
-# self.ui.write("want username and/or password for %s\n" % authuri)
-
- short_uri = canonical_url(authuri)
-
- authkey = self.ui.config('easyhg', 'authkey')
- authfile = self.ui.config('easyhg', 'authfile')
- use_authfile = (easyhg_authfile_imports_ok and authkey and authfile)
- if authfile:
- authfile = os.path.expanduser(authfile)
- authdata = None
-
- dialog = QtGui.QDialog()
- layout = QtGui.QGridLayout()
- dialog.setLayout(layout)
-
- layout.addWidget(QtGui.QLabel(_('
Please provide your login details for the repository at%s
:') % short_uri), 0, 0, 1, 2)
-
- user_field = QtGui.QLineEdit()
- if user:
- user_field.setText(user)
- layout.addWidget(QtGui.QLabel(_('User:')), 1, 0)
- layout.addWidget(user_field, 1, 1)
-
- passwd_field = QtGui.QLineEdit()
- passwd_field.setEchoMode(QtGui.QLineEdit.Password)
- if passwd:
- passwd_field.setText(passwd)
- layout.addWidget(QtGui.QLabel(_('Password:')), 2, 0)
- layout.addWidget(passwd_field, 2, 1)
-
- user_field.connect(user_field, Qt.SIGNAL("textChanged(QString)"),
- passwd_field, Qt.SLOT("clear()"))
-
- remember_field = None
- remember = False
- authconfig = None
-
- if use_authfile:
- authconfig = ConfigParser.RawConfigParser()
- load_config(authconfig, authfile)
- remember = get_boolean_from_config(authconfig, 'preferences',
- 'remember', False)
- authdata = get_from_config(authconfig, 'auth',
- remote_key(short_uri, user))
- if authdata:
- cachedpwd = decrypt_salted(authdata, authkey)
- passwd_field.setText(cachedpwd)
- remember_field = QtGui.QCheckBox()
- remember_field.setChecked(remember)
- remember_field.setText(_('Remember this password until EasyMercurial exits'))
- layout.addWidget(remember_field, 3, 1)
-
- bb = QtGui.QDialogButtonBox()
- ok = bb.addButton(bb.Ok)
- cancel = bb.addButton(bb.Cancel)
- cancel.setDefault(False)
- cancel.setAutoDefault(False)
- ok.setDefault(True)
- bb.connect(ok, Qt.SIGNAL("clicked()"), dialog, Qt.SLOT("accept()"))
- bb.connect(cancel, Qt.SIGNAL("clicked()"), dialog, Qt.SLOT("reject()"))
- layout.addWidget(bb, 4, 0, 1, 2)
-
- dialog.setWindowTitle(_('EasyMercurial: Login'))
- dialog.show()
-
- if not user:
- user_field.setFocus(True)
- elif not passwd:
- passwd_field.setFocus(True)
-
- dialog.raise_()
- ok = dialog.exec_()
- if not ok:
- raise util.Abort(_('password entry cancelled'))
-
- user = user_field.text()
- passwd = passwd_field.text()
-
- if use_authfile:
- remember = remember_field.isChecked()
- set_to_config(authconfig, 'preferences', 'remember', remember)
- if user:
- if passwd and remember:
- authdata = encrypt_salted(passwd, authkey)
- set_to_config(authconfig, 'auth', remote_key(short_uri, user), authdata)
- else:
- set_to_config(authconfig, 'auth', remote_key(short_uri, user), '')
- save_config(authconfig, authfile)
-
- self.add_password(realm, authuri, user, passwd)
- return (user, passwd)
-
-