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