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