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