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