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