##// END OF EJS Templates
git: use smarter way for checking if repo is empty. This doesn't spam logs with some dulwich exceptions, we shouldn't really care about here.`
marcink -
r2955:7eb44380 default
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -1,1009 +1,1009 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2014-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 GIT repository module
23 23 """
24 24
25 25 import logging
26 26 import os
27 27 import re
28 28
29 29 from zope.cachedescriptors.property import Lazy as LazyProperty
30 30
31 31 from rhodecode.lib.compat import OrderedDict
32 32 from rhodecode.lib.datelib import (
33 33 utcdate_fromtimestamp, makedate, date_astimestamp)
34 34 from rhodecode.lib.utils import safe_unicode, safe_str
35 35 from rhodecode.lib.vcs import connection, path as vcspath
36 36 from rhodecode.lib.vcs.backends.base import (
37 37 BaseRepository, CollectionGenerator, Config, MergeResponse,
38 38 MergeFailureReason, Reference)
39 39 from rhodecode.lib.vcs.backends.git.commit import GitCommit
40 40 from rhodecode.lib.vcs.backends.git.diff import GitDiff
41 41 from rhodecode.lib.vcs.backends.git.inmemory import GitInMemoryCommit
42 42 from rhodecode.lib.vcs.exceptions import (
43 43 CommitDoesNotExistError, EmptyRepositoryError,
44 44 RepositoryError, TagAlreadyExistError, TagDoesNotExistError, VCSError)
45 45
46 46
47 47 SHA_PATTERN = re.compile(r'^[[0-9a-fA-F]{12}|[0-9a-fA-F]{40}]$')
48 48
49 49 log = logging.getLogger(__name__)
50 50
51 51
52 52 class GitRepository(BaseRepository):
53 53 """
54 54 Git repository backend.
55 55 """
56 56 DEFAULT_BRANCH_NAME = 'master'
57 57
58 58 contact = BaseRepository.DEFAULT_CONTACT
59 59
60 60 def __init__(self, repo_path, config=None, create=False, src_url=None,
61 61 update_after_clone=False, with_wire=None, bare=False):
62 62
63 63 self.path = safe_str(os.path.abspath(repo_path))
64 64 self.config = config if config else self.get_default_config()
65 65 self.with_wire = with_wire
66 66
67 67 self._init_repo(create, src_url, update_after_clone, bare)
68 68
69 69 # caches
70 70 self._commit_ids = {}
71 71
72 72 @LazyProperty
73 73 def _remote(self):
74 74 return connection.Git(self.path, self.config, with_wire=self.with_wire)
75 75
76 76 @LazyProperty
77 77 def bare(self):
78 78 return self._remote.bare()
79 79
80 80 @LazyProperty
81 81 def head(self):
82 82 return self._remote.head()
83 83
84 84 @LazyProperty
85 85 def commit_ids(self):
86 86 """
87 87 Returns list of commit ids, in ascending order. Being lazy
88 88 attribute allows external tools to inject commit ids from cache.
89 89 """
90 90 commit_ids = self._get_all_commit_ids()
91 91 self._rebuild_cache(commit_ids)
92 92 return commit_ids
93 93
94 94 def _rebuild_cache(self, commit_ids):
95 95 self._commit_ids = dict((commit_id, index)
96 96 for index, commit_id in enumerate(commit_ids))
97 97
98 98 def run_git_command(self, cmd, **opts):
99 99 """
100 100 Runs given ``cmd`` as git command and returns tuple
101 101 (stdout, stderr).
102 102
103 103 :param cmd: git command to be executed
104 104 :param opts: env options to pass into Subprocess command
105 105 """
106 106 if not isinstance(cmd, list):
107 107 raise ValueError('cmd must be a list, got %s instead' % type(cmd))
108 108
109 109 skip_stderr_log = opts.pop('skip_stderr_log', False)
110 110 out, err = self._remote.run_git_command(cmd, **opts)
111 111 if err and not skip_stderr_log:
112 112 log.debug('Stderr output of git command "%s":\n%s', cmd, err)
113 113 return out, err
114 114
115 115 @staticmethod
116 116 def check_url(url, config):
117 117 """
118 118 Function will check given url and try to verify if it's a valid
119 119 link. Sometimes it may happened that git will issue basic
120 120 auth request that can cause whole API to hang when used from python
121 121 or other external calls.
122 122
123 123 On failures it'll raise urllib2.HTTPError, exception is also thrown
124 124 when the return code is non 200
125 125 """
126 126 # check first if it's not an url
127 127 if os.path.isdir(url) or url.startswith('file:'):
128 128 return True
129 129
130 130 if '+' in url.split('://', 1)[0]:
131 131 url = url.split('+', 1)[1]
132 132
133 133 # Request the _remote to verify the url
134 134 return connection.Git.check_url(url, config.serialize())
135 135
136 136 @staticmethod
137 137 def is_valid_repository(path):
138 138 if os.path.isdir(os.path.join(path, '.git')):
139 139 return True
140 140 # check case of bare repository
141 141 try:
142 142 GitRepository(path)
143 143 return True
144 144 except VCSError:
145 145 pass
146 146 return False
147 147
148 148 def _init_repo(self, create, src_url=None, update_after_clone=False,
149 149 bare=False):
150 150 if create and os.path.exists(self.path):
151 151 raise RepositoryError(
152 152 "Cannot create repository at %s, location already exist"
153 153 % self.path)
154 154
155 155 try:
156 156 if create and src_url:
157 157 GitRepository.check_url(src_url, self.config)
158 158 self.clone(src_url, update_after_clone, bare)
159 159 elif create:
160 160 os.makedirs(self.path, mode=0755)
161 161
162 162 if bare:
163 163 self._remote.init_bare()
164 164 else:
165 165 self._remote.init()
166 166 else:
167 167 if not self._remote.assert_correct_path():
168 168 raise RepositoryError(
169 169 'Path "%s" does not contain a Git repository' %
170 170 (self.path,))
171 171
172 172 # TODO: johbo: check if we have to translate the OSError here
173 173 except OSError as err:
174 174 raise RepositoryError(err)
175 175
176 176 def _get_all_commit_ids(self, filters=None):
177 177 # we must check if this repo is not empty, since later command
178 178 # fails if it is. And it's cheaper to ask than throw the subprocess
179 179 # errors
180 try:
181 self._remote.head()
182 except KeyError:
180
181 head = self._remote.head(show_exc=False)
182 if not head:
183 183 return []
184 184
185 185 rev_filter = ['--branches', '--tags']
186 186 extra_filter = []
187 187
188 188 if filters:
189 189 if filters.get('since'):
190 190 extra_filter.append('--since=%s' % (filters['since']))
191 191 if filters.get('until'):
192 192 extra_filter.append('--until=%s' % (filters['until']))
193 193 if filters.get('branch_name'):
194 194 rev_filter = ['--tags']
195 195 extra_filter.append(filters['branch_name'])
196 196 rev_filter.extend(extra_filter)
197 197
198 198 # if filters.get('start') or filters.get('end'):
199 199 # # skip is offset, max-count is limit
200 200 # if filters.get('start'):
201 201 # extra_filter += ' --skip=%s' % filters['start']
202 202 # if filters.get('end'):
203 203 # extra_filter += ' --max-count=%s' % (filters['end'] - (filters['start'] or 0))
204 204
205 205 cmd = ['rev-list', '--reverse', '--date-order'] + rev_filter
206 206 try:
207 207 output, __ = self.run_git_command(cmd)
208 208 except RepositoryError:
209 209 # Can be raised for empty repositories
210 210 return []
211 211 return output.splitlines()
212 212
213 213 def _get_commit_id(self, commit_id_or_idx):
214 214 def is_null(value):
215 215 return len(value) == commit_id_or_idx.count('0')
216 216
217 217 if self.is_empty():
218 218 raise EmptyRepositoryError("There are no commits yet")
219 219
220 220 if commit_id_or_idx in (None, '', 'tip', 'HEAD', 'head', -1):
221 221 return self.commit_ids[-1]
222 222
223 223 is_bstr = isinstance(commit_id_or_idx, (str, unicode))
224 224 if ((is_bstr and commit_id_or_idx.isdigit() and len(commit_id_or_idx) < 12)
225 225 or isinstance(commit_id_or_idx, int) or is_null(commit_id_or_idx)):
226 226 try:
227 227 commit_id_or_idx = self.commit_ids[int(commit_id_or_idx)]
228 228 except Exception:
229 229 msg = "Commit %s does not exist for %s" % (
230 230 commit_id_or_idx, self)
231 231 raise CommitDoesNotExistError(msg)
232 232
233 233 elif is_bstr:
234 234 # check full path ref, eg. refs/heads/master
235 235 ref_id = self._refs.get(commit_id_or_idx)
236 236 if ref_id:
237 237 return ref_id
238 238
239 239 # check branch name
240 240 branch_ids = self.branches.values()
241 241 ref_id = self._refs.get('refs/heads/%s' % commit_id_or_idx)
242 242 if ref_id:
243 243 return ref_id
244 244
245 245 # check tag name
246 246 ref_id = self._refs.get('refs/tags/%s' % commit_id_or_idx)
247 247 if ref_id:
248 248 return ref_id
249 249
250 250 if (not SHA_PATTERN.match(commit_id_or_idx) or
251 251 commit_id_or_idx not in self.commit_ids):
252 252 msg = "Commit %s does not exist for %s" % (
253 253 commit_id_or_idx, self)
254 254 raise CommitDoesNotExistError(msg)
255 255
256 256 # Ensure we return full id
257 257 if not SHA_PATTERN.match(str(commit_id_or_idx)):
258 258 raise CommitDoesNotExistError(
259 259 "Given commit id %s not recognized" % commit_id_or_idx)
260 260 return commit_id_or_idx
261 261
262 262 def get_hook_location(self):
263 263 """
264 264 returns absolute path to location where hooks are stored
265 265 """
266 266 loc = os.path.join(self.path, 'hooks')
267 267 if not self.bare:
268 268 loc = os.path.join(self.path, '.git', 'hooks')
269 269 return loc
270 270
271 271 @LazyProperty
272 272 def last_change(self):
273 273 """
274 274 Returns last change made on this repository as
275 275 `datetime.datetime` object.
276 276 """
277 277 try:
278 278 return self.get_commit().date
279 279 except RepositoryError:
280 280 tzoffset = makedate()[1]
281 281 return utcdate_fromtimestamp(self._get_fs_mtime(), tzoffset)
282 282
283 283 def _get_fs_mtime(self):
284 284 idx_loc = '' if self.bare else '.git'
285 285 # fallback to filesystem
286 286 in_path = os.path.join(self.path, idx_loc, "index")
287 287 he_path = os.path.join(self.path, idx_loc, "HEAD")
288 288 if os.path.exists(in_path):
289 289 return os.stat(in_path).st_mtime
290 290 else:
291 291 return os.stat(he_path).st_mtime
292 292
293 293 @LazyProperty
294 294 def description(self):
295 295 description = self._remote.get_description()
296 296 return safe_unicode(description or self.DEFAULT_DESCRIPTION)
297 297
298 298 def _get_refs_entries(self, prefix='', reverse=False, strip_prefix=True):
299 299 if self.is_empty():
300 300 return OrderedDict()
301 301
302 302 result = []
303 303 for ref, sha in self._refs.iteritems():
304 304 if ref.startswith(prefix):
305 305 ref_name = ref
306 306 if strip_prefix:
307 307 ref_name = ref[len(prefix):]
308 308 result.append((safe_unicode(ref_name), sha))
309 309
310 310 def get_name(entry):
311 311 return entry[0]
312 312
313 313 return OrderedDict(sorted(result, key=get_name, reverse=reverse))
314 314
315 315 def _get_branches(self):
316 316 return self._get_refs_entries(prefix='refs/heads/', strip_prefix=True)
317 317
318 318 @LazyProperty
319 319 def branches(self):
320 320 return self._get_branches()
321 321
322 322 @LazyProperty
323 323 def branches_closed(self):
324 324 return {}
325 325
326 326 @LazyProperty
327 327 def bookmarks(self):
328 328 return {}
329 329
330 330 @LazyProperty
331 331 def branches_all(self):
332 332 all_branches = {}
333 333 all_branches.update(self.branches)
334 334 all_branches.update(self.branches_closed)
335 335 return all_branches
336 336
337 337 @LazyProperty
338 338 def tags(self):
339 339 return self._get_tags()
340 340
341 341 def _get_tags(self):
342 342 return self._get_refs_entries(
343 343 prefix='refs/tags/', strip_prefix=True, reverse=True)
344 344
345 345 def tag(self, name, user, commit_id=None, message=None, date=None,
346 346 **kwargs):
347 347 # TODO: fix this method to apply annotated tags correct with message
348 348 """
349 349 Creates and returns a tag for the given ``commit_id``.
350 350
351 351 :param name: name for new tag
352 352 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
353 353 :param commit_id: commit id for which new tag would be created
354 354 :param message: message of the tag's commit
355 355 :param date: date of tag's commit
356 356
357 357 :raises TagAlreadyExistError: if tag with same name already exists
358 358 """
359 359 if name in self.tags:
360 360 raise TagAlreadyExistError("Tag %s already exists" % name)
361 361 commit = self.get_commit(commit_id=commit_id)
362 362 message = message or "Added tag %s for commit %s" % (
363 363 name, commit.raw_id)
364 364 self._remote.set_refs('refs/tags/%s' % name, commit._commit['id'])
365 365
366 366 self._refs = self._get_refs()
367 367 self.tags = self._get_tags()
368 368 return commit
369 369
370 370 def remove_tag(self, name, user, message=None, date=None):
371 371 """
372 372 Removes tag with the given ``name``.
373 373
374 374 :param name: name of the tag to be removed
375 375 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
376 376 :param message: message of the tag's removal commit
377 377 :param date: date of tag's removal commit
378 378
379 379 :raises TagDoesNotExistError: if tag with given name does not exists
380 380 """
381 381 if name not in self.tags:
382 382 raise TagDoesNotExistError("Tag %s does not exist" % name)
383 383 tagpath = vcspath.join(
384 384 self._remote.get_refs_path(), 'refs', 'tags', name)
385 385 try:
386 386 os.remove(tagpath)
387 387 self._refs = self._get_refs()
388 388 self.tags = self._get_tags()
389 389 except OSError as e:
390 390 raise RepositoryError(e.strerror)
391 391
392 392 def _get_refs(self):
393 393 return self._remote.get_refs()
394 394
395 395 @LazyProperty
396 396 def _refs(self):
397 397 return self._get_refs()
398 398
399 399 @property
400 400 def _ref_tree(self):
401 401 node = tree = {}
402 402 for ref, sha in self._refs.iteritems():
403 403 path = ref.split('/')
404 404 for bit in path[:-1]:
405 405 node = node.setdefault(bit, {})
406 406 node[path[-1]] = sha
407 407 node = tree
408 408 return tree
409 409
410 410 def get_remote_ref(self, ref_name):
411 411 ref_key = 'refs/remotes/origin/{}'.format(safe_str(ref_name))
412 412 try:
413 413 return self._refs[ref_key]
414 414 except Exception:
415 415 return
416 416
417 417 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
418 418 """
419 419 Returns `GitCommit` object representing commit from git repository
420 420 at the given `commit_id` or head (most recent commit) if None given.
421 421 """
422 422 if commit_id is not None:
423 423 self._validate_commit_id(commit_id)
424 424 elif commit_idx is not None:
425 425 self._validate_commit_idx(commit_idx)
426 426 commit_id = commit_idx
427 427 commit_id = self._get_commit_id(commit_id)
428 428 try:
429 429 # Need to call remote to translate id for tagging scenario
430 430 commit_id = self._remote.get_object(commit_id)["commit_id"]
431 431 idx = self._commit_ids[commit_id]
432 432 except KeyError:
433 433 raise RepositoryError("Cannot get object with id %s" % commit_id)
434 434
435 435 return GitCommit(self, commit_id, idx, pre_load=pre_load)
436 436
437 437 def get_commits(
438 438 self, start_id=None, end_id=None, start_date=None, end_date=None,
439 439 branch_name=None, show_hidden=False, pre_load=None):
440 440 """
441 441 Returns generator of `GitCommit` objects from start to end (both
442 442 are inclusive), in ascending date order.
443 443
444 444 :param start_id: None, str(commit_id)
445 445 :param end_id: None, str(commit_id)
446 446 :param start_date: if specified, commits with commit date less than
447 447 ``start_date`` would be filtered out from returned set
448 448 :param end_date: if specified, commits with commit date greater than
449 449 ``end_date`` would be filtered out from returned set
450 450 :param branch_name: if specified, commits not reachable from given
451 451 branch would be filtered out from returned set
452 452 :param show_hidden: Show hidden commits such as obsolete or hidden from
453 453 Mercurial evolve
454 454 :raise BranchDoesNotExistError: If given `branch_name` does not
455 455 exist.
456 456 :raise CommitDoesNotExistError: If commits for given `start` or
457 457 `end` could not be found.
458 458
459 459 """
460 460 if self.is_empty():
461 461 raise EmptyRepositoryError("There are no commits yet")
462 462 self._validate_branch_name(branch_name)
463 463
464 464 if start_id is not None:
465 465 self._validate_commit_id(start_id)
466 466 if end_id is not None:
467 467 self._validate_commit_id(end_id)
468 468
469 469 start_raw_id = self._get_commit_id(start_id)
470 470 start_pos = self._commit_ids[start_raw_id] if start_id else None
471 471 end_raw_id = self._get_commit_id(end_id)
472 472 end_pos = max(0, self._commit_ids[end_raw_id]) if end_id else None
473 473
474 474 if None not in [start_id, end_id] and start_pos > end_pos:
475 475 raise RepositoryError(
476 476 "Start commit '%s' cannot be after end commit '%s'" %
477 477 (start_id, end_id))
478 478
479 479 if end_pos is not None:
480 480 end_pos += 1
481 481
482 482 filter_ = []
483 483 if branch_name:
484 484 filter_.append({'branch_name': branch_name})
485 485 if start_date and not end_date:
486 486 filter_.append({'since': start_date})
487 487 if end_date and not start_date:
488 488 filter_.append({'until': end_date})
489 489 if start_date and end_date:
490 490 filter_.append({'since': start_date})
491 491 filter_.append({'until': end_date})
492 492
493 493 # if start_pos or end_pos:
494 494 # filter_.append({'start': start_pos})
495 495 # filter_.append({'end': end_pos})
496 496
497 497 if filter_:
498 498 revfilters = {
499 499 'branch_name': branch_name,
500 500 'since': start_date.strftime('%m/%d/%y %H:%M:%S') if start_date else None,
501 501 'until': end_date.strftime('%m/%d/%y %H:%M:%S') if end_date else None,
502 502 'start': start_pos,
503 503 'end': end_pos,
504 504 }
505 505 commit_ids = self._get_all_commit_ids(filters=revfilters)
506 506
507 507 # pure python stuff, it's slow due to walker walking whole repo
508 508 # def get_revs(walker):
509 509 # for walker_entry in walker:
510 510 # yield walker_entry.commit.id
511 511 # revfilters = {}
512 512 # commit_ids = list(reversed(list(get_revs(self._repo.get_walker(**revfilters)))))
513 513 else:
514 514 commit_ids = self.commit_ids
515 515
516 516 if start_pos or end_pos:
517 517 commit_ids = commit_ids[start_pos: end_pos]
518 518
519 519 return CollectionGenerator(self, commit_ids, pre_load=pre_load)
520 520
521 521 def get_diff(
522 522 self, commit1, commit2, path='', ignore_whitespace=False,
523 523 context=3, path1=None):
524 524 """
525 525 Returns (git like) *diff*, as plain text. Shows changes introduced by
526 526 ``commit2`` since ``commit1``.
527 527
528 528 :param commit1: Entry point from which diff is shown. Can be
529 529 ``self.EMPTY_COMMIT`` - in this case, patch showing all
530 530 the changes since empty state of the repository until ``commit2``
531 531 :param commit2: Until which commits changes should be shown.
532 532 :param ignore_whitespace: If set to ``True``, would not show whitespace
533 533 changes. Defaults to ``False``.
534 534 :param context: How many lines before/after changed lines should be
535 535 shown. Defaults to ``3``.
536 536 """
537 537 self._validate_diff_commits(commit1, commit2)
538 538 if path1 is not None and path1 != path:
539 539 raise ValueError("Diff of two different paths not supported.")
540 540
541 541 flags = [
542 542 '-U%s' % context, '--full-index', '--binary', '-p',
543 543 '-M', '--abbrev=40']
544 544 if ignore_whitespace:
545 545 flags.append('-w')
546 546
547 547 if commit1 == self.EMPTY_COMMIT:
548 548 cmd = ['show'] + flags + [commit2.raw_id]
549 549 else:
550 550 cmd = ['diff'] + flags + [commit1.raw_id, commit2.raw_id]
551 551
552 552 if path:
553 553 cmd.extend(['--', path])
554 554
555 555 stdout, __ = self.run_git_command(cmd)
556 556 # If we used 'show' command, strip first few lines (until actual diff
557 557 # starts)
558 558 if commit1 == self.EMPTY_COMMIT:
559 559 lines = stdout.splitlines()
560 560 x = 0
561 561 for line in lines:
562 562 if line.startswith('diff'):
563 563 break
564 564 x += 1
565 565 # Append new line just like 'diff' command do
566 566 stdout = '\n'.join(lines[x:]) + '\n'
567 567 return GitDiff(stdout)
568 568
569 569 def strip(self, commit_id, branch_name):
570 570 commit = self.get_commit(commit_id=commit_id)
571 571 if commit.merge:
572 572 raise Exception('Cannot reset to merge commit')
573 573
574 574 # parent is going to be the new head now
575 575 commit = commit.parents[0]
576 576 self._remote.set_refs('refs/heads/%s' % branch_name, commit.raw_id)
577 577
578 578 self.commit_ids = self._get_all_commit_ids()
579 579 self._rebuild_cache(self.commit_ids)
580 580
581 581 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
582 582 if commit_id1 == commit_id2:
583 583 return commit_id1
584 584
585 585 if self != repo2:
586 586 commits = self._remote.get_missing_revs(
587 587 commit_id1, commit_id2, repo2.path)
588 588 if commits:
589 589 commit = repo2.get_commit(commits[-1])
590 590 if commit.parents:
591 591 ancestor_id = commit.parents[0].raw_id
592 592 else:
593 593 ancestor_id = None
594 594 else:
595 595 # no commits from other repo, ancestor_id is the commit_id2
596 596 ancestor_id = commit_id2
597 597 else:
598 598 output, __ = self.run_git_command(
599 599 ['merge-base', commit_id1, commit_id2])
600 600 ancestor_id = re.findall(r'[0-9a-fA-F]{40}', output)[0]
601 601
602 602 return ancestor_id
603 603
604 604 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
605 605 repo1 = self
606 606 ancestor_id = None
607 607
608 608 if commit_id1 == commit_id2:
609 609 commits = []
610 610 elif repo1 != repo2:
611 611 missing_ids = self._remote.get_missing_revs(commit_id1, commit_id2,
612 612 repo2.path)
613 613 commits = [
614 614 repo2.get_commit(commit_id=commit_id, pre_load=pre_load)
615 615 for commit_id in reversed(missing_ids)]
616 616 else:
617 617 output, __ = repo1.run_git_command(
618 618 ['log', '--reverse', '--pretty=format: %H', '-s',
619 619 '%s..%s' % (commit_id1, commit_id2)])
620 620 commits = [
621 621 repo1.get_commit(commit_id=commit_id, pre_load=pre_load)
622 622 for commit_id in re.findall(r'[0-9a-fA-F]{40}', output)]
623 623
624 624 return commits
625 625
626 626 @LazyProperty
627 627 def in_memory_commit(self):
628 628 """
629 629 Returns ``GitInMemoryCommit`` object for this repository.
630 630 """
631 631 return GitInMemoryCommit(self)
632 632
633 633 def clone(self, url, update_after_clone=True, bare=False):
634 634 """
635 635 Tries to clone commits from external location.
636 636
637 637 :param update_after_clone: If set to ``False``, git won't checkout
638 638 working directory
639 639 :param bare: If set to ``True``, repository would be cloned into
640 640 *bare* git repository (no working directory at all).
641 641 """
642 642 # init_bare and init expect empty dir created to proceed
643 643 if not os.path.exists(self.path):
644 644 os.mkdir(self.path)
645 645
646 646 if bare:
647 647 self._remote.init_bare()
648 648 else:
649 649 self._remote.init()
650 650
651 651 deferred = '^{}'
652 652 valid_refs = ('refs/heads', 'refs/tags', 'HEAD')
653 653
654 654 return self._remote.clone(
655 655 url, deferred, valid_refs, update_after_clone)
656 656
657 657 def pull(self, url, commit_ids=None):
658 658 """
659 659 Tries to pull changes from external location. We use fetch here since
660 660 pull in get does merges and we want to be compatible with hg backend so
661 661 pull == fetch in this case
662 662 """
663 663 self.fetch(url, commit_ids=commit_ids)
664 664
665 665 def fetch(self, url, commit_ids=None):
666 666 """
667 667 Tries to fetch changes from external location.
668 668 """
669 669 refs = None
670 670
671 671 if commit_ids is not None:
672 672 remote_refs = self._remote.get_remote_refs(url)
673 673 refs = [
674 674 ref for ref in remote_refs if remote_refs[ref] in commit_ids]
675 675 self._remote.fetch(url, refs=refs)
676 676
677 677 def push(self, url):
678 678 refs = None
679 679 self._remote.sync_push(url, refs=refs)
680 680
681 681 def set_refs(self, ref_name, commit_id):
682 682 self._remote.set_refs(ref_name, commit_id)
683 683
684 684 def remove_ref(self, ref_name):
685 685 self._remote.remove_ref(ref_name)
686 686
687 687 def _update_server_info(self):
688 688 """
689 689 runs gits update-server-info command in this repo instance
690 690 """
691 691 self._remote.update_server_info()
692 692
693 693 def _current_branch(self):
694 694 """
695 695 Return the name of the current branch.
696 696
697 697 It only works for non bare repositories (i.e. repositories with a
698 698 working copy)
699 699 """
700 700 if self.bare:
701 701 raise RepositoryError('Bare git repos do not have active branches')
702 702
703 703 if self.is_empty():
704 704 return None
705 705
706 706 stdout, _ = self.run_git_command(['rev-parse', '--abbrev-ref', 'HEAD'])
707 707 return stdout.strip()
708 708
709 709 def _checkout(self, branch_name, create=False, force=False):
710 710 """
711 711 Checkout a branch in the working directory.
712 712
713 713 It tries to create the branch if create is True, failing if the branch
714 714 already exists.
715 715
716 716 It only works for non bare repositories (i.e. repositories with a
717 717 working copy)
718 718 """
719 719 if self.bare:
720 720 raise RepositoryError('Cannot checkout branches in a bare git repo')
721 721
722 722 cmd = ['checkout']
723 723 if force:
724 724 cmd.append('-f')
725 725 if create:
726 726 cmd.append('-b')
727 727 cmd.append(branch_name)
728 728 self.run_git_command(cmd, fail_on_stderr=False)
729 729
730 730 def _identify(self):
731 731 """
732 732 Return the current state of the working directory.
733 733 """
734 734 if self.bare:
735 735 raise RepositoryError('Bare git repos do not have active branches')
736 736
737 737 if self.is_empty():
738 738 return None
739 739
740 740 stdout, _ = self.run_git_command(['rev-parse', 'HEAD'])
741 741 return stdout.strip()
742 742
743 743 def _local_clone(self, clone_path, branch_name, source_branch=None):
744 744 """
745 745 Create a local clone of the current repo.
746 746 """
747 747 # N.B.(skreft): the --branch option is required as otherwise the shallow
748 748 # clone will only fetch the active branch.
749 749 cmd = ['clone', '--branch', branch_name,
750 750 self.path, os.path.abspath(clone_path)]
751 751
752 752 self.run_git_command(cmd, fail_on_stderr=False)
753 753
754 754 # if we get the different source branch, make sure we also fetch it for
755 755 # merge conditions
756 756 if source_branch and source_branch != branch_name:
757 757 # check if the ref exists.
758 758 shadow_repo = GitRepository(os.path.abspath(clone_path))
759 759 if shadow_repo.get_remote_ref(source_branch):
760 760 cmd = ['fetch', self.path, source_branch]
761 761 self.run_git_command(cmd, fail_on_stderr=False)
762 762
763 763 def _local_fetch(self, repository_path, branch_name, use_origin=False):
764 764 """
765 765 Fetch a branch from a local repository.
766 766 """
767 767 repository_path = os.path.abspath(repository_path)
768 768 if repository_path == self.path:
769 769 raise ValueError('Cannot fetch from the same repository')
770 770
771 771 if use_origin:
772 772 branch_name = '+{branch}:refs/heads/{branch}'.format(
773 773 branch=branch_name)
774 774
775 775 cmd = ['fetch', '--no-tags', '--update-head-ok',
776 776 repository_path, branch_name]
777 777 self.run_git_command(cmd, fail_on_stderr=False)
778 778
779 779 def _local_reset(self, branch_name):
780 780 branch_name = '{}'.format(branch_name)
781 781 cmd = ['reset', '--hard', branch_name]
782 782 self.run_git_command(cmd, fail_on_stderr=False)
783 783
784 784 def _last_fetch_heads(self):
785 785 """
786 786 Return the last fetched heads that need merging.
787 787
788 788 The algorithm is defined at
789 789 https://github.com/git/git/blob/v2.1.3/git-pull.sh#L283
790 790 """
791 791 if not self.bare:
792 792 fetch_heads_path = os.path.join(self.path, '.git', 'FETCH_HEAD')
793 793 else:
794 794 fetch_heads_path = os.path.join(self.path, 'FETCH_HEAD')
795 795
796 796 heads = []
797 797 with open(fetch_heads_path) as f:
798 798 for line in f:
799 799 if ' not-for-merge ' in line:
800 800 continue
801 801 line = re.sub('\t.*', '', line, flags=re.DOTALL)
802 802 heads.append(line)
803 803
804 804 return heads
805 805
806 806 def _get_shadow_instance(self, shadow_repository_path, enable_hooks=False):
807 807 return GitRepository(shadow_repository_path)
808 808
809 809 def _local_pull(self, repository_path, branch_name, ff_only=True):
810 810 """
811 811 Pull a branch from a local repository.
812 812 """
813 813 if self.bare:
814 814 raise RepositoryError('Cannot pull into a bare git repository')
815 815 # N.B.(skreft): The --ff-only option is to make sure this is a
816 816 # fast-forward (i.e., we are only pulling new changes and there are no
817 817 # conflicts with our current branch)
818 818 # Additionally, that option needs to go before --no-tags, otherwise git
819 819 # pull complains about it being an unknown flag.
820 820 cmd = ['pull']
821 821 if ff_only:
822 822 cmd.append('--ff-only')
823 823 cmd.extend(['--no-tags', repository_path, branch_name])
824 824 self.run_git_command(cmd, fail_on_stderr=False)
825 825
826 826 def _local_merge(self, merge_message, user_name, user_email, heads):
827 827 """
828 828 Merge the given head into the checked out branch.
829 829
830 830 It will force a merge commit.
831 831
832 832 Currently it raises an error if the repo is empty, as it is not possible
833 833 to create a merge commit in an empty repo.
834 834
835 835 :param merge_message: The message to use for the merge commit.
836 836 :param heads: the heads to merge.
837 837 """
838 838 if self.bare:
839 839 raise RepositoryError('Cannot merge into a bare git repository')
840 840
841 841 if not heads:
842 842 return
843 843
844 844 if self.is_empty():
845 845 # TODO(skreft): do somehting more robust in this case.
846 846 raise RepositoryError(
847 847 'Do not know how to merge into empty repositories yet')
848 848
849 849 # N.B.(skreft): the --no-ff option is used to enforce the creation of a
850 850 # commit message. We also specify the user who is doing the merge.
851 851 cmd = ['-c', 'user.name="%s"' % safe_str(user_name),
852 852 '-c', 'user.email=%s' % safe_str(user_email),
853 853 'merge', '--no-ff', '-m', safe_str(merge_message)]
854 854 cmd.extend(heads)
855 855 try:
856 856 output = self.run_git_command(cmd, fail_on_stderr=False)
857 857 except RepositoryError:
858 858 # Cleanup any merge leftovers
859 859 self.run_git_command(['merge', '--abort'], fail_on_stderr=False)
860 860 raise
861 861
862 862 def _local_push(
863 863 self, source_branch, repository_path, target_branch,
864 864 enable_hooks=False, rc_scm_data=None):
865 865 """
866 866 Push the source_branch to the given repository and target_branch.
867 867
868 868 Currently it if the target_branch is not master and the target repo is
869 869 empty, the push will work, but then GitRepository won't be able to find
870 870 the pushed branch or the commits. As the HEAD will be corrupted (i.e.,
871 871 pointing to master, which does not exist).
872 872
873 873 It does not run the hooks in the target repo.
874 874 """
875 875 # TODO(skreft): deal with the case in which the target repo is empty,
876 876 # and the target_branch is not master.
877 877 target_repo = GitRepository(repository_path)
878 878 if (not target_repo.bare and
879 879 target_repo._current_branch() == target_branch):
880 880 # Git prevents pushing to the checked out branch, so simulate it by
881 881 # pulling into the target repository.
882 882 target_repo._local_pull(self.path, source_branch)
883 883 else:
884 884 cmd = ['push', os.path.abspath(repository_path),
885 885 '%s:%s' % (source_branch, target_branch)]
886 886 gitenv = {}
887 887 if rc_scm_data:
888 888 gitenv.update({'RC_SCM_DATA': rc_scm_data})
889 889
890 890 if not enable_hooks:
891 891 gitenv['RC_SKIP_HOOKS'] = '1'
892 892 self.run_git_command(cmd, fail_on_stderr=False, extra_env=gitenv)
893 893
894 894 def _get_new_pr_branch(self, source_branch, target_branch):
895 895 prefix = 'pr_%s-%s_' % (source_branch, target_branch)
896 896 pr_branches = []
897 897 for branch in self.branches:
898 898 if branch.startswith(prefix):
899 899 pr_branches.append(int(branch[len(prefix):]))
900 900
901 901 if not pr_branches:
902 902 branch_id = 0
903 903 else:
904 904 branch_id = max(pr_branches) + 1
905 905
906 906 return '%s%d' % (prefix, branch_id)
907 907
908 908 def _maybe_prepare_merge_workspace(
909 909 self, repo_id, workspace_id, target_ref, source_ref):
910 910 shadow_repository_path = self._get_shadow_repository_path(
911 911 repo_id, workspace_id)
912 912 if not os.path.exists(shadow_repository_path):
913 913 self._local_clone(
914 914 shadow_repository_path, target_ref.name, source_ref.name)
915 915 log.debug(
916 916 'Prepared shadow repository in %s', shadow_repository_path)
917 917
918 918 return shadow_repository_path
919 919
920 920 def _merge_repo(self, repo_id, workspace_id, target_ref,
921 921 source_repo, source_ref, merge_message,
922 922 merger_name, merger_email, dry_run=False,
923 923 use_rebase=False, close_branch=False):
924 924 if target_ref.commit_id != self.branches[target_ref.name]:
925 925 log.warning('Target ref %s commit mismatch %s vs %s', target_ref,
926 926 target_ref.commit_id, self.branches[target_ref.name])
927 927 return MergeResponse(
928 928 False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD)
929 929
930 930 shadow_repository_path = self._maybe_prepare_merge_workspace(
931 931 repo_id, workspace_id, target_ref, source_ref)
932 932 shadow_repo = self._get_shadow_instance(shadow_repository_path)
933 933
934 934 # checkout source, if it's different. Otherwise we could not
935 935 # fetch proper commits for merge testing
936 936 if source_ref.name != target_ref.name:
937 937 if shadow_repo.get_remote_ref(source_ref.name):
938 938 shadow_repo._checkout(source_ref.name, force=True)
939 939
940 940 # checkout target, and fetch changes
941 941 shadow_repo._checkout(target_ref.name, force=True)
942 942
943 943 # fetch/reset pull the target, in case it is changed
944 944 # this handles even force changes
945 945 shadow_repo._local_fetch(self.path, target_ref.name, use_origin=True)
946 946 shadow_repo._local_reset(target_ref.name)
947 947
948 948 # Need to reload repo to invalidate the cache, or otherwise we cannot
949 949 # retrieve the last target commit.
950 950 shadow_repo = self._get_shadow_instance(shadow_repository_path)
951 951 if target_ref.commit_id != shadow_repo.branches[target_ref.name]:
952 952 log.warning('Shadow Target ref %s commit mismatch %s vs %s',
953 953 target_ref, target_ref.commit_id,
954 954 shadow_repo.branches[target_ref.name])
955 955 return MergeResponse(
956 956 False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD)
957 957
958 958 # calculate new branch
959 959 pr_branch = shadow_repo._get_new_pr_branch(
960 960 source_ref.name, target_ref.name)
961 961 log.debug('using pull-request merge branch: `%s`', pr_branch)
962 962 # checkout to temp branch, and fetch changes
963 963 shadow_repo._checkout(pr_branch, create=True)
964 964 try:
965 965 shadow_repo._local_fetch(source_repo.path, source_ref.name)
966 966 except RepositoryError:
967 967 log.exception('Failure when doing local fetch on git shadow repo')
968 968 return MergeResponse(
969 969 False, False, None, MergeFailureReason.MISSING_SOURCE_REF)
970 970
971 971 merge_ref = None
972 972 merge_failure_reason = MergeFailureReason.NONE
973 973 try:
974 974 shadow_repo._local_merge(merge_message, merger_name, merger_email,
975 975 [source_ref.commit_id])
976 976 merge_possible = True
977 977
978 978 # Need to reload repo to invalidate the cache, or otherwise we
979 979 # cannot retrieve the merge commit.
980 980 shadow_repo = GitRepository(shadow_repository_path)
981 981 merge_commit_id = shadow_repo.branches[pr_branch]
982 982
983 983 # Set a reference pointing to the merge commit. This reference may
984 984 # be used to easily identify the last successful merge commit in
985 985 # the shadow repository.
986 986 shadow_repo.set_refs('refs/heads/pr-merge', merge_commit_id)
987 987 merge_ref = Reference('branch', 'pr-merge', merge_commit_id)
988 988 except RepositoryError:
989 989 log.exception('Failure when doing local merge on git shadow repo')
990 990 merge_possible = False
991 991 merge_failure_reason = MergeFailureReason.MERGE_FAILED
992 992
993 993 if merge_possible and not dry_run:
994 994 try:
995 995 shadow_repo._local_push(
996 996 pr_branch, self.path, target_ref.name, enable_hooks=True,
997 997 rc_scm_data=self.config.get('rhodecode', 'RC_SCM_DATA'))
998 998 merge_succeeded = True
999 999 except RepositoryError:
1000 1000 log.exception(
1001 1001 'Failure when doing local push on git shadow repo')
1002 1002 merge_succeeded = False
1003 1003 merge_failure_reason = MergeFailureReason.PUSH_FAILED
1004 1004 else:
1005 1005 merge_succeeded = False
1006 1006
1007 1007 return MergeResponse(
1008 1008 merge_possible, merge_succeeded, merge_ref,
1009 1009 merge_failure_reason)
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now