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