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