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