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