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