Mercurial > hg > easyhg
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 |