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