diff easyhg.py @ 441:b616c9c6cfd2

Bring new-look easyhg.py into action
author Chris Cannam
date Tue, 28 Jun 2011 14:15:42 +0100
parents easyhg2.py@0d779f3cb4bc
children 66ec8b2ee946
line wrap: on
line diff
--- 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(_('<h3>Login required</h3><p>Please provide your login details for the repository at<br><code>%s</code>:') % 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)
+
+