##// END OF EJS Templates
Added url validator for git
marcink -
r2704:959d0daa beta
parent child Browse files
Show More
@@ -1,616 +1,655 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 vcs.backends.git
4 4 ~~~~~~~~~~~~~~~~
5 5
6 6 Git backend 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 re
14 14 import time
15 15 import posixpath
16 16 import logging
17 17 import traceback
18 import urllib
19 import urllib2
18 20 from dulwich.repo import Repo, NotGitRepository
19 21 #from dulwich.config import ConfigFile
20 22 from string import Template
21 23 from subprocess import Popen, PIPE
22 24 from rhodecode.lib.vcs.backends.base import BaseRepository
23 25 from rhodecode.lib.vcs.exceptions import BranchDoesNotExistError
24 26 from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError
25 27 from rhodecode.lib.vcs.exceptions import EmptyRepositoryError
26 28 from rhodecode.lib.vcs.exceptions import RepositoryError
27 29 from rhodecode.lib.vcs.exceptions import TagAlreadyExistError
28 30 from rhodecode.lib.vcs.exceptions import TagDoesNotExistError
29 31 from rhodecode.lib.vcs.utils import safe_unicode, makedate, date_fromtimestamp
30 32 from rhodecode.lib.vcs.utils.lazy import LazyProperty
31 33 from rhodecode.lib.vcs.utils.ordered_dict import OrderedDict
32 34 from rhodecode.lib.vcs.utils.paths import abspath
33 35 from rhodecode.lib.vcs.utils.paths import get_user_home
34 36 from .workdir import GitWorkdir
35 37 from .changeset import GitChangeset
36 38 from .inmemory import GitInMemoryChangeset
37 39 from .config import ConfigFile
38 40 from rhodecode.lib import subprocessio
39 41
40 42
41 43 log = logging.getLogger(__name__)
42 44
43 45
44 46 class GitRepository(BaseRepository):
45 47 """
46 48 Git repository backend.
47 49 """
48 50 DEFAULT_BRANCH_NAME = 'master'
49 51 scm = 'git'
50 52
51 53 def __init__(self, repo_path, create=False, src_url=None,
52 54 update_after_clone=False, bare=False):
53 55
54 56 self.path = abspath(repo_path)
55 57 self._repo = self._get_repo(create, src_url, update_after_clone, bare)
56 58 #temporary set that to now at later we will move it to constructor
57 59 baseui = None
58 60 if baseui is None:
59 61 from mercurial.ui import ui
60 62 baseui = ui()
61 63 # patch the instance of GitRepo with an "FAKE" ui object to add
62 64 # compatibility layer with Mercurial
63 65 setattr(self._repo, 'ui', baseui)
64 66
65 67 try:
66 68 self.head = self._repo.head()
67 69 except KeyError:
68 70 self.head = None
69 71
70 72 self._config_files = [
71 73 bare and abspath(self.path, 'config') or abspath(self.path, '.git',
72 74 'config'),
73 75 abspath(get_user_home(), '.gitconfig'),
74 76 ]
75 77 self.bare = self._repo.bare
76 78
77 79 @LazyProperty
78 80 def revisions(self):
79 81 """
80 82 Returns list of revisions' ids, in ascending order. Being lazy
81 83 attribute allows external tools to inject shas from cache.
82 84 """
83 85 return self._get_all_revisions()
84 86
85 87 def run_git_command(self, cmd):
86 88 """
87 89 Runs given ``cmd`` as git command and returns tuple
88 90 (returncode, stdout, stderr).
89 91
90 92 .. note::
91 93 This method exists only until log/blame functionality is implemented
92 94 at Dulwich (see https://bugs.launchpad.net/bugs/645142). Parsing
93 95 os command's output is road to hell...
94 96
95 97 :param cmd: git command to be executed
96 98 """
97 99
98 100 _copts = ['-c', 'core.quotepath=false', ]
99 101 _str_cmd = False
100 102 if isinstance(cmd, basestring):
101 103 cmd = [cmd]
102 104 _str_cmd = True
103 105
104 106 gitenv = os.environ
105 107 # need to clean fix GIT_DIR !
106 108 if 'GIT_DIR' in gitenv:
107 109 del gitenv['GIT_DIR']
108 110 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
109 111
110 112 cmd = ['git'] + _copts + cmd
111 113 if _str_cmd:
112 114 cmd = ' '.join(cmd)
113 115 try:
114 116 opts = dict(
115 117 env=gitenv,
116 118 )
117 119 if os.path.isdir(self.path):
118 120 opts['cwd'] = self.path
119 121 p = subprocessio.SubprocessIOChunker(cmd, **opts)
120 122 except (EnvironmentError, OSError), err:
121 123 log.error(traceback.format_exc())
122 124 raise RepositoryError("Couldn't run git command (%s).\n"
123 125 "Original error was:%s" % (cmd, err))
124 126
125 127 so = ''.join(p)
126 128 se = None
127 129 return so, se
128 130
129 def _check_url(self, url):
131 @classmethod
132 def _check_url(cls, url):
130 133 """
131 134 Functon will check given url and try to verify if it's a valid
132 135 link. Sometimes it may happened that mercurial will issue basic
133 136 auth request that can cause whole API to hang when used from python
134 137 or other external calls.
135 138
136 139 On failures it'll raise urllib2.HTTPError
137 140 """
141 from mercurial.util import url as Url
138 142
139 #TODO: implement this
140 pass
143 # those authnadlers are patched for python 2.6.5 bug an
144 # infinit looping when given invalid resources
145 from mercurial.url import httpbasicauthhandler, httpdigestauthhandler
146
147 # check first if it's not an local url
148 if os.path.isdir(url) or url.startswith('file:'):
149 return True
150
151 if('+' in url[:url.find('://')]):
152 url = url[url.find('+') + 1:]
153
154 handlers = []
155 test_uri, authinfo = Url(url).authinfo()
156 if not test_uri.endswith('info/refs'):
157 test_uri = test_uri.rstrip('/') + '/info/refs'
158 if authinfo:
159 #create a password manager
160 passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
161 passmgr.add_password(*authinfo)
162
163 handlers.extend((httpbasicauthhandler(passmgr),
164 httpdigestauthhandler(passmgr)))
165
166 o = urllib2.build_opener(*handlers)
167 o.addheaders = [('User-Agent', 'git/1.7.8.0')] # fake some git
168
169 q = {"service": 'git-upload-pack'}
170 qs = '?%s' % urllib.urlencode(q)
171 cu = "%s%s" % (test_uri, qs)
172 req = urllib2.Request(cu, None, {})
173
174 try:
175 resp = o.open(req)
176 return resp.code == 200
177 except Exception, e:
178 # means it cannot be cloned
179 raise urllib2.URLError(e)
141 180
142 181 def _get_repo(self, create, src_url=None, update_after_clone=False,
143 182 bare=False):
144 183 if create and os.path.exists(self.path):
145 184 raise RepositoryError("Location already exist")
146 185 if src_url and not create:
147 186 raise RepositoryError("Create should be set to True if src_url is "
148 187 "given (clone operation creates repository)")
149 188 try:
150 189 if create and src_url:
151 self._check_url(src_url)
190 GitRepository._check_url(src_url)
152 191 self.clone(src_url, update_after_clone, bare)
153 192 return Repo(self.path)
154 193 elif create:
155 194 os.mkdir(self.path)
156 195 if bare:
157 196 return Repo.init_bare(self.path)
158 197 else:
159 198 return Repo.init(self.path)
160 199 else:
161 200 return Repo(self.path)
162 201 except (NotGitRepository, OSError), err:
163 202 raise RepositoryError(err)
164 203
165 204 def _get_all_revisions(self):
166 205 # we must check if this repo is not empty, since later command
167 206 # fails if it is. And it's cheaper to ask than throw the subprocess
168 207 # errors
169 208 try:
170 209 self._repo.head()
171 210 except KeyError:
172 211 return []
173 212 cmd = 'rev-list --all --reverse --date-order'
174 213 try:
175 214 so, se = self.run_git_command(cmd)
176 215 except RepositoryError:
177 216 # Can be raised for empty repositories
178 217 return []
179 218 return so.splitlines()
180 219
181 220 def _get_all_revisions2(self):
182 221 #alternate implementation using dulwich
183 222 includes = [x[1][0] for x in self._parsed_refs.iteritems()
184 223 if x[1][1] != 'T']
185 224 return [c.commit.id for c in self._repo.get_walker(include=includes)]
186 225
187 226 def _get_revision(self, revision):
188 227 """
189 228 For git backend we always return integer here. This way we ensure
190 229 that changset's revision attribute would become integer.
191 230 """
192 231 pattern = re.compile(r'^[[0-9a-fA-F]{12}|[0-9a-fA-F]{40}]$')
193 232 is_bstr = lambda o: isinstance(o, (str, unicode))
194 233 is_null = lambda o: len(o) == revision.count('0')
195 234
196 235 if len(self.revisions) == 0:
197 236 raise EmptyRepositoryError("There are no changesets yet")
198 237
199 238 if revision in (None, '', 'tip', 'HEAD', 'head', -1):
200 239 revision = self.revisions[-1]
201 240
202 241 if ((is_bstr(revision) and revision.isdigit() and len(revision) < 12)
203 242 or isinstance(revision, int) or is_null(revision)):
204 243 try:
205 244 revision = self.revisions[int(revision)]
206 245 except:
207 246 raise ChangesetDoesNotExistError("Revision %r does not exist "
208 247 "for this repository %s" % (revision, self))
209 248
210 249 elif is_bstr(revision):
211 250 # get by branch/tag name
212 251 _ref_revision = self._parsed_refs.get(revision)
213 252 _tags_shas = self.tags.values()
214 253 if _ref_revision: # and _ref_revision[1] in ['H', 'RH', 'T']:
215 254 return _ref_revision[0]
216 255
217 256 # maybe it's a tag ? we don't have them in self.revisions
218 257 elif revision in _tags_shas:
219 258 return _tags_shas[_tags_shas.index(revision)]
220 259
221 260 elif not pattern.match(revision) or revision not in self.revisions:
222 261 raise ChangesetDoesNotExistError("Revision %r does not exist "
223 262 "for this repository %s" % (revision, self))
224 263
225 264 # Ensure we return full id
226 265 if not pattern.match(str(revision)):
227 266 raise ChangesetDoesNotExistError("Given revision %r not recognized"
228 267 % revision)
229 268 return revision
230 269
231 270 def _get_archives(self, archive_name='tip'):
232 271
233 272 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
234 273 yield {"type": i[0], "extension": i[1], "node": archive_name}
235 274
236 275 def _get_url(self, url):
237 276 """
238 277 Returns normalized url. If schema is not given, would fall to
239 278 filesystem (``file:///``) schema.
240 279 """
241 280 url = str(url)
242 281 if url != 'default' and not '://' in url:
243 282 url = ':///'.join(('file', url))
244 283 return url
245 284
246 285 @LazyProperty
247 286 def name(self):
248 287 return os.path.basename(self.path)
249 288
250 289 @LazyProperty
251 290 def last_change(self):
252 291 """
253 292 Returns last change made on this repository as datetime object
254 293 """
255 294 return date_fromtimestamp(self._get_mtime(), makedate()[1])
256 295
257 296 def _get_mtime(self):
258 297 try:
259 298 return time.mktime(self.get_changeset().date.timetuple())
260 299 except RepositoryError:
261 300 idx_loc = '' if self.bare else '.git'
262 301 # fallback to filesystem
263 302 in_path = os.path.join(self.path, idx_loc, "index")
264 303 he_path = os.path.join(self.path, idx_loc, "HEAD")
265 304 if os.path.exists(in_path):
266 305 return os.stat(in_path).st_mtime
267 306 else:
268 307 return os.stat(he_path).st_mtime
269 308
270 309 @LazyProperty
271 310 def description(self):
272 311 idx_loc = '' if self.bare else '.git'
273 312 undefined_description = u'unknown'
274 313 description_path = os.path.join(self.path, idx_loc, 'description')
275 314 if os.path.isfile(description_path):
276 315 return safe_unicode(open(description_path).read())
277 316 else:
278 317 return undefined_description
279 318
280 319 @LazyProperty
281 320 def contact(self):
282 321 undefined_contact = u'Unknown'
283 322 return undefined_contact
284 323
285 324 @property
286 325 def branches(self):
287 326 if not self.revisions:
288 327 return {}
289 328 sortkey = lambda ctx: ctx[0]
290 329 _branches = [(x[0], x[1][0])
291 330 for x in self._parsed_refs.iteritems() if x[1][1] == 'H']
292 331 return OrderedDict(sorted(_branches, key=sortkey, reverse=False))
293 332
294 333 @LazyProperty
295 334 def tags(self):
296 335 return self._get_tags()
297 336
298 337 def _get_tags(self):
299 338 if not self.revisions:
300 339 return {}
301 340
302 341 sortkey = lambda ctx: ctx[0]
303 342 _tags = [(x[0], x[1][0])
304 343 for x in self._parsed_refs.iteritems() if x[1][1] == 'T']
305 344 return OrderedDict(sorted(_tags, key=sortkey, reverse=True))
306 345
307 346 def tag(self, name, user, revision=None, message=None, date=None,
308 347 **kwargs):
309 348 """
310 349 Creates and returns a tag for the given ``revision``.
311 350
312 351 :param name: name for new tag
313 352 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
314 353 :param revision: changeset id for which new tag would be created
315 354 :param message: message of the tag's commit
316 355 :param date: date of tag's commit
317 356
318 357 :raises TagAlreadyExistError: if tag with same name already exists
319 358 """
320 359 if name in self.tags:
321 360 raise TagAlreadyExistError("Tag %s already exists" % name)
322 361 changeset = self.get_changeset(revision)
323 362 message = message or "Added tag %s for commit %s" % (name,
324 363 changeset.raw_id)
325 364 self._repo.refs["refs/tags/%s" % name] = changeset._commit.id
326 365
327 366 self._parsed_refs = self._get_parsed_refs()
328 367 self.tags = self._get_tags()
329 368 return changeset
330 369
331 370 def remove_tag(self, name, user, message=None, date=None):
332 371 """
333 372 Removes tag with the given ``name``.
334 373
335 374 :param name: name of the tag to be removed
336 375 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
337 376 :param message: message of the tag's removal commit
338 377 :param date: date of tag's removal commit
339 378
340 379 :raises TagDoesNotExistError: if tag with given name does not exists
341 380 """
342 381 if name not in self.tags:
343 382 raise TagDoesNotExistError("Tag %s does not exist" % name)
344 383 tagpath = posixpath.join(self._repo.refs.path, 'refs', 'tags', name)
345 384 try:
346 385 os.remove(tagpath)
347 386 self._parsed_refs = self._get_parsed_refs()
348 387 self.tags = self._get_tags()
349 388 except OSError, e:
350 389 raise RepositoryError(e.strerror)
351 390
352 391 @LazyProperty
353 392 def _parsed_refs(self):
354 393 return self._get_parsed_refs()
355 394
356 395 def _get_parsed_refs(self):
357 396 refs = self._repo.get_refs()
358 397 keys = [('refs/heads/', 'H'),
359 398 ('refs/remotes/origin/', 'RH'),
360 399 ('refs/tags/', 'T')]
361 400 _refs = {}
362 401 for ref, sha in refs.iteritems():
363 402 for k, type_ in keys:
364 403 if ref.startswith(k):
365 404 _key = ref[len(k):]
366 405 _refs[_key] = [sha, type_]
367 406 break
368 407 return _refs
369 408
370 409 def _heads(self, reverse=False):
371 410 refs = self._repo.get_refs()
372 411 heads = {}
373 412
374 413 for key, val in refs.items():
375 414 for ref_key in ['refs/heads/', 'refs/remotes/origin/']:
376 415 if key.startswith(ref_key):
377 416 n = key[len(ref_key):]
378 417 if n not in ['HEAD']:
379 418 heads[n] = val
380 419
381 420 return heads if reverse else dict((y, x) for x, y in heads.iteritems())
382 421
383 422 def get_changeset(self, revision=None):
384 423 """
385 424 Returns ``GitChangeset`` object representing commit from git repository
386 425 at the given revision or head (most recent commit) if None given.
387 426 """
388 427 if isinstance(revision, GitChangeset):
389 428 return revision
390 429 revision = self._get_revision(revision)
391 430 changeset = GitChangeset(repository=self, revision=revision)
392 431 return changeset
393 432
394 433 def get_changesets(self, start=None, end=None, start_date=None,
395 434 end_date=None, branch_name=None, reverse=False):
396 435 """
397 436 Returns iterator of ``GitChangeset`` objects from start to end (both
398 437 are inclusive), in ascending date order (unless ``reverse`` is set).
399 438
400 439 :param start: changeset ID, as str; first returned changeset
401 440 :param end: changeset ID, as str; last returned changeset
402 441 :param start_date: if specified, changesets with commit date less than
403 442 ``start_date`` would be filtered out from returned set
404 443 :param end_date: if specified, changesets with commit date greater than
405 444 ``end_date`` would be filtered out from returned set
406 445 :param branch_name: if specified, changesets not reachable from given
407 446 branch would be filtered out from returned set
408 447 :param reverse: if ``True``, returned generator would be reversed
409 448 (meaning that returned changesets would have descending date order)
410 449
411 450 :raise BranchDoesNotExistError: If given ``branch_name`` does not
412 451 exist.
413 452 :raise ChangesetDoesNotExistError: If changeset for given ``start`` or
414 453 ``end`` could not be found.
415 454
416 455 """
417 456 if branch_name and branch_name not in self.branches:
418 457 raise BranchDoesNotExistError("Branch '%s' not found" \
419 458 % branch_name)
420 459 # %H at format means (full) commit hash, initial hashes are retrieved
421 460 # in ascending date order
422 461 cmd_template = 'log --date-order --reverse --pretty=format:"%H"'
423 462 cmd_params = {}
424 463 if start_date:
425 464 cmd_template += ' --since "$since"'
426 465 cmd_params['since'] = start_date.strftime('%m/%d/%y %H:%M:%S')
427 466 if end_date:
428 467 cmd_template += ' --until "$until"'
429 468 cmd_params['until'] = end_date.strftime('%m/%d/%y %H:%M:%S')
430 469 if branch_name:
431 470 cmd_template += ' $branch_name'
432 471 cmd_params['branch_name'] = branch_name
433 472 else:
434 473 cmd_template += ' --all'
435 474
436 475 cmd = Template(cmd_template).safe_substitute(**cmd_params)
437 476 revs = self.run_git_command(cmd)[0].splitlines()
438 477 start_pos = 0
439 478 end_pos = len(revs)
440 479 if start:
441 480 _start = self._get_revision(start)
442 481 try:
443 482 start_pos = revs.index(_start)
444 483 except ValueError:
445 484 pass
446 485
447 486 if end is not None:
448 487 _end = self._get_revision(end)
449 488 try:
450 489 end_pos = revs.index(_end)
451 490 except ValueError:
452 491 pass
453 492
454 493 if None not in [start, end] and start_pos > end_pos:
455 494 raise RepositoryError('start cannot be after end')
456 495
457 496 if end_pos is not None:
458 497 end_pos += 1
459 498
460 499 revs = revs[start_pos:end_pos]
461 500 if reverse:
462 501 revs = reversed(revs)
463 502 for rev in revs:
464 503 yield self.get_changeset(rev)
465 504
466 505 def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
467 506 context=3):
468 507 """
469 508 Returns (git like) *diff*, as plain text. Shows changes introduced by
470 509 ``rev2`` since ``rev1``.
471 510
472 511 :param rev1: Entry point from which diff is shown. Can be
473 512 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
474 513 the changes since empty state of the repository until ``rev2``
475 514 :param rev2: Until which revision changes should be shown.
476 515 :param ignore_whitespace: If set to ``True``, would not show whitespace
477 516 changes. Defaults to ``False``.
478 517 :param context: How many lines before/after changed lines should be
479 518 shown. Defaults to ``3``.
480 519 """
481 520 flags = ['-U%s' % context]
482 521 if ignore_whitespace:
483 522 flags.append('-w')
484 523
485 524 if hasattr(rev1, 'raw_id'):
486 525 rev1 = getattr(rev1, 'raw_id')
487 526
488 527 if hasattr(rev2, 'raw_id'):
489 528 rev2 = getattr(rev2, 'raw_id')
490 529
491 530 if rev1 == self.EMPTY_CHANGESET:
492 531 rev2 = self.get_changeset(rev2).raw_id
493 532 cmd = ' '.join(['show'] + flags + [rev2])
494 533 else:
495 534 rev1 = self.get_changeset(rev1).raw_id
496 535 rev2 = self.get_changeset(rev2).raw_id
497 536 cmd = ' '.join(['diff'] + flags + [rev1, rev2])
498 537
499 538 if path:
500 539 cmd += ' -- "%s"' % path
501 540 stdout, stderr = self.run_git_command(cmd)
502 541 # If we used 'show' command, strip first few lines (until actual diff
503 542 # starts)
504 543 if rev1 == self.EMPTY_CHANGESET:
505 544 lines = stdout.splitlines()
506 545 x = 0
507 546 for line in lines:
508 547 if line.startswith('diff'):
509 548 break
510 549 x += 1
511 550 # Append new line just like 'diff' command do
512 551 stdout = '\n'.join(lines[x:]) + '\n'
513 552 return stdout
514 553
515 554 @LazyProperty
516 555 def in_memory_changeset(self):
517 556 """
518 557 Returns ``GitInMemoryChangeset`` object for this repository.
519 558 """
520 559 return GitInMemoryChangeset(self)
521 560
522 561 def clone(self, url, update_after_clone=True, bare=False):
523 562 """
524 563 Tries to clone changes from external location.
525 564
526 565 :param update_after_clone: If set to ``False``, git won't checkout
527 566 working directory
528 567 :param bare: If set to ``True``, repository would be cloned into
529 568 *bare* git repository (no working directory at all).
530 569 """
531 570 url = self._get_url(url)
532 571 cmd = ['clone']
533 572 if bare:
534 573 cmd.append('--bare')
535 574 elif not update_after_clone:
536 575 cmd.append('--no-checkout')
537 576 cmd += ['--', '"%s"' % url, '"%s"' % self.path]
538 577 cmd = ' '.join(cmd)
539 578 # If error occurs run_git_command raises RepositoryError already
540 579 self.run_git_command(cmd)
541 580
542 581 def pull(self, url):
543 582 """
544 583 Tries to pull changes from external location.
545 584 """
546 585 url = self._get_url(url)
547 586 cmd = ['pull']
548 587 cmd.append("--ff-only")
549 588 cmd.append(url)
550 589 cmd = ' '.join(cmd)
551 590 # If error occurs run_git_command raises RepositoryError already
552 591 self.run_git_command(cmd)
553 592
554 593 def fetch(self, url):
555 594 """
556 595 Tries to pull changes from external location.
557 596 """
558 597 url = self._get_url(url)
559 598 cmd = ['fetch']
560 599 cmd.append(url)
561 600 cmd = ' '.join(cmd)
562 601 # If error occurs run_git_command raises RepositoryError already
563 602 self.run_git_command(cmd)
564 603
565 604 @LazyProperty
566 605 def workdir(self):
567 606 """
568 607 Returns ``Workdir`` instance for this repository.
569 608 """
570 609 return GitWorkdir(self)
571 610
572 611 def get_config_value(self, section, name, config_file=None):
573 612 """
574 613 Returns configuration value for a given [``section``] and ``name``.
575 614
576 615 :param section: Section we want to retrieve value from
577 616 :param name: Name of configuration we want to retrieve
578 617 :param config_file: A path to file which should be used to retrieve
579 618 configuration from (might also be a list of file paths)
580 619 """
581 620 if config_file is None:
582 621 config_file = []
583 622 elif isinstance(config_file, basestring):
584 623 config_file = [config_file]
585 624
586 625 def gen_configs():
587 626 for path in config_file + self._config_files:
588 627 try:
589 628 yield ConfigFile.from_path(path)
590 629 except (IOError, OSError, ValueError):
591 630 continue
592 631
593 632 for config in gen_configs():
594 633 try:
595 634 return config.get(section, name)
596 635 except KeyError:
597 636 continue
598 637 return None
599 638
600 639 def get_user_name(self, config_file=None):
601 640 """
602 641 Returns user's name from global configuration file.
603 642
604 643 :param config_file: A path to file which should be used to retrieve
605 644 configuration from (might also be a list of file paths)
606 645 """
607 646 return self.get_config_value('user', 'name', config_file)
608 647
609 648 def get_user_email(self, config_file=None):
610 649 """
611 650 Returns user's email from global configuration file.
612 651
613 652 :param config_file: A path to file which should be used to retrieve
614 653 configuration from (might also be a list of file paths)
615 654 """
616 655 return self.get_config_value('user', 'email', config_file)
@@ -1,530 +1,531 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
246 246 file_filter = match(self.path, '', [path])
247 247 return ''.join(patch.diff(self._repo, rev1, rev2, match=file_filter,
248 248 opts=diffopts(git=True,
249 249 ignorews=ignore_whitespace,
250 250 context=context)))
251 251
252 def _check_url(self, url):
252 @classmethod
253 def _check_url(cls, url):
253 254 """
254 255 Function will check given url and try to verify if it's a valid
255 256 link. Sometimes it may happened that mercurial will issue basic
256 257 auth request that can cause whole API to hang when used from python
257 258 or other external calls.
258 259
259 260 On failures it'll raise urllib2.HTTPError, return code 200 if url
260 261 is valid or True if it's a local path
261 262 """
262 263
263 264 from mercurial.util import url as Url
264 265
265 266 # those authnadlers are patched for python 2.6.5 bug an
266 267 # infinit looping when given invalid resources
267 268 from mercurial.url import httpbasicauthhandler, httpdigestauthhandler
268 269
269 270 # check first if it's not an local url
270 271 if os.path.isdir(url) or url.startswith('file:'):
271 272 return True
272 273
273 274 if('+' in url[:url.find('://')]):
274 url = url[url.find('+')+1:]
275 url = url[url.find('+') + 1:]
275 276
276 277 handlers = []
277 278 test_uri, authinfo = Url(url).authinfo()
278 279
279 280 if authinfo:
280 281 #create a password manager
281 282 passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
282 283 passmgr.add_password(*authinfo)
283 284
284 285 handlers.extend((httpbasicauthhandler(passmgr),
285 286 httpdigestauthhandler(passmgr)))
286 287
287 288 o = urllib2.build_opener(*handlers)
288 289 o.addheaders = [('Content-Type', 'application/mercurial-0.1'),
289 290 ('Accept', 'application/mercurial-0.1')]
290 291
291 292 q = {"cmd": 'between'}
292 293 q.update({'pairs': "%s-%s" % ('0' * 40, '0' * 40)})
293 294 qs = '?%s' % urllib.urlencode(q)
294 295 cu = "%s%s" % (test_uri, qs)
295 296 req = urllib2.Request(cu, None, {})
296 297
297 298 try:
298 299 resp = o.open(req)
299 300 return resp.code == 200
300 301 except Exception, e:
301 302 # means it cannot be cloned
302 303 raise urllib2.URLError(e)
303 304
304 305 def _get_repo(self, create, src_url=None, update_after_clone=False):
305 306 """
306 307 Function will check for mercurial repository in given path and return
307 308 a localrepo object. If there is no repository in that path it will
308 309 raise an exception unless ``create`` parameter is set to True - in
309 310 that case repository would be created and returned.
310 311 If ``src_url`` is given, would try to clone repository from the
311 312 location at given clone_point. Additionally it'll make update to
312 313 working copy accordingly to ``update_after_clone`` flag
313 314 """
314 315 try:
315 316 if src_url:
316 317 url = str(self._get_url(src_url))
317 318 opts = {}
318 319 if not update_after_clone:
319 320 opts.update({'noupdate': True})
320 321 try:
321 self._check_url(url)
322 MercurialRepository._check_url(url)
322 323 clone(self.baseui, url, self.path, **opts)
323 324 # except urllib2.URLError:
324 325 # raise Abort("Got HTTP 404 error")
325 326 except Exception:
326 327 raise
327 328 # Don't try to create if we've already cloned repo
328 329 create = False
329 330 return localrepository(self.baseui, self.path, create=create)
330 331 except (Abort, RepoError), err:
331 332 if create:
332 333 msg = "Cannot create repository at %s. Original error was %s"\
333 334 % (self.path, err)
334 335 else:
335 336 msg = "Not valid repository at %s. Original error was %s"\
336 337 % (self.path, err)
337 338 raise RepositoryError(msg)
338 339
339 340 @LazyProperty
340 341 def in_memory_changeset(self):
341 342 return MercurialInMemoryChangeset(self)
342 343
343 344 @LazyProperty
344 345 def description(self):
345 346 undefined_description = u'unknown'
346 347 return safe_unicode(self._repo.ui.config('web', 'description',
347 348 undefined_description, untrusted=True))
348 349
349 350 @LazyProperty
350 351 def contact(self):
351 352 undefined_contact = u'Unknown'
352 353 return safe_unicode(get_contact(self._repo.ui.config)
353 354 or undefined_contact)
354 355
355 356 @LazyProperty
356 357 def last_change(self):
357 358 """
358 359 Returns last change made on this repository as datetime object
359 360 """
360 361 return date_fromtimestamp(self._get_mtime(), makedate()[1])
361 362
362 363 def _get_mtime(self):
363 364 try:
364 365 return time.mktime(self.get_changeset().date.timetuple())
365 366 except RepositoryError:
366 367 #fallback to filesystem
367 368 cl_path = os.path.join(self.path, '.hg', "00changelog.i")
368 369 st_path = os.path.join(self.path, '.hg', "store")
369 370 if os.path.exists(cl_path):
370 371 return os.stat(cl_path).st_mtime
371 372 else:
372 373 return os.stat(st_path).st_mtime
373 374
374 375 def _get_hidden(self):
375 376 return self._repo.ui.configbool("web", "hidden", untrusted=True)
376 377
377 378 def _get_revision(self, revision):
378 379 """
379 380 Get's an ID revision given as str. This will always return a fill
380 381 40 char revision number
381 382
382 383 :param revision: str or int or None
383 384 """
384 385
385 386 if self._empty:
386 387 raise EmptyRepositoryError("There are no changesets yet")
387 388
388 389 if revision in [-1, 'tip', None]:
389 390 revision = 'tip'
390 391
391 392 try:
392 393 revision = hex(self._repo.lookup(revision))
393 394 except (IndexError, ValueError, RepoLookupError, TypeError):
394 395 raise ChangesetDoesNotExistError("Revision %r does not "
395 396 "exist for this repository %s" \
396 397 % (revision, self))
397 398 return revision
398 399
399 400 def _get_archives(self, archive_name='tip'):
400 401 allowed = self.baseui.configlist("web", "allow_archive",
401 402 untrusted=True)
402 403 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
403 404 if i[0] in allowed or self._repo.ui.configbool("web",
404 405 "allow" + i[0],
405 406 untrusted=True):
406 407 yield {"type": i[0], "extension": i[1], "node": archive_name}
407 408
408 409 def _get_url(self, url):
409 410 """
410 411 Returns normalized url. If schema is not given, would fall
411 412 to filesystem
412 413 (``file:///``) schema.
413 414 """
414 415 url = str(url)
415 416 if url != 'default' and not '://' in url:
416 417 url = "file:" + urllib.pathname2url(url)
417 418 return url
418 419
419 420 def get_changeset(self, revision=None):
420 421 """
421 422 Returns ``MercurialChangeset`` object representing repository's
422 423 changeset at the given ``revision``.
423 424 """
424 425 revision = self._get_revision(revision)
425 426 changeset = MercurialChangeset(repository=self, revision=revision)
426 427 return changeset
427 428
428 429 def get_changesets(self, start=None, end=None, start_date=None,
429 430 end_date=None, branch_name=None, reverse=False):
430 431 """
431 432 Returns iterator of ``MercurialChangeset`` objects from start to end
432 433 (both are inclusive)
433 434
434 435 :param start: None, str, int or mercurial lookup format
435 436 :param end: None, str, int or mercurial lookup format
436 437 :param start_date:
437 438 :param end_date:
438 439 :param branch_name:
439 440 :param reversed: return changesets in reversed order
440 441 """
441 442
442 443 start_raw_id = self._get_revision(start)
443 444 start_pos = self.revisions.index(start_raw_id) if start else None
444 445 end_raw_id = self._get_revision(end)
445 446 end_pos = self.revisions.index(end_raw_id) if end else None
446 447
447 448 if None not in [start, end] and start_pos > end_pos:
448 449 raise RepositoryError("start revision '%s' cannot be "
449 450 "after end revision '%s'" % (start, end))
450 451
451 452 if branch_name and branch_name not in self.branches.keys():
452 453 raise BranchDoesNotExistError('Such branch %s does not exists for'
453 454 ' this repository' % branch_name)
454 455 if end_pos is not None:
455 456 end_pos += 1
456 457
457 458 slice_ = reversed(self.revisions[start_pos:end_pos]) if reverse else \
458 459 self.revisions[start_pos:end_pos]
459 460
460 461 for id_ in slice_:
461 462 cs = self.get_changeset(id_)
462 463 if branch_name and cs.branch != branch_name:
463 464 continue
464 465 if start_date and cs.date < start_date:
465 466 continue
466 467 if end_date and cs.date > end_date:
467 468 continue
468 469
469 470 yield cs
470 471
471 472 def pull(self, url):
472 473 """
473 474 Tries to pull changes from external location.
474 475 """
475 476 url = self._get_url(url)
476 477 try:
477 478 pull(self.baseui, self._repo, url)
478 479 except Abort, err:
479 480 # Propagate error but with vcs's type
480 481 raise RepositoryError(str(err))
481 482
482 483 @LazyProperty
483 484 def workdir(self):
484 485 """
485 486 Returns ``Workdir`` instance for this repository.
486 487 """
487 488 return MercurialWorkdir(self)
488 489
489 490 def get_config_value(self, section, name, config_file=None):
490 491 """
491 492 Returns configuration value for a given [``section``] and ``name``.
492 493
493 494 :param section: Section we want to retrieve value from
494 495 :param name: Name of configuration we want to retrieve
495 496 :param config_file: A path to file which should be used to retrieve
496 497 configuration from (might also be a list of file paths)
497 498 """
498 499 if config_file is None:
499 500 config_file = []
500 501 elif isinstance(config_file, basestring):
501 502 config_file = [config_file]
502 503
503 504 config = self._repo.ui
504 505 for path in config_file:
505 506 config.readconfig(path)
506 507 return config.config(section, name)
507 508
508 509 def get_user_name(self, config_file=None):
509 510 """
510 511 Returns user's name from global configuration file.
511 512
512 513 :param config_file: A path to file which should be used to retrieve
513 514 configuration from (might also be a list of file paths)
514 515 """
515 516 username = self.get_config_value('ui', 'username')
516 517 if username:
517 518 return author_name(username)
518 519 return None
519 520
520 521 def get_user_email(self, config_file=None):
521 522 """
522 523 Returns user's email from global configuration file.
523 524
524 525 :param config_file: A path to file which should be used to retrieve
525 526 configuration from (might also be a list of file paths)
526 527 """
527 528 username = self.get_config_value('ui', 'username')
528 529 if username:
529 530 return author_email(username)
530 531 return None
General Comments 0
You need to be logged in to leave comments. Login now