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