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