##// END OF EJS Templates
hg: reimplement branch listings more efficiently...
Mads Kiilerich -
r4728:1cd9bdf1 default
parent child Browse files
Show More
@@ -1,624 +1,606 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 vcs.backends.hg.repository
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Mercurial repository implementation.
7 7
8 8 :created_on: Apr 8, 2010
9 9 :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
10 10 """
11 11
12 12 import os
13 13 import time
14 14 import urllib
15 15 import urllib2
16 16 import logging
17 17 import datetime
18 18
19 19 from kallithea.lib.vcs.backends.base import BaseRepository, CollectionGenerator
20 20
21 21 from kallithea.lib.vcs.exceptions import (
22 22 BranchDoesNotExistError, ChangesetDoesNotExistError, EmptyRepositoryError,
23 23 RepositoryError, VCSError, TagAlreadyExistError, TagDoesNotExistError
24 24 )
25 25 from kallithea.lib.vcs.utils import (
26 26 author_email, author_name, date_fromtimestamp, makedate, safe_unicode, safe_str,
27 27 )
28 28 from kallithea.lib.vcs.utils.lazy import LazyProperty
29 29 from kallithea.lib.vcs.utils.ordered_dict import OrderedDict
30 30 from kallithea.lib.vcs.utils.paths import abspath
31 31 from kallithea.lib.vcs.utils.hgcompat import (
32 32 ui, nullid, match, patch, diffopts, clone, get_contact, pull,
33 33 localrepository, RepoLookupError, Abort, RepoError, hex, scmutil, hg_url,
34 34 httpbasicauthhandler, httpdigestauthhandler, peer, httppeer
35 35 )
36 36
37 37 from .changeset import MercurialChangeset
38 38 from .inmemory import MercurialInMemoryChangeset
39 39 from .workdir import MercurialWorkdir
40 40
41 41 log = logging.getLogger(__name__)
42 42
43 43
44 44 class MercurialRepository(BaseRepository):
45 45 """
46 46 Mercurial repository backend
47 47 """
48 48 DEFAULT_BRANCH_NAME = 'default'
49 49 scm = 'hg'
50 50
51 51 def __init__(self, repo_path, create=False, baseui=None, src_url=None,
52 52 update_after_clone=False):
53 53 """
54 54 Raises RepositoryError if repository could not be find at the given
55 55 ``repo_path``.
56 56
57 57 :param repo_path: local path of the repository
58 58 :param create=False: if set to True, would try to create repository if
59 59 it does not exist rather than raising exception
60 60 :param baseui=None: user data
61 61 :param src_url=None: would try to clone repository from given location
62 62 :param update_after_clone=False: sets update of working copy after
63 63 making a clone
64 64 """
65 65
66 66 if not isinstance(repo_path, str):
67 67 raise VCSError('Mercurial backend requires repository path to '
68 68 'be instance of <str> got %s instead' %
69 69 type(repo_path))
70 70
71 71 self.path = abspath(repo_path)
72 72 self.baseui = baseui or ui.ui()
73 73 # We've set path and ui, now we can set _repo itself
74 74 self._repo = self._get_repo(create, src_url, update_after_clone)
75 75
76 76 @property
77 77 def _empty(self):
78 78 """
79 79 Checks if repository is empty ie. without any changesets
80 80 """
81 81 # TODO: Following raises errors when using InMemoryChangeset...
82 82 # return len(self._repo.changelog) == 0
83 83 return len(self.revisions) == 0
84 84
85 85 @LazyProperty
86 86 def revisions(self):
87 87 """
88 88 Returns list of revisions' ids, in ascending order. Being lazy
89 89 attribute allows external tools to inject shas from cache.
90 90 """
91 91 return self._get_all_revisions()
92 92
93 93 @LazyProperty
94 94 def name(self):
95 95 return os.path.basename(self.path)
96 96
97 97 @LazyProperty
98 98 def branches(self):
99 99 return self._get_branches()
100 100
101 101 @LazyProperty
102 102 def closed_branches(self):
103 103 return self._get_branches(normal=False, closed=True)
104 104
105 105 @LazyProperty
106 106 def allbranches(self):
107 107 """
108 108 List all branches, including closed branches.
109 109 """
110 110 return self._get_branches(closed=True)
111 111
112 112 def _get_branches(self, normal=True, closed=False):
113 113 """
114 114 Gets branches for this repository
115 115 Returns only not closed branches by default
116 116
117 117 :param closed: return also closed branches for mercurial
118 118 :param normal: return also normal branches
119 119 """
120 120
121 121 if self._empty:
122 122 return {}
123 123
124 def _branchtags(localrepo):
125 """
126 Patched version of mercurial branchtags to not return the closed
127 branches
128
129 :param localrepo: locarepository instance
130 """
124 bt = OrderedDict()
125 for bn, _heads, tip, isclosed in sorted(self._repo.branchmap().iterbranches()):
126 if isclosed:
127 if closed:
128 bt[safe_unicode(bn)] = hex(tip)
129 else:
130 if normal:
131 bt[safe_unicode(bn)] = hex(tip)
131 132
132 bt = {}
133 bt_closed = {}
134 for bn, heads in localrepo.branchmap().iteritems():
135 tip = heads[-1]
136 if 'close' in localrepo.changelog.read(tip)[5]:
137 bt_closed[bn] = tip
138 else:
139 bt[bn] = tip
140
141 if not normal:
142 return bt_closed
143 if closed:
144 bt.update(bt_closed)
145 return bt
146
147 sortkey = lambda ctx: ctx[0] # sort by name
148 _branches = [(safe_unicode(n), hex(h),) for n, h in
149 _branchtags(self._repo).items()]
150
151 return OrderedDict(sorted(_branches, key=sortkey, reverse=False))
133 return bt
152 134
153 135 @LazyProperty
154 136 def tags(self):
155 137 """
156 138 Gets tags for this repository
157 139 """
158 140 return self._get_tags()
159 141
160 142 def _get_tags(self):
161 143 if self._empty:
162 144 return {}
163 145
164 146 sortkey = lambda ctx: ctx[0] # sort by name
165 147 _tags = [(safe_unicode(n), hex(h),) for n, h in
166 148 self._repo.tags().items()]
167 149
168 150 return OrderedDict(sorted(_tags, key=sortkey, reverse=True))
169 151
170 152 def tag(self, name, user, revision=None, message=None, date=None,
171 153 **kwargs):
172 154 """
173 155 Creates and returns a tag for the given ``revision``.
174 156
175 157 :param name: name for new tag
176 158 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
177 159 :param revision: changeset id for which new tag would be created
178 160 :param message: message of the tag's commit
179 161 :param date: date of tag's commit
180 162
181 163 :raises TagAlreadyExistError: if tag with same name already exists
182 164 """
183 165 if name in self.tags:
184 166 raise TagAlreadyExistError("Tag %s already exists" % name)
185 167 changeset = self.get_changeset(revision)
186 168 local = kwargs.setdefault('local', False)
187 169
188 170 if message is None:
189 171 message = "Added tag %s for changeset %s" % (name,
190 172 changeset.short_id)
191 173
192 174 if date is None:
193 175 date = datetime.datetime.now().ctime()
194 176
195 177 try:
196 178 self._repo.tag(name, changeset._ctx.node(), message, local, user,
197 179 date)
198 180 except Abort, e:
199 181 raise RepositoryError(e.message)
200 182
201 183 # Reinitialize tags
202 184 self.tags = self._get_tags()
203 185 tag_id = self.tags[name]
204 186
205 187 return self.get_changeset(revision=tag_id)
206 188
207 189 def remove_tag(self, name, user, message=None, date=None):
208 190 """
209 191 Removes tag with the given ``name``.
210 192
211 193 :param name: name of the tag to be removed
212 194 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
213 195 :param message: message of the tag's removal commit
214 196 :param date: date of tag's removal commit
215 197
216 198 :raises TagDoesNotExistError: if tag with given name does not exists
217 199 """
218 200 if name not in self.tags:
219 201 raise TagDoesNotExistError("Tag %s does not exist" % name)
220 202 if message is None:
221 203 message = "Removed tag %s" % name
222 204 if date is None:
223 205 date = datetime.datetime.now().ctime()
224 206 local = False
225 207
226 208 try:
227 209 self._repo.tag(name, nullid, message, local, user, date)
228 210 self.tags = self._get_tags()
229 211 except Abort, e:
230 212 raise RepositoryError(e.message)
231 213
232 214 @LazyProperty
233 215 def bookmarks(self):
234 216 """
235 217 Gets bookmarks for this repository
236 218 """
237 219 return self._get_bookmarks()
238 220
239 221 def _get_bookmarks(self):
240 222 if self._empty:
241 223 return {}
242 224
243 225 sortkey = lambda ctx: ctx[0] # sort by name
244 226 _bookmarks = [(safe_unicode(n), hex(h),) for n, h in
245 227 self._repo._bookmarks.items()]
246 228 return OrderedDict(sorted(_bookmarks, key=sortkey, reverse=True))
247 229
248 230 def _get_all_revisions(self):
249 231
250 232 return map(lambda x: hex(x[7]), self._repo.changelog.index)[:-1]
251 233
252 234 def get_diff(self, rev1, rev2, path='', ignore_whitespace=False,
253 235 context=3):
254 236 """
255 237 Returns (git like) *diff*, as plain text. Shows changes introduced by
256 238 ``rev2`` since ``rev1``.
257 239
258 240 :param rev1: Entry point from which diff is shown. Can be
259 241 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
260 242 the changes since empty state of the repository until ``rev2``
261 243 :param rev2: Until which revision changes should be shown.
262 244 :param ignore_whitespace: If set to ``True``, would not show whitespace
263 245 changes. Defaults to ``False``.
264 246 :param context: How many lines before/after changed lines should be
265 247 shown. Defaults to ``3``.
266 248 """
267 249 if hasattr(rev1, 'raw_id'):
268 250 rev1 = getattr(rev1, 'raw_id')
269 251
270 252 if hasattr(rev2, 'raw_id'):
271 253 rev2 = getattr(rev2, 'raw_id')
272 254
273 255 # Check if given revisions are present at repository (may raise
274 256 # ChangesetDoesNotExistError)
275 257 if rev1 != self.EMPTY_CHANGESET:
276 258 self.get_changeset(rev1)
277 259 self.get_changeset(rev2)
278 260 if path:
279 261 file_filter = match(self.path, '', [path])
280 262 else:
281 263 file_filter = None
282 264
283 265 return ''.join(patch.diff(self._repo, rev1, rev2, match=file_filter,
284 266 opts=diffopts(git=True,
285 267 ignorews=ignore_whitespace,
286 268 context=context)))
287 269
288 270 @classmethod
289 271 def _check_url(cls, url, repoui=None):
290 272 """
291 273 Function will check given url and try to verify if it's a valid
292 274 link. Sometimes it may happened that mercurial will issue basic
293 275 auth request that can cause whole API to hang when used from python
294 276 or other external calls.
295 277
296 278 On failures it'll raise urllib2.HTTPError, exception is also thrown
297 279 when the return code is non 200
298 280 """
299 281 # check first if it's not an local url
300 282 if os.path.isdir(url) or url.startswith('file:'):
301 283 return True
302 284
303 285 if '+' in url[:url.find('://')]:
304 286 url = url[url.find('+') + 1:]
305 287
306 288 handlers = []
307 289 url_obj = hg_url(url)
308 290 test_uri, authinfo = url_obj.authinfo()
309 291 url_obj.passwd = '*****'
310 292 cleaned_uri = str(url_obj)
311 293
312 294 if authinfo:
313 295 #create a password manager
314 296 passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
315 297 passmgr.add_password(*authinfo)
316 298
317 299 handlers.extend((httpbasicauthhandler(passmgr),
318 300 httpdigestauthhandler(passmgr)))
319 301
320 302 o = urllib2.build_opener(*handlers)
321 303 o.addheaders = [('Content-Type', 'application/mercurial-0.1'),
322 304 ('Accept', 'application/mercurial-0.1')]
323 305
324 306 q = {"cmd": 'between'}
325 307 q.update({'pairs': "%s-%s" % ('0' * 40, '0' * 40)})
326 308 qs = '?%s' % urllib.urlencode(q)
327 309 cu = "%s%s" % (test_uri, qs)
328 310 req = urllib2.Request(cu, None, {})
329 311
330 312 try:
331 313 resp = o.open(req)
332 314 if resp.code != 200:
333 315 raise Exception('Return Code is not 200')
334 316 except Exception, e:
335 317 # means it cannot be cloned
336 318 raise urllib2.URLError("[%s] org_exc: %s" % (cleaned_uri, e))
337 319
338 320 # now check if it's a proper hg repo
339 321 try:
340 322 httppeer(repoui or ui.ui(), url).lookup('tip')
341 323 except Exception, e:
342 324 raise urllib2.URLError(
343 325 "url [%s] does not look like an hg repo org_exc: %s"
344 326 % (cleaned_uri, e))
345 327
346 328 return True
347 329
348 330 def _get_repo(self, create, src_url=None, update_after_clone=False):
349 331 """
350 332 Function will check for mercurial repository in given path and return
351 333 a localrepo object. If there is no repository in that path it will
352 334 raise an exception unless ``create`` parameter is set to True - in
353 335 that case repository would be created and returned.
354 336 If ``src_url`` is given, would try to clone repository from the
355 337 location at given clone_point. Additionally it'll make update to
356 338 working copy accordingly to ``update_after_clone`` flag
357 339 """
358 340
359 341 try:
360 342 if src_url:
361 343 url = str(self._get_url(src_url))
362 344 opts = {}
363 345 if not update_after_clone:
364 346 opts.update({'noupdate': True})
365 347 try:
366 348 MercurialRepository._check_url(url, self.baseui)
367 349 clone(self.baseui, url, self.path, **opts)
368 350 # except urllib2.URLError:
369 351 # raise Abort("Got HTTP 404 error")
370 352 except Exception:
371 353 raise
372 354
373 355 # Don't try to create if we've already cloned repo
374 356 create = False
375 357 return localrepository(self.baseui, self.path, create=create)
376 358 except (Abort, RepoError), err:
377 359 if create:
378 360 msg = "Cannot create repository at %s. Original error was %s"\
379 361 % (self.path, err)
380 362 else:
381 363 msg = "Not valid repository at %s. Original error was %s"\
382 364 % (self.path, err)
383 365 raise RepositoryError(msg)
384 366
385 367 @LazyProperty
386 368 def in_memory_changeset(self):
387 369 return MercurialInMemoryChangeset(self)
388 370
389 371 @LazyProperty
390 372 def description(self):
391 373 undefined_description = u'unknown'
392 374 _desc = self._repo.ui.config('web', 'description', None, untrusted=True)
393 375 return safe_unicode(_desc or undefined_description)
394 376
395 377 @LazyProperty
396 378 def contact(self):
397 379 undefined_contact = u'Unknown'
398 380 return safe_unicode(get_contact(self._repo.ui.config)
399 381 or undefined_contact)
400 382
401 383 @LazyProperty
402 384 def last_change(self):
403 385 """
404 386 Returns last change made on this repository as datetime object
405 387 """
406 388 return date_fromtimestamp(self._get_mtime(), makedate()[1])
407 389
408 390 def _get_mtime(self):
409 391 try:
410 392 return time.mktime(self.get_changeset().date.timetuple())
411 393 except RepositoryError:
412 394 #fallback to filesystem
413 395 cl_path = os.path.join(self.path, '.hg', "00changelog.i")
414 396 st_path = os.path.join(self.path, '.hg', "store")
415 397 if os.path.exists(cl_path):
416 398 return os.stat(cl_path).st_mtime
417 399 else:
418 400 return os.stat(st_path).st_mtime
419 401
420 402 def _get_revision(self, revision):
421 403 """
422 404 Gets an ID revision given as str. This will always return a fill
423 405 40 char revision number
424 406
425 407 :param revision: str or int or None
426 408 """
427 409 if isinstance(revision, unicode):
428 410 revision = safe_str(revision)
429 411
430 412 if self._empty:
431 413 raise EmptyRepositoryError("There are no changesets yet")
432 414
433 415 if revision in [-1, 'tip', None]:
434 416 revision = 'tip'
435 417
436 418 try:
437 419 revision = hex(self._repo.lookup(revision))
438 420 except (LookupError, ):
439 421 msg = ("Ambiguous identifier `%s` for %s" % (revision, self))
440 422 raise ChangesetDoesNotExistError(msg)
441 423 except (IndexError, ValueError, RepoLookupError, TypeError):
442 424 msg = ("Revision %s does not exist for %s" % (revision, self))
443 425 raise ChangesetDoesNotExistError(msg)
444 426
445 427 return revision
446 428
447 429 def get_ref_revision(self, ref_type, ref_name):
448 430 """
449 431 Returns revision number for the given reference.
450 432 """
451 433 ref_name = safe_str(ref_name)
452 434 if ref_type == 'rev' and not ref_name.strip('0'):
453 435 return self.EMPTY_CHANGESET
454 436 # lookup up the exact node id
455 437 _revset_predicates = {
456 438 'branch': 'branch',
457 439 'book': 'bookmark',
458 440 'tag': 'tag',
459 441 'rev': 'id',
460 442 }
461 443 # avoid expensive branch(x) iteration over whole repo
462 444 rev_spec = "%%s & %s(%%s)" % _revset_predicates[ref_type]
463 445 try:
464 446 revs = self._repo.revs(rev_spec, ref_name, ref_name)
465 447 except LookupError:
466 448 msg = ("Ambiguous identifier %s:%s for %s" % (ref_type, ref_name, self.name))
467 449 raise ChangesetDoesNotExistError(msg)
468 450 except RepoLookupError:
469 451 msg = ("Revision %s:%s does not exist for %s" % (ref_type, ref_name, self.name))
470 452 raise ChangesetDoesNotExistError(msg)
471 453 if revs:
472 454 revision = revs[-1]
473 455 else:
474 456 # TODO: just report 'not found'?
475 457 revision = ref_name
476 458
477 459 return self._get_revision(revision)
478 460
479 461 def _get_archives(self, archive_name='tip'):
480 462 allowed = self.baseui.configlist("web", "allow_archive",
481 463 untrusted=True)
482 464 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
483 465 if i[0] in allowed or self._repo.ui.configbool("web",
484 466 "allow" + i[0],
485 467 untrusted=True):
486 468 yield {"type": i[0], "extension": i[1], "node": archive_name}
487 469
488 470 def _get_url(self, url):
489 471 """
490 472 Returns normalized url. If schema is not given, would fall
491 473 to filesystem
492 474 (``file:///``) schema.
493 475 """
494 476 url = str(url)
495 477 if url != 'default' and not '://' in url:
496 478 url = "file:" + urllib.pathname2url(url)
497 479 return url
498 480
499 481 def get_hook_location(self):
500 482 """
501 483 returns absolute path to location where hooks are stored
502 484 """
503 485 return os.path.join(self.path, '.hg', '.hgrc')
504 486
505 487 def get_changeset(self, revision=None):
506 488 """
507 489 Returns ``MercurialChangeset`` object representing repository's
508 490 changeset at the given ``revision``.
509 491 """
510 492 revision = self._get_revision(revision)
511 493 changeset = MercurialChangeset(repository=self, revision=revision)
512 494 return changeset
513 495
514 496 def get_changesets(self, start=None, end=None, start_date=None,
515 497 end_date=None, branch_name=None, reverse=False):
516 498 """
517 499 Returns iterator of ``MercurialChangeset`` objects from start to end
518 500 (both are inclusive)
519 501
520 502 :param start: None, str, int or mercurial lookup format
521 503 :param end: None, str, int or mercurial lookup format
522 504 :param start_date:
523 505 :param end_date:
524 506 :param branch_name:
525 507 :param reversed: return changesets in reversed order
526 508 """
527 509
528 510 start_raw_id = self._get_revision(start)
529 511 start_pos = self.revisions.index(start_raw_id) if start else None
530 512 end_raw_id = self._get_revision(end)
531 513 end_pos = self.revisions.index(end_raw_id) if end else None
532 514
533 515 if None not in [start, end] and start_pos > end_pos:
534 516 raise RepositoryError("Start revision '%s' cannot be "
535 517 "after end revision '%s'" % (start, end))
536 518
537 519 if branch_name and branch_name not in self.allbranches.keys():
538 520 msg = ("Branch %s not found in %s" % (branch_name, self))
539 521 raise BranchDoesNotExistError(msg)
540 522 if end_pos is not None:
541 523 end_pos += 1
542 524 #filter branches
543 525 filter_ = []
544 526 if branch_name:
545 527 filter_.append('branch("%s")' % (branch_name))
546 528
547 529 if start_date and not end_date:
548 530 filter_.append('date(">%s")' % start_date)
549 531 if end_date and not start_date:
550 532 filter_.append('date("<%s")' % end_date)
551 533 if start_date and end_date:
552 534 filter_.append('date(">%s") and date("<%s")' % (start_date, end_date))
553 535 if filter_:
554 536 revisions = scmutil.revrange(self._repo, filter_)
555 537 else:
556 538 revisions = self.revisions
557 539
558 540 revs = revisions[start_pos:end_pos]
559 541 if reverse:
560 542 revs = reversed(revs)
561 543
562 544 return CollectionGenerator(self, revs)
563 545
564 546 def pull(self, url):
565 547 """
566 548 Tries to pull changes from external location.
567 549 """
568 550 url = self._get_url(url)
569 551 try:
570 552 other = peer(self._repo, {}, url)
571 553 self._repo.pull(other, heads=None, force=None)
572 554 except Abort, err:
573 555 # Propagate error but with vcs's type
574 556 raise RepositoryError(str(err))
575 557
576 558 @LazyProperty
577 559 def workdir(self):
578 560 """
579 561 Returns ``Workdir`` instance for this repository.
580 562 """
581 563 return MercurialWorkdir(self)
582 564
583 565 def get_config_value(self, section, name=None, config_file=None):
584 566 """
585 567 Returns configuration value for a given [``section``] and ``name``.
586 568
587 569 :param section: Section we want to retrieve value from
588 570 :param name: Name of configuration we want to retrieve
589 571 :param config_file: A path to file which should be used to retrieve
590 572 configuration from (might also be a list of file paths)
591 573 """
592 574 if config_file is None:
593 575 config_file = []
594 576 elif isinstance(config_file, basestring):
595 577 config_file = [config_file]
596 578
597 579 config = self._repo.ui
598 580 for path in config_file:
599 581 config.readconfig(path)
600 582 return config.config(section, name)
601 583
602 584 def get_user_name(self, config_file=None):
603 585 """
604 586 Returns user's name from global configuration file.
605 587
606 588 :param config_file: A path to file which should be used to retrieve
607 589 configuration from (might also be a list of file paths)
608 590 """
609 591 username = self.get_config_value('ui', 'username')
610 592 if username:
611 593 return author_name(username)
612 594 return None
613 595
614 596 def get_user_email(self, config_file=None):
615 597 """
616 598 Returns user's email from global configuration file.
617 599
618 600 :param config_file: A path to file which should be used to retrieve
619 601 configuration from (might also be a list of file paths)
620 602 """
621 603 username = self.get_config_value('ui', 'username')
622 604 if username:
623 605 return author_email(username)
624 606 return None
General Comments 0
You need to be logged in to leave comments. Login now