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