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