annotate easyhg.py @ 600:641ccce7c771

Avoid messing with font size when zooming, let it zoom naturally; don't delete detail item when removing it, just let it wait to be shown again (and do delete it when deleting main item)
author Chris Cannam
date Fri, 11 May 2012 17:44:33 +0100
parents 533519ebc0cb
children 92929d26b8db
rev   line source
Chris@427 1 # -*- coding: utf-8 -*-
Chris@427 2 #
Chris@427 3 # EasyMercurial
Chris@427 4 #
Chris@427 5 # Based on hgExplorer by Jari Korhonen
Chris@427 6 # Copyright (c) 2010 Jari Korhonen
Chris@560 7 # Copyright (c) 2010-2012 Chris Cannam
Chris@560 8 # Copyright (c) 2010-2012 Queen Mary, University of London
Chris@427 9 #
Chris@427 10 # This program is free software; you can redistribute it and/or
Chris@427 11 # modify it under the terms of the GNU General Public License as
Chris@427 12 # published by the Free Software Foundation; either version 2 of the
Chris@427 13 # License, or (at your option) any later version. See the file
Chris@427 14 # COPYING included with this distribution for more information.
Chris@427 15
Chris@503 16 import sys, os, stat, urllib, urllib2, urlparse, hashlib
Chris@427 17
Chris@438 18 from mercurial.i18n import _
Chris@433 19 from mercurial import ui, util, error
Chris@427 20 try:
Chris@427 21 from mercurial.url import passwordmgr
Chris@427 22 except:
Chris@427 23 from mercurial.httprepo import passwordmgr
Chris@427 24
Chris@427 25 # The value assigned here may be modified during installation, by
Chris@427 26 # replacing its default value with another one. We can't compare
Chris@427 27 # against its default value, because then the comparison text would
Chris@427 28 # get modified as well. So, compare using prefix only.
Chris@427 29 #
Chris@427 30 easyhg_import_path = 'NO_EASYHG_IMPORT_PATH'
Chris@427 31 if not easyhg_import_path.startswith('NO_'):
Chris@427 32 # We have an installation path: append it twice, once with
Chris@427 33 # the Python version suffixed
Chris@440 34 version_suffix = 'Py%d.%d' % (sys.version_info[0], sys.version_info[1])
Chris@427 35 sys.path.append(easyhg_import_path + "/" + version_suffix)
Chris@427 36 sys.path.append(easyhg_import_path)
Chris@427 37
Chris@427 38 # Try to load the PyQt4 module that we need. If this fails, we should
Chris@427 39 # bail out later (in uisetup), because if we bail out now, Mercurial
Chris@427 40 # will just continue without us and report success. The invoking
Chris@427 41 # application needs to be able to discover whether the module load
Chris@427 42 # succeeded or not, so we need to ensure that Mercurial itself returns
Chris@427 43 # failure if it didn't.
Chris@427 44 #
Chris@427 45 easyhg_pyqt_ok = True
Chris@427 46 try:
Chris@457 47 from PyQt4 import Qt, QtCore, QtGui
Chris@427 48 except ImportError:
Chris@427 49 easyhg_pyqt_ok = False
Chris@427 50 easyhg_qtapp = None
Chris@427 51
Chris@440 52 # These imports are optional, we just can't use the authfile (i.e.
Chris@438 53 # "remember this password") feature without them
Chris@438 54 #
Chris@437 55 easyhg_authfile_imports_ok = True
Chris@437 56 try:
Chris@437 57 from Crypto.Cipher import AES
Chris@437 58 import ConfigParser # Mercurial version won't write files
Chris@437 59 import base64
Chris@437 60 except ImportError:
Chris@445 61 print "EasyHg: Failed to import required modules for authfile support"
Chris@437 62 easyhg_authfile_imports_ok = False
Chris@433 63
Chris@431 64
Chris@452 65 class EasyHgAuthStore(object):
Chris@431 66
Chris@452 67 def __init__(self, ui, url, user, passwd):
Chris@452 68
Chris@452 69 self.ui = ui
Chris@452 70 self.remote_url = url
Chris@452 71
Chris@452 72 self.user = user
Chris@452 73 self.passwd = passwd
Chris@452 74
Chris@452 75 self.auth_key = self.ui.config('easyhg', 'authkey')
Chris@452 76 self.auth_file = self.ui.config('easyhg', 'authfile')
Chris@452 77
Chris@452 78 self.use_auth_file = (easyhg_authfile_imports_ok and
Chris@452 79 self.auth_key and self.auth_file)
Chris@452 80
Chris@458 81 self.auth_config = None
Chris@458 82 self.auth_cipher = None
Chris@458 83 self.remember = False
Chris@458 84
Chris@452 85 if self.use_auth_file:
Chris@452 86 self.auth_cipher = AES.new(self.auth_key, AES.MODE_CBC)
Chris@452 87 self.auth_file = os.path.expanduser(self.auth_file)
Chris@452 88 self.load_auth_data()
Chris@452 89
Chris@452 90 def save(self):
Chris@452 91 if self.use_auth_file:
Chris@452 92 self.save_auth_data()
Chris@452 93
Chris@452 94 def encrypt(self, text):
Chris@452 95 iv = os.urandom(12)
Chris@452 96 text = '%s.%d.%s.easyhg' % (base64.b64encode(iv), len(text), text)
Chris@452 97 text += (16 - (len(text) % 16)) * ' '
Chris@452 98 ctext = base64.b64encode(self.auth_cipher.encrypt(text))
Chris@452 99 return ctext
Chris@452 100
Chris@452 101 def decrypt(self, ctext):
Chris@448 102 try:
Chris@456 103 text = self.auth_cipher.decrypt(base64.b64decode(ctext))
Chris@456 104 (iv, d, text) = text.partition('.')
Chris@456 105 (tlen, d, text) = text.partition('.')
Chris@452 106 return text[0:int(tlen)]
Chris@448 107 except:
Chris@452 108 self.ui.write("failed to decrypt/convert cached data!")
Chris@452 109 return ''
Chris@452 110
Chris@452 111 def argless_url(self):
Chris@452 112 parsed = urlparse.urlparse(self.remote_url)
Chris@452 113 return "%s://%s%s" % (parsed.scheme, parsed.netloc, parsed.path)
Chris@452 114
Chris@452 115 def pathless_url(self):
Chris@452 116 parsed = urlparse.urlparse(self.remote_url)
Chris@452 117 return "%s://%s" % (parsed.scheme, parsed.netloc)
Chris@452 118
Chris@452 119 def load_config(self):
Chris@453 120 if not self.auth_config:
Chris@453 121 self.auth_config = ConfigParser.RawConfigParser()
Chris@452 122 fp = None
Chris@452 123 try:
Chris@452 124 fp = open(self.auth_file)
Chris@452 125 except:
Chris@452 126 self.ui.write("unable to read authfile %s, ignoring\n" % self.auth_file)
Chris@452 127 return
Chris@452 128 self.auth_config.readfp(fp)
Chris@452 129 fp.close()
Chris@452 130
Chris@452 131 def save_config(self):
Chris@452 132 ofp = None
Chris@452 133 try:
Chris@452 134 ofp = open(self.auth_file, 'w')
Chris@452 135 except:
Chris@452 136 self.ui.write("failed to open authfile %s for writing\n" % self.auth_file)
Chris@448 137 raise
Chris@503 138 if os.name == 'posix':
Chris@452 139 try:
Chris@452 140 os.fchmod(ofp.fileno(), stat.S_IRUSR | stat.S_IWUSR)
Chris@452 141 except:
Chris@452 142 ofp.close()
Chris@452 143 self.ui.write("failed to set permissions on authfile %s\n" % self.auth_file)
Chris@452 144 raise
Chris@452 145 self.auth_config.write(ofp)
Chris@452 146 ofp.close()
Chris@434 147
Chris@452 148 def get_from_config(self, sect, key):
Chris@452 149 data = None
Chris@452 150 try:
Chris@452 151 data = self.auth_config.get(sect, key)
Chris@452 152 except (ConfigParser.NoOptionError, ConfigParser.NoSectionError):
Chris@452 153 pass
Chris@452 154 return data
Chris@436 155
Chris@452 156 def get_boolean_from_config(self, sect, key, deflt):
Chris@452 157 data = deflt
Chris@452 158 try:
Chris@452 159 data = self.auth_config.getboolean(sect, key)
Chris@452 160 except (ConfigParser.NoOptionError, ConfigParser.NoSectionError):
Chris@452 161 pass
Chris@452 162 return data
Chris@436 163
Chris@452 164 def set_to_config(self, sect, key, data):
Chris@452 165 if not self.auth_config.has_section(sect):
Chris@452 166 self.auth_config.add_section(sect)
Chris@452 167 self.auth_config.set(sect, key, data)
Chris@436 168
Chris@452 169 def remote_key(self, url, user):
Chris@452 170 # generate a "safe-for-config-file" key representing uri+user
Chris@459 171 # self.ui.write('generating remote_key for url %s and user %s\n' % (url, user))
Chris@452 172 s = '%s@@%s' % (url, user)
Chris@452 173 h = hashlib.sha1()
Chris@452 174 h.update(self.auth_key)
Chris@452 175 h.update(s)
Chris@452 176 hx = h.hexdigest()
Chris@452 177 return hx
Chris@452 178
Chris@452 179 def remote_user_key(self):
Chris@453 180 return self.remote_key(self.pathless_url(), '')
Chris@452 181
Chris@452 182 def remote_passwd_key(self):
Chris@452 183 return self.remote_key(self.pathless_url(), self.user)
Chris@452 184
Chris@452 185 def load_auth_data(self):
Chris@452 186
Chris@452 187 self.load_config()
Chris@452 188 if not self.auth_config: return
Chris@452 189
Chris@452 190 self.remember = self.get_boolean_from_config(
Chris@452 191 'preferences', 'remember', False)
Chris@452 192
Chris@452 193 if not self.user:
Chris@452 194 d = self.get_from_config('user', self.remote_user_key())
Chris@452 195 if d:
Chris@452 196 self.user = self.decrypt(d)
Chris@452 197
Chris@452 198 if self.user:
Chris@452 199 d = self.get_from_config('auth', self.remote_passwd_key())
Chris@452 200 if d:
Chris@452 201 self.passwd = self.decrypt(d)
Chris@452 202
Chris@452 203 def save_auth_data(self):
Chris@452 204
Chris@453 205 self.load_config()
Chris@452 206 if not self.auth_config: return
Chris@453 207
Chris@452 208 self.set_to_config('preferences', 'remember', self.remember)
Chris@452 209
Chris@459 210 # self.ui.write('aiming to store details for user %s\n' % self.user)
Chris@452 211
Chris@452 212 if self.remember and self.user:
Chris@452 213 d = self.encrypt(self.user)
Chris@452 214 self.set_to_config('user', self.remote_user_key(), d)
Chris@452 215 else:
Chris@452 216 self.set_to_config('user', self.remote_user_key(), '')
Chris@452 217
Chris@452 218 if self.remember and self.user and self.passwd:
Chris@452 219 d = self.encrypt(self.passwd)
Chris@452 220 self.set_to_config('auth', self.remote_passwd_key(), d)
Chris@452 221 elif self.user:
Chris@452 222 self.set_to_config('auth', self.remote_passwd_key(), '')
Chris@452 223
Chris@452 224 self.save_config()
Chris@452 225
Chris@452 226 class EasyHgAuthDialog(object):
Chris@452 227
Chris@452 228 auth_store = None
Chris@452 229
Chris@452 230 def __init__(self, ui, url, user, passwd):
Chris@452 231 self.auth_store = EasyHgAuthStore(ui, url, user, passwd)
Chris@452 232
Chris@470 233 def ask(self, repeat):
Chris@459 234
Chris@460 235 if self.auth_store.user and self.auth_store.passwd and self.auth_store.remember:
Chris@470 236 if not repeat:
Chris@459 237 return (self.auth_store.user, self.auth_store.passwd)
Chris@459 238
Chris@452 239 dialog = QtGui.QDialog()
Chris@452 240 layout = QtGui.QGridLayout()
Chris@452 241 dialog.setLayout(layout)
Chris@452 242
Chris@470 243 heading = _('Login required')
Chris@470 244 if repeat:
Chris@470 245 heading = _('Login failed: please try again')
Chris@470 246 label_text = _(('<h3>%s</h3><p>Please provide your login details for the repository at<br><code>%s</code>:') % (heading, self.auth_store.argless_url()))
Chris@470 247 layout.addWidget(QtGui.QLabel(label_text), 0, 0, 1, 2)
Chris@452 248
Chris@452 249 user_field = QtGui.QLineEdit()
Chris@452 250 if self.auth_store.user: user_field.setText(self.auth_store.user)
Chris@452 251 layout.addWidget(QtGui.QLabel(_('User:')), 1, 0)
Chris@452 252 layout.addWidget(user_field, 1, 1)
Chris@452 253
Chris@452 254 passwd_field = QtGui.QLineEdit()
Chris@452 255 passwd_field.setEchoMode(QtGui.QLineEdit.Password)
Chris@452 256 if self.auth_store.passwd: passwd_field.setText(self.auth_store.passwd)
Chris@452 257 layout.addWidget(QtGui.QLabel(_('Password:')), 2, 0)
Chris@452 258 layout.addWidget(passwd_field, 2, 1)
Chris@452 259
Chris@452 260 user_field.connect(user_field, Qt.SIGNAL("textChanged(QString)"),
Chris@452 261 passwd_field, Qt.SLOT("clear()"))
Chris@452 262
Chris@452 263 remember_field = None
Chris@452 264 if self.auth_store.use_auth_file:
Chris@452 265 remember_field = QtGui.QCheckBox()
Chris@452 266 remember_field.setChecked(self.auth_store.remember)
Chris@452 267 remember_field.setText(_('Remember these details while EasyMercurial is running'))
Chris@452 268 layout.addWidget(remember_field, 3, 1)
Chris@457 269 warning_field = QtGui.QLabel()
Chris@461 270 warning_field.setText(_('<qt><i><small>Do not use this option if anyone else has access to your computer!</small></i><br></qt>'))
Chris@457 271 warning_field.hide()
Chris@457 272 remember_field.connect(remember_field, Qt.SIGNAL("clicked()"),
Chris@457 273 warning_field, Qt.SLOT("show()"))
Chris@457 274 layout.addWidget(warning_field, 4, 1, QtCore.Qt.AlignRight)
Chris@452 275
Chris@452 276 bb = QtGui.QDialogButtonBox()
Chris@452 277 ok = bb.addButton(bb.Ok)
Chris@452 278 cancel = bb.addButton(bb.Cancel)
Chris@452 279 cancel.setDefault(False)
Chris@452 280 cancel.setAutoDefault(False)
Chris@452 281 ok.setDefault(True)
Chris@452 282 bb.connect(ok, Qt.SIGNAL("clicked()"), dialog, Qt.SLOT("accept()"))
Chris@452 283 bb.connect(cancel, Qt.SIGNAL("clicked()"), dialog, Qt.SLOT("reject()"))
Chris@457 284 layout.addWidget(bb, 5, 0, 1, 2)
Chris@452 285
Chris@452 286 dialog.setWindowTitle(_('EasyMercurial: Login'))
Chris@452 287 dialog.show()
Chris@452 288
Chris@452 289 if not self.auth_store.user:
Chris@452 290 user_field.setFocus(True)
Chris@452 291 elif not self.auth_store.passwd:
Chris@452 292 passwd_field.setFocus(True)
Chris@452 293 else:
Chris@452 294 ok.setFocus(True)
Chris@452 295
Chris@452 296 dialog.raise_()
Chris@452 297 ok = dialog.exec_()
Chris@452 298 if not ok:
Chris@452 299 raise util.Abort(_('password entry cancelled'))
Chris@452 300
Chris@452 301 self.auth_store.user = user_field.text()
Chris@452 302 self.auth_store.passwd = passwd_field.text()
Chris@452 303
Chris@452 304 if remember_field:
Chris@452 305 self.auth_store.remember = remember_field.isChecked()
Chris@452 306
Chris@452 307 self.auth_store.save()
Chris@452 308
Chris@452 309 return (self.auth_store.user, self.auth_store.passwd)
Chris@438 310
Chris@440 311
Chris@440 312 def uisetup(ui):
Chris@440 313 if not easyhg_pyqt_ok:
Chris@440 314 raise util.Abort(_('Failed to load PyQt4 module required by easyhg.py'))
Chris@440 315 global easyhg_qtapp
Chris@440 316 easyhg_qtapp = QtGui.QApplication([])
Chris@440 317
Chris@440 318 def monkeypatch_method(cls):
Chris@440 319 def decorator(func):
Chris@440 320 setattr(cls, func.__name__, func)
Chris@440 321 return func
Chris@440 322 return decorator
Chris@440 323
Chris@440 324 orig_find = passwordmgr.find_user_password
Chris@440 325
Chris@427 326 @monkeypatch_method(passwordmgr)
Chris@427 327 def find_user_password(self, realm, authuri):
Chris@427 328
Chris@459 329 if not hasattr(self, '__easyhg_last'):
Chris@459 330 self.__easyhg_last = None
Chris@459 331
Chris@427 332 if not self.ui.interactive():
Chris@427 333 return orig_find(self, realm, authuri)
Chris@427 334 if not easyhg_pyqt_ok:
Chris@427 335 return orig_find(self, realm, authuri)
Chris@427 336
Chris@427 337 authinfo = urllib2.HTTPPasswordMgrWithDefaultRealm.find_user_password(
Chris@427 338 self, realm, authuri)
Chris@427 339 user, passwd = authinfo
Chris@427 340
Chris@470 341 repeat = False
Chris@459 342
Chris@459 343 if (realm, authuri) == self.__easyhg_last:
Chris@459 344 # If we are called again just after identical previous
Chris@459 345 # request, then the previously returned auth must have been
Chris@459 346 # wrong. So we note this to force password prompt (and avoid
Chris@459 347 # reusing bad password indefinitely). Thanks to
Chris@459 348 # mercurial_keyring (Marcin Kasperski) for this logic
Chris@470 349 repeat = True
Chris@470 350
Chris@470 351 if user and passwd and not repeat:
Chris@470 352 return orig_find(self, realm, authuri)
Chris@427 353
Chris@452 354 dialog = EasyHgAuthDialog(self.ui, authuri, user, passwd)
Chris@427 355
Chris@470 356 (user, passwd) = dialog.ask(repeat)
Chris@433 357
Chris@470 358 self.add_password(realm, authuri, user, passwd)
Chris@459 359 self.__easyhg_last = (realm, authuri)
Chris@427 360 return (user, passwd)
Chris@427 361
Chris@427 362