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