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