comparison 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
comparison
equal deleted inserted replaced
440:0d779f3cb4bc 441:b616c9c6cfd2
11 # modify it under the terms of the GNU General Public License as 11 # modify it under the terms of the GNU General Public License as
12 # published by the Free Software Foundation; either version 2 of the 12 # published by the Free Software Foundation; either version 2 of the
13 # License, or (at your option) any later version. See the file 13 # License, or (at your option) any later version. See the file
14 # COPYING included with this distribution for more information. 14 # COPYING included with this distribution for more information.
15 15
16 import sys 16 import sys, os, stat, urllib, urllib2, urlparse
17 from mercurial import ui, getpass, util 17
18 from mercurial.i18n import _ 18 from mercurial.i18n import _
19 from mercurial import ui, util, error
20 try:
21 from mercurial.url import passwordmgr
22 except:
23 from mercurial.httprepo import passwordmgr
19 24
20 # The value assigned here may be modified during installation, by 25 # The value assigned here may be modified during installation, by
21 # replacing its default value with another one. We can't compare 26 # replacing its default value with another one. We can't compare
22 # against its default value, because then the comparison text would 27 # against its default value, because then the comparison text would
23 # get modified as well. So, compare using prefix only. 28 # get modified as well. So, compare using prefix only.
24 # 29 #
25 easyhg_import_path = 'NO_EASYHG_IMPORT_PATH' 30 easyhg_import_path = 'NO_EASYHG_IMPORT_PATH'
26 if not easyhg_import_path.startswith('NO_'): 31 if not easyhg_import_path.startswith('NO_'):
27 # We have an installation path: append it twice, once with 32 # We have an installation path: append it twice, once with
28 # the Python version suffixed 33 # the Python version suffixed
29 version_suffix = "Py" + str(sys.version_info[0]) + "." + str(sys.version_info[1]); 34 version_suffix = 'Py%d.%d' % (sys.version_info[0], sys.version_info[1])
30 sys.path.append(easyhg_import_path + "/" + version_suffix) 35 sys.path.append(easyhg_import_path + "/" + version_suffix)
31 sys.path.append(easyhg_import_path) 36 sys.path.append(easyhg_import_path)
32 37
33 # Try to load the PyQt4 module that we need. If this fails, we should 38 # Try to load the PyQt4 module that we need. If this fails, we should
34 # bail out later (in uisetup), because if we bail out now, Mercurial 39 # bail out later (in uisetup), because if we bail out now, Mercurial
37 # succeeded or not, so we need to ensure that Mercurial itself returns 42 # succeeded or not, so we need to ensure that Mercurial itself returns
38 # failure if it didn't. 43 # failure if it didn't.
39 # 44 #
40 easyhg_pyqt_ok = True 45 easyhg_pyqt_ok = True
41 try: 46 try:
42 from PyQt4 import QtGui 47 from PyQt4 import Qt, QtGui
43 except ImportError: 48 except ImportError:
44 easyhg_pyqt_ok = False 49 easyhg_pyqt_ok = False
45
46 easyhg_qtapp = None 50 easyhg_qtapp = None
51
52 # These imports are optional, we just can't use the authfile (i.e.
53 # "remember this password") feature without them
54 #
55 easyhg_authfile_imports_ok = True
56 try:
57 from Crypto.Cipher import AES
58 import ConfigParser # Mercurial version won't write files
59 import base64
60 except ImportError:
61 easyhg_authfile_imports_ok = False
62
63
64 def encrypt_salted(text, key):
65 salt = os.urandom(8)
66 text = '%d.%s.%s' % (len(text), base64.b64encode(salt), text)
67 text += (16 - len(text) % 16) * ' '
68 cipher = AES.new(key)
69 return base64.b64encode(cipher.encrypt(text))
70
71 def decrypt_salted(ctext, key):
72 cipher = AES.new(key)
73 text = cipher.decrypt(base64.b64decode(ctext))
74 (tlen, d, text) = text.partition('.')
75 (salt, d, text) = text.partition('.')
76 return text[0:int(tlen)]
77
78 # from mercurial_keyring by Marcin Kasperski
79 def canonical_url(authuri):
80 parsed_url = urlparse.urlparse(authuri)
81 return "%s://%s%s" % (parsed_url.scheme, parsed_url.netloc,
82 parsed_url.path)
83
84 def load_config(pcfg, pfile):
85 fp = None
86 try:
87 fp = open(pfile)
88 except:
89 return
90 pcfg.readfp(fp)
91 fp.close()
92
93 def save_config(pcfg, pfile):
94 ofp = None
95 try:
96 ofp = open(pfile, 'w')
97 except:
98 self.ui.write("failed to open authfile %s for writing\n" % pfile)
99 raise
100 try:
101 #!!! Windows equivalent?
102 os.fchmod(ofp.fileno(), stat.S_IRUSR | stat.S_IWUSR)
103 except:
104 ofp.close()
105 self.ui.write("failed to set permissions on authfile %s\n" % pfile)
106 raise
107 pcfg.write(ofp)
108 ofp.close()
109
110 def get_from_config(pcfg, sect, key):
111 data = None
112 try:
113 data = pcfg.get(sect, key)
114 except (ConfigParser.NoOptionError, ConfigParser.NoSectionError):
115 pass
116 return data
117
118 def get_boolean_from_config(pcfg, sect, key, deflt):
119 data = deflt
120 try:
121 data = pcfg.getboolean(sect, key)
122 except (ConfigParser.NoOptionError, ConfigParser.NoSectionError):
123 pass
124 return data
125
126 def set_to_config(pcfg, sect, key, data):
127 if not pcfg.has_section(sect):
128 pcfg.add_section(sect)
129 pcfg.set(sect, key, data)
130
131 def remote_key(uri, user):
132 # generate a "safe-for-config-file" key representing uri+user
133 # tuple (n.b. trailing = on base64 is not safe)
134 return base64.b64encode('%s@@%s' % (uri, user)).replace('=', '_')
135
47 136
48 def uisetup(ui): 137 def uisetup(ui):
49 if not easyhg_pyqt_ok: 138 if not easyhg_pyqt_ok:
50 raise util.Abort(_('Failed to load PyQt4 module required by easyhg.py')) 139 raise util.Abort(_('Failed to load PyQt4 module required by easyhg.py'))
51 ui.__class__.prompt = easyhg_prompt
52 ui.__class__.getpass = easyhg_getpass
53 global easyhg_qtapp 140 global easyhg_qtapp
54 easyhg_qtapp = QtGui.QApplication([]) 141 easyhg_qtapp = QtGui.QApplication([])
55 142
56 def easyhg_prompt(self, msg, default="y"): 143 def monkeypatch_method(cls):
57 if not self.interactive(): 144 def decorator(func):
58 self.write(msg, ' ', default, "\n") 145 setattr(cls, func.__name__, func)
59 return default 146 return func
60 isusername = False 147 return decorator
61 if msg == _('user:'): 148
62 msg = _('Username for remote repository:') 149 orig_find = passwordmgr.find_user_password
63 isusername = True 150
64 d = QtGui.QInputDialog() 151 @monkeypatch_method(passwordmgr)
65 d.setInputMode(QtGui.QInputDialog.TextInput) 152 def find_user_password(self, realm, authuri):
66 d.setTextEchoMode(QtGui.QLineEdit.Normal) 153
67 d.setLabelText(msg) 154 if not self.ui.interactive():
68 d.setWindowTitle(_('EasyMercurial: Information')) 155 return orig_find(self, realm, authuri)
69 d.show() 156 if not easyhg_pyqt_ok:
70 d.raise_() 157 return orig_find(self, realm, authuri)
71 ok = d.exec_() 158
72 r = d.textValue() 159 authinfo = urllib2.HTTPPasswordMgrWithDefaultRealm.find_user_password(
73 if not ok: 160 self, realm, authuri)
74 if isusername: 161 user, passwd = authinfo
75 raise util.Abort(_('username entry cancelled')) 162
76 else: 163 if user and passwd:
77 raise util.Abort(_('information entry cancelled')) 164 return orig_find(self, realm, authuri)
78 if not r: 165
79 return default 166 # self.ui.write("want username and/or password for %s\n" % authuri)
80 return r 167
81 168 short_uri = canonical_url(authuri)
82 def easyhg_getpass(self, prompt=None, default=None): 169
83 if not self.interactive(): 170 authkey = self.ui.config('easyhg', 'authkey')
84 return default 171 authfile = self.ui.config('easyhg', 'authfile')
85 if not prompt or prompt == _('password:'): 172 use_authfile = (easyhg_authfile_imports_ok and authkey and authfile)
86 prompt = _('Password for remote repository:'); 173 if authfile:
87 d = QtGui.QInputDialog() 174 authfile = os.path.expanduser(authfile)
88 d.setInputMode(QtGui.QInputDialog.TextInput) 175 authdata = None
89 d.setTextEchoMode(QtGui.QLineEdit.Password) 176
90 d.setLabelText(prompt) 177 dialog = QtGui.QDialog()
91 d.setWindowTitle(_('EasyMercurial: Password')) 178 layout = QtGui.QGridLayout()
92 d.show() 179 dialog.setLayout(layout)
93 d.raise_() 180
94 ok = d.exec_() 181 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)
95 r = d.textValue() 182
183 user_field = QtGui.QLineEdit()
184 if user:
185 user_field.setText(user)
186 layout.addWidget(QtGui.QLabel(_('User:')), 1, 0)
187 layout.addWidget(user_field, 1, 1)
188
189 passwd_field = QtGui.QLineEdit()
190 passwd_field.setEchoMode(QtGui.QLineEdit.Password)
191 if passwd:
192 passwd_field.setText(passwd)
193 layout.addWidget(QtGui.QLabel(_('Password:')), 2, 0)
194 layout.addWidget(passwd_field, 2, 1)
195
196 user_field.connect(user_field, Qt.SIGNAL("textChanged(QString)"),
197 passwd_field, Qt.SLOT("clear()"))
198
199 remember_field = None
200 remember = False
201 authconfig = None
202
203 if use_authfile:
204 authconfig = ConfigParser.RawConfigParser()
205 load_config(authconfig, authfile)
206 remember = get_boolean_from_config(authconfig, 'preferences',
207 'remember', False)
208 authdata = get_from_config(authconfig, 'auth',
209 remote_key(short_uri, user))
210 if authdata:
211 cachedpwd = decrypt_salted(authdata, authkey)
212 passwd_field.setText(cachedpwd)
213 remember_field = QtGui.QCheckBox()
214 remember_field.setChecked(remember)
215 remember_field.setText(_('Remember this password until EasyMercurial exits'))
216 layout.addWidget(remember_field, 3, 1)
217
218 bb = QtGui.QDialogButtonBox()
219 ok = bb.addButton(bb.Ok)
220 cancel = bb.addButton(bb.Cancel)
221 cancel.setDefault(False)
222 cancel.setAutoDefault(False)
223 ok.setDefault(True)
224 bb.connect(ok, Qt.SIGNAL("clicked()"), dialog, Qt.SLOT("accept()"))
225 bb.connect(cancel, Qt.SIGNAL("clicked()"), dialog, Qt.SLOT("reject()"))
226 layout.addWidget(bb, 4, 0, 1, 2)
227
228 dialog.setWindowTitle(_('EasyMercurial: Login'))
229 dialog.show()
230
231 if not user:
232 user_field.setFocus(True)
233 elif not passwd:
234 passwd_field.setFocus(True)
235
236 dialog.raise_()
237 ok = dialog.exec_()
96 if not ok: 238 if not ok:
97 raise util.Abort(_('password entry cancelled')) 239 raise util.Abort(_('password entry cancelled'))
98 if not r: 240
99 return default 241 user = user_field.text()
100 return r 242 passwd = passwd_field.text()
101 243
102 244 if use_authfile:
245 remember = remember_field.isChecked()
246 set_to_config(authconfig, 'preferences', 'remember', remember)
247 if user:
248 if passwd and remember:
249 authdata = encrypt_salted(passwd, authkey)
250 set_to_config(authconfig, 'auth', remote_key(short_uri, user), authdata)
251 else:
252 set_to_config(authconfig, 'auth', remote_key(short_uri, user), '')
253 save_config(authconfig, authfile)
254
255 self.add_password(realm, authuri, user, passwd)
256 return (user, passwd)
257
258