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