##// END OF EJS Templates
git: use bool condition to check if the git path is fine. Using exceptions...
marcink -
r2452:c74b82e7 default
parent child Browse files
Show More
@@ -1,947 +1,951 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2014-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 GIT repository module
23 23 """
24 24
25 25 import logging
26 26 import os
27 27 import re
28 28 import shutil
29 29
30 30 from zope.cachedescriptors.property import Lazy as LazyProperty
31 31
32 32 from rhodecode.lib.compat import OrderedDict
33 33 from rhodecode.lib.datelib import (
34 34 utcdate_fromtimestamp, makedate, date_astimestamp)
35 35 from rhodecode.lib.utils import safe_unicode, safe_str
36 36 from rhodecode.lib.vcs import connection, path as vcspath
37 37 from rhodecode.lib.vcs.backends.base import (
38 38 BaseRepository, CollectionGenerator, Config, MergeResponse,
39 39 MergeFailureReason, Reference)
40 40 from rhodecode.lib.vcs.backends.git.commit import GitCommit
41 41 from rhodecode.lib.vcs.backends.git.diff import GitDiff
42 42 from rhodecode.lib.vcs.backends.git.inmemory import GitInMemoryCommit
43 43 from rhodecode.lib.vcs.exceptions import (
44 44 CommitDoesNotExistError, EmptyRepositoryError,
45 45 RepositoryError, TagAlreadyExistError, TagDoesNotExistError, VCSError)
46 46
47 47
48 48 SHA_PATTERN = re.compile(r'^[[0-9a-fA-F]{12}|[0-9a-fA-F]{40}]$')
49 49
50 50 log = logging.getLogger(__name__)
51 51
52 52
53 53 class GitRepository(BaseRepository):
54 54 """
55 55 Git repository backend.
56 56 """
57 57 DEFAULT_BRANCH_NAME = 'master'
58 58
59 59 contact = BaseRepository.DEFAULT_CONTACT
60 60
61 61 def __init__(self, repo_path, config=None, create=False, src_url=None,
62 62 update_after_clone=False, with_wire=None, bare=False):
63 63
64 64 self.path = safe_str(os.path.abspath(repo_path))
65 65 self.config = config if config else Config()
66 66 self._remote = connection.Git(
67 67 self.path, self.config, with_wire=with_wire)
68 68
69 69 self._init_repo(create, src_url, update_after_clone, bare)
70 70
71 71 # caches
72 72 self._commit_ids = {}
73 73
74 74 self.bookmarks = {}
75 75
76 76 @LazyProperty
77 77 def bare(self):
78 78 return self._remote.bare()
79 79
80 80 @LazyProperty
81 81 def head(self):
82 82 return self._remote.head()
83 83
84 84 @LazyProperty
85 85 def commit_ids(self):
86 86 """
87 87 Returns list of commit ids, in ascending order. Being lazy
88 88 attribute allows external tools to inject commit ids from cache.
89 89 """
90 90 commit_ids = self._get_all_commit_ids()
91 91 self._rebuild_cache(commit_ids)
92 92 return commit_ids
93 93
94 94 def _rebuild_cache(self, commit_ids):
95 95 self._commit_ids = dict((commit_id, index)
96 96 for index, commit_id in enumerate(commit_ids))
97 97
98 98 def run_git_command(self, cmd, **opts):
99 99 """
100 100 Runs given ``cmd`` as git command and returns tuple
101 101 (stdout, stderr).
102 102
103 103 :param cmd: git command to be executed
104 104 :param opts: env options to pass into Subprocess command
105 105 """
106 106 if not isinstance(cmd, list):
107 107 raise ValueError('cmd must be a list, got %s instead' % type(cmd))
108 108
109 109 out, err = self._remote.run_git_command(cmd, **opts)
110 110 if err:
111 111 log.debug('Stderr output of git command "%s":\n%s', cmd, err)
112 112 return out, err
113 113
114 114 @staticmethod
115 115 def check_url(url, config):
116 116 """
117 117 Function will check given url and try to verify if it's a valid
118 118 link. Sometimes it may happened that git will issue basic
119 119 auth request that can cause whole API to hang when used from python
120 120 or other external calls.
121 121
122 122 On failures it'll raise urllib2.HTTPError, exception is also thrown
123 123 when the return code is non 200
124 124 """
125 125 # check first if it's not an url
126 126 if os.path.isdir(url) or url.startswith('file:'):
127 127 return True
128 128
129 129 if '+' in url.split('://', 1)[0]:
130 130 url = url.split('+', 1)[1]
131 131
132 132 # Request the _remote to verify the url
133 133 return connection.Git.check_url(url, config.serialize())
134 134
135 135 @staticmethod
136 136 def is_valid_repository(path):
137 137 if os.path.isdir(os.path.join(path, '.git')):
138 138 return True
139 139 # check case of bare repository
140 140 try:
141 141 GitRepository(path)
142 142 return True
143 143 except VCSError:
144 144 pass
145 145 return False
146 146
147 147 def _init_repo(self, create, src_url=None, update_after_clone=False,
148 148 bare=False):
149 149 if create and os.path.exists(self.path):
150 150 raise RepositoryError(
151 151 "Cannot create repository at %s, location already exist"
152 152 % self.path)
153 153
154 154 try:
155 155 if create and src_url:
156 156 GitRepository.check_url(src_url, self.config)
157 157 self.clone(src_url, update_after_clone, bare)
158 158 elif create:
159 159 os.makedirs(self.path, mode=0755)
160 160
161 161 if bare:
162 162 self._remote.init_bare()
163 163 else:
164 164 self._remote.init()
165 165 else:
166 self._remote.assert_correct_path()
166 if not self._remote.assert_correct_path():
167 raise RepositoryError(
168 'Path "%s" does not contain a Git repository' %
169 (self.path,))
170
167 171 # TODO: johbo: check if we have to translate the OSError here
168 172 except OSError as err:
169 173 raise RepositoryError(err)
170 174
171 175 def _get_all_commit_ids(self, filters=None):
172 176 # we must check if this repo is not empty, since later command
173 177 # fails if it is. And it's cheaper to ask than throw the subprocess
174 178 # errors
175 179 try:
176 180 self._remote.head()
177 181 except KeyError:
178 182 return []
179 183
180 184 rev_filter = ['--branches', '--tags']
181 185 extra_filter = []
182 186
183 187 if filters:
184 188 if filters.get('since'):
185 189 extra_filter.append('--since=%s' % (filters['since']))
186 190 if filters.get('until'):
187 191 extra_filter.append('--until=%s' % (filters['until']))
188 192 if filters.get('branch_name'):
189 193 rev_filter = ['--tags']
190 194 extra_filter.append(filters['branch_name'])
191 195 rev_filter.extend(extra_filter)
192 196
193 197 # if filters.get('start') or filters.get('end'):
194 198 # # skip is offset, max-count is limit
195 199 # if filters.get('start'):
196 200 # extra_filter += ' --skip=%s' % filters['start']
197 201 # if filters.get('end'):
198 202 # extra_filter += ' --max-count=%s' % (filters['end'] - (filters['start'] or 0))
199 203
200 204 cmd = ['rev-list', '--reverse', '--date-order'] + rev_filter
201 205 try:
202 206 output, __ = self.run_git_command(cmd)
203 207 except RepositoryError:
204 208 # Can be raised for empty repositories
205 209 return []
206 210 return output.splitlines()
207 211
208 212 def _get_commit_id(self, commit_id_or_idx):
209 213 def is_null(value):
210 214 return len(value) == commit_id_or_idx.count('0')
211 215
212 216 if self.is_empty():
213 217 raise EmptyRepositoryError("There are no commits yet")
214 218
215 219 if commit_id_or_idx in (None, '', 'tip', 'HEAD', 'head', -1):
216 220 return self.commit_ids[-1]
217 221
218 222 is_bstr = isinstance(commit_id_or_idx, (str, unicode))
219 223 if ((is_bstr and commit_id_or_idx.isdigit() and len(commit_id_or_idx) < 12)
220 224 or isinstance(commit_id_or_idx, int) or is_null(commit_id_or_idx)):
221 225 try:
222 226 commit_id_or_idx = self.commit_ids[int(commit_id_or_idx)]
223 227 except Exception:
224 228 msg = "Commit %s does not exist for %s" % (
225 229 commit_id_or_idx, self)
226 230 raise CommitDoesNotExistError(msg)
227 231
228 232 elif is_bstr:
229 233 # check full path ref, eg. refs/heads/master
230 234 ref_id = self._refs.get(commit_id_or_idx)
231 235 if ref_id:
232 236 return ref_id
233 237
234 238 # check branch name
235 239 branch_ids = self.branches.values()
236 240 ref_id = self._refs.get('refs/heads/%s' % commit_id_or_idx)
237 241 if ref_id:
238 242 return ref_id
239 243
240 244 # check tag name
241 245 ref_id = self._refs.get('refs/tags/%s' % commit_id_or_idx)
242 246 if ref_id:
243 247 return ref_id
244 248
245 249 if (not SHA_PATTERN.match(commit_id_or_idx) or
246 250 commit_id_or_idx not in self.commit_ids):
247 251 msg = "Commit %s does not exist for %s" % (
248 252 commit_id_or_idx, self)
249 253 raise CommitDoesNotExistError(msg)
250 254
251 255 # Ensure we return full id
252 256 if not SHA_PATTERN.match(str(commit_id_or_idx)):
253 257 raise CommitDoesNotExistError(
254 258 "Given commit id %s not recognized" % commit_id_or_idx)
255 259 return commit_id_or_idx
256 260
257 261 def get_hook_location(self):
258 262 """
259 263 returns absolute path to location where hooks are stored
260 264 """
261 265 loc = os.path.join(self.path, 'hooks')
262 266 if not self.bare:
263 267 loc = os.path.join(self.path, '.git', 'hooks')
264 268 return loc
265 269
266 270 @LazyProperty
267 271 def last_change(self):
268 272 """
269 273 Returns last change made on this repository as
270 274 `datetime.datetime` object.
271 275 """
272 276 try:
273 277 return self.get_commit().date
274 278 except RepositoryError:
275 279 tzoffset = makedate()[1]
276 280 return utcdate_fromtimestamp(self._get_fs_mtime(), tzoffset)
277 281
278 282 def _get_fs_mtime(self):
279 283 idx_loc = '' if self.bare else '.git'
280 284 # fallback to filesystem
281 285 in_path = os.path.join(self.path, idx_loc, "index")
282 286 he_path = os.path.join(self.path, idx_loc, "HEAD")
283 287 if os.path.exists(in_path):
284 288 return os.stat(in_path).st_mtime
285 289 else:
286 290 return os.stat(he_path).st_mtime
287 291
288 292 @LazyProperty
289 293 def description(self):
290 294 description = self._remote.get_description()
291 295 return safe_unicode(description or self.DEFAULT_DESCRIPTION)
292 296
293 297 def _get_refs_entries(self, prefix='', reverse=False, strip_prefix=True):
294 298 if self.is_empty():
295 299 return OrderedDict()
296 300
297 301 result = []
298 302 for ref, sha in self._refs.iteritems():
299 303 if ref.startswith(prefix):
300 304 ref_name = ref
301 305 if strip_prefix:
302 306 ref_name = ref[len(prefix):]
303 307 result.append((safe_unicode(ref_name), sha))
304 308
305 309 def get_name(entry):
306 310 return entry[0]
307 311
308 312 return OrderedDict(sorted(result, key=get_name, reverse=reverse))
309 313
310 314 def _get_branches(self):
311 315 return self._get_refs_entries(prefix='refs/heads/', strip_prefix=True)
312 316
313 317 @LazyProperty
314 318 def branches(self):
315 319 return self._get_branches()
316 320
317 321 @LazyProperty
318 322 def branches_closed(self):
319 323 return {}
320 324
321 325 @LazyProperty
322 326 def branches_all(self):
323 327 all_branches = {}
324 328 all_branches.update(self.branches)
325 329 all_branches.update(self.branches_closed)
326 330 return all_branches
327 331
328 332 @LazyProperty
329 333 def tags(self):
330 334 return self._get_tags()
331 335
332 336 def _get_tags(self):
333 337 return self._get_refs_entries(
334 338 prefix='refs/tags/', strip_prefix=True, reverse=True)
335 339
336 340 def tag(self, name, user, commit_id=None, message=None, date=None,
337 341 **kwargs):
338 342 # TODO: fix this method to apply annotated tags correct with message
339 343 """
340 344 Creates and returns a tag for the given ``commit_id``.
341 345
342 346 :param name: name for new tag
343 347 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
344 348 :param commit_id: commit id for which new tag would be created
345 349 :param message: message of the tag's commit
346 350 :param date: date of tag's commit
347 351
348 352 :raises TagAlreadyExistError: if tag with same name already exists
349 353 """
350 354 if name in self.tags:
351 355 raise TagAlreadyExistError("Tag %s already exists" % name)
352 356 commit = self.get_commit(commit_id=commit_id)
353 357 message = message or "Added tag %s for commit %s" % (
354 358 name, commit.raw_id)
355 359 self._remote.set_refs('refs/tags/%s' % name, commit._commit['id'])
356 360
357 361 self._refs = self._get_refs()
358 362 self.tags = self._get_tags()
359 363 return commit
360 364
361 365 def remove_tag(self, name, user, message=None, date=None):
362 366 """
363 367 Removes tag with the given ``name``.
364 368
365 369 :param name: name of the tag to be removed
366 370 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
367 371 :param message: message of the tag's removal commit
368 372 :param date: date of tag's removal commit
369 373
370 374 :raises TagDoesNotExistError: if tag with given name does not exists
371 375 """
372 376 if name not in self.tags:
373 377 raise TagDoesNotExistError("Tag %s does not exist" % name)
374 378 tagpath = vcspath.join(
375 379 self._remote.get_refs_path(), 'refs', 'tags', name)
376 380 try:
377 381 os.remove(tagpath)
378 382 self._refs = self._get_refs()
379 383 self.tags = self._get_tags()
380 384 except OSError as e:
381 385 raise RepositoryError(e.strerror)
382 386
383 387 def _get_refs(self):
384 388 return self._remote.get_refs()
385 389
386 390 @LazyProperty
387 391 def _refs(self):
388 392 return self._get_refs()
389 393
390 394 @property
391 395 def _ref_tree(self):
392 396 node = tree = {}
393 397 for ref, sha in self._refs.iteritems():
394 398 path = ref.split('/')
395 399 for bit in path[:-1]:
396 400 node = node.setdefault(bit, {})
397 401 node[path[-1]] = sha
398 402 node = tree
399 403 return tree
400 404
401 405 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
402 406 """
403 407 Returns `GitCommit` object representing commit from git repository
404 408 at the given `commit_id` or head (most recent commit) if None given.
405 409 """
406 410 if commit_id is not None:
407 411 self._validate_commit_id(commit_id)
408 412 elif commit_idx is not None:
409 413 self._validate_commit_idx(commit_idx)
410 414 commit_id = commit_idx
411 415 commit_id = self._get_commit_id(commit_id)
412 416 try:
413 417 # Need to call remote to translate id for tagging scenario
414 418 commit_id = self._remote.get_object(commit_id)["commit_id"]
415 419 idx = self._commit_ids[commit_id]
416 420 except KeyError:
417 421 raise RepositoryError("Cannot get object with id %s" % commit_id)
418 422
419 423 return GitCommit(self, commit_id, idx, pre_load=pre_load)
420 424
421 425 def get_commits(
422 426 self, start_id=None, end_id=None, start_date=None, end_date=None,
423 427 branch_name=None, show_hidden=False, pre_load=None):
424 428 """
425 429 Returns generator of `GitCommit` objects from start to end (both
426 430 are inclusive), in ascending date order.
427 431
428 432 :param start_id: None, str(commit_id)
429 433 :param end_id: None, str(commit_id)
430 434 :param start_date: if specified, commits with commit date less than
431 435 ``start_date`` would be filtered out from returned set
432 436 :param end_date: if specified, commits with commit date greater than
433 437 ``end_date`` would be filtered out from returned set
434 438 :param branch_name: if specified, commits not reachable from given
435 439 branch would be filtered out from returned set
436 440 :param show_hidden: Show hidden commits such as obsolete or hidden from
437 441 Mercurial evolve
438 442 :raise BranchDoesNotExistError: If given `branch_name` does not
439 443 exist.
440 444 :raise CommitDoesNotExistError: If commits for given `start` or
441 445 `end` could not be found.
442 446
443 447 """
444 448 if self.is_empty():
445 449 raise EmptyRepositoryError("There are no commits yet")
446 450 self._validate_branch_name(branch_name)
447 451
448 452 if start_id is not None:
449 453 self._validate_commit_id(start_id)
450 454 if end_id is not None:
451 455 self._validate_commit_id(end_id)
452 456
453 457 start_raw_id = self._get_commit_id(start_id)
454 458 start_pos = self._commit_ids[start_raw_id] if start_id else None
455 459 end_raw_id = self._get_commit_id(end_id)
456 460 end_pos = max(0, self._commit_ids[end_raw_id]) if end_id else None
457 461
458 462 if None not in [start_id, end_id] and start_pos > end_pos:
459 463 raise RepositoryError(
460 464 "Start commit '%s' cannot be after end commit '%s'" %
461 465 (start_id, end_id))
462 466
463 467 if end_pos is not None:
464 468 end_pos += 1
465 469
466 470 filter_ = []
467 471 if branch_name:
468 472 filter_.append({'branch_name': branch_name})
469 473 if start_date and not end_date:
470 474 filter_.append({'since': start_date})
471 475 if end_date and not start_date:
472 476 filter_.append({'until': end_date})
473 477 if start_date and end_date:
474 478 filter_.append({'since': start_date})
475 479 filter_.append({'until': end_date})
476 480
477 481 # if start_pos or end_pos:
478 482 # filter_.append({'start': start_pos})
479 483 # filter_.append({'end': end_pos})
480 484
481 485 if filter_:
482 486 revfilters = {
483 487 'branch_name': branch_name,
484 488 'since': start_date.strftime('%m/%d/%y %H:%M:%S') if start_date else None,
485 489 'until': end_date.strftime('%m/%d/%y %H:%M:%S') if end_date else None,
486 490 'start': start_pos,
487 491 'end': end_pos,
488 492 }
489 493 commit_ids = self._get_all_commit_ids(filters=revfilters)
490 494
491 495 # pure python stuff, it's slow due to walker walking whole repo
492 496 # def get_revs(walker):
493 497 # for walker_entry in walker:
494 498 # yield walker_entry.commit.id
495 499 # revfilters = {}
496 500 # commit_ids = list(reversed(list(get_revs(self._repo.get_walker(**revfilters)))))
497 501 else:
498 502 commit_ids = self.commit_ids
499 503
500 504 if start_pos or end_pos:
501 505 commit_ids = commit_ids[start_pos: end_pos]
502 506
503 507 return CollectionGenerator(self, commit_ids, pre_load=pre_load)
504 508
505 509 def get_diff(
506 510 self, commit1, commit2, path='', ignore_whitespace=False,
507 511 context=3, path1=None):
508 512 """
509 513 Returns (git like) *diff*, as plain text. Shows changes introduced by
510 514 ``commit2`` since ``commit1``.
511 515
512 516 :param commit1: Entry point from which diff is shown. Can be
513 517 ``self.EMPTY_COMMIT`` - in this case, patch showing all
514 518 the changes since empty state of the repository until ``commit2``
515 519 :param commit2: Until which commits changes should be shown.
516 520 :param ignore_whitespace: If set to ``True``, would not show whitespace
517 521 changes. Defaults to ``False``.
518 522 :param context: How many lines before/after changed lines should be
519 523 shown. Defaults to ``3``.
520 524 """
521 525 self._validate_diff_commits(commit1, commit2)
522 526 if path1 is not None and path1 != path:
523 527 raise ValueError("Diff of two different paths not supported.")
524 528
525 529 flags = [
526 530 '-U%s' % context, '--full-index', '--binary', '-p',
527 531 '-M', '--abbrev=40']
528 532 if ignore_whitespace:
529 533 flags.append('-w')
530 534
531 535 if commit1 == self.EMPTY_COMMIT:
532 536 cmd = ['show'] + flags + [commit2.raw_id]
533 537 else:
534 538 cmd = ['diff'] + flags + [commit1.raw_id, commit2.raw_id]
535 539
536 540 if path:
537 541 cmd.extend(['--', path])
538 542
539 543 stdout, __ = self.run_git_command(cmd)
540 544 # If we used 'show' command, strip first few lines (until actual diff
541 545 # starts)
542 546 if commit1 == self.EMPTY_COMMIT:
543 547 lines = stdout.splitlines()
544 548 x = 0
545 549 for line in lines:
546 550 if line.startswith('diff'):
547 551 break
548 552 x += 1
549 553 # Append new line just like 'diff' command do
550 554 stdout = '\n'.join(lines[x:]) + '\n'
551 555 return GitDiff(stdout)
552 556
553 557 def strip(self, commit_id, branch_name):
554 558 commit = self.get_commit(commit_id=commit_id)
555 559 if commit.merge:
556 560 raise Exception('Cannot reset to merge commit')
557 561
558 562 # parent is going to be the new head now
559 563 commit = commit.parents[0]
560 564 self._remote.set_refs('refs/heads/%s' % branch_name, commit.raw_id)
561 565
562 566 self.commit_ids = self._get_all_commit_ids()
563 567 self._rebuild_cache(self.commit_ids)
564 568
565 569 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
566 570 if commit_id1 == commit_id2:
567 571 return commit_id1
568 572
569 573 if self != repo2:
570 574 commits = self._remote.get_missing_revs(
571 575 commit_id1, commit_id2, repo2.path)
572 576 if commits:
573 577 commit = repo2.get_commit(commits[-1])
574 578 if commit.parents:
575 579 ancestor_id = commit.parents[0].raw_id
576 580 else:
577 581 ancestor_id = None
578 582 else:
579 583 # no commits from other repo, ancestor_id is the commit_id2
580 584 ancestor_id = commit_id2
581 585 else:
582 586 output, __ = self.run_git_command(
583 587 ['merge-base', commit_id1, commit_id2])
584 588 ancestor_id = re.findall(r'[0-9a-fA-F]{40}', output)[0]
585 589
586 590 return ancestor_id
587 591
588 592 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
589 593 repo1 = self
590 594 ancestor_id = None
591 595
592 596 if commit_id1 == commit_id2:
593 597 commits = []
594 598 elif repo1 != repo2:
595 599 missing_ids = self._remote.get_missing_revs(commit_id1, commit_id2,
596 600 repo2.path)
597 601 commits = [
598 602 repo2.get_commit(commit_id=commit_id, pre_load=pre_load)
599 603 for commit_id in reversed(missing_ids)]
600 604 else:
601 605 output, __ = repo1.run_git_command(
602 606 ['log', '--reverse', '--pretty=format: %H', '-s',
603 607 '%s..%s' % (commit_id1, commit_id2)])
604 608 commits = [
605 609 repo1.get_commit(commit_id=commit_id, pre_load=pre_load)
606 610 for commit_id in re.findall(r'[0-9a-fA-F]{40}', output)]
607 611
608 612 return commits
609 613
610 614 @LazyProperty
611 615 def in_memory_commit(self):
612 616 """
613 617 Returns ``GitInMemoryCommit`` object for this repository.
614 618 """
615 619 return GitInMemoryCommit(self)
616 620
617 621 def clone(self, url, update_after_clone=True, bare=False):
618 622 """
619 623 Tries to clone commits from external location.
620 624
621 625 :param update_after_clone: If set to ``False``, git won't checkout
622 626 working directory
623 627 :param bare: If set to ``True``, repository would be cloned into
624 628 *bare* git repository (no working directory at all).
625 629 """
626 630 # init_bare and init expect empty dir created to proceed
627 631 if not os.path.exists(self.path):
628 632 os.mkdir(self.path)
629 633
630 634 if bare:
631 635 self._remote.init_bare()
632 636 else:
633 637 self._remote.init()
634 638
635 639 deferred = '^{}'
636 640 valid_refs = ('refs/heads', 'refs/tags', 'HEAD')
637 641
638 642 return self._remote.clone(
639 643 url, deferred, valid_refs, update_after_clone)
640 644
641 645 def pull(self, url, commit_ids=None):
642 646 """
643 647 Tries to pull changes from external location. We use fetch here since
644 648 pull in get does merges and we want to be compatible with hg backend so
645 649 pull == fetch in this case
646 650 """
647 651 self.fetch(url, commit_ids=commit_ids)
648 652
649 653 def fetch(self, url, commit_ids=None):
650 654 """
651 655 Tries to fetch changes from external location.
652 656 """
653 657 refs = None
654 658
655 659 if commit_ids is not None:
656 660 remote_refs = self._remote.get_remote_refs(url)
657 661 refs = [
658 662 ref for ref in remote_refs if remote_refs[ref] in commit_ids]
659 663 self._remote.fetch(url, refs=refs)
660 664
661 665 def set_refs(self, ref_name, commit_id):
662 666 self._remote.set_refs(ref_name, commit_id)
663 667
664 668 def remove_ref(self, ref_name):
665 669 self._remote.remove_ref(ref_name)
666 670
667 671 def _update_server_info(self):
668 672 """
669 673 runs gits update-server-info command in this repo instance
670 674 """
671 675 self._remote.update_server_info()
672 676
673 677 def _current_branch(self):
674 678 """
675 679 Return the name of the current branch.
676 680
677 681 It only works for non bare repositories (i.e. repositories with a
678 682 working copy)
679 683 """
680 684 if self.bare:
681 685 raise RepositoryError('Bare git repos do not have active branches')
682 686
683 687 if self.is_empty():
684 688 return None
685 689
686 690 stdout, _ = self.run_git_command(['rev-parse', '--abbrev-ref', 'HEAD'])
687 691 return stdout.strip()
688 692
689 693 def _checkout(self, branch_name, create=False):
690 694 """
691 695 Checkout a branch in the working directory.
692 696
693 697 It tries to create the branch if create is True, failing if the branch
694 698 already exists.
695 699
696 700 It only works for non bare repositories (i.e. repositories with a
697 701 working copy)
698 702 """
699 703 if self.bare:
700 704 raise RepositoryError('Cannot checkout branches in a bare git repo')
701 705
702 706 cmd = ['checkout']
703 707 if create:
704 708 cmd.append('-b')
705 709 cmd.append(branch_name)
706 710 self.run_git_command(cmd, fail_on_stderr=False)
707 711
708 712 def _identify(self):
709 713 """
710 714 Return the current state of the working directory.
711 715 """
712 716 if self.bare:
713 717 raise RepositoryError('Bare git repos do not have active branches')
714 718
715 719 if self.is_empty():
716 720 return None
717 721
718 722 stdout, _ = self.run_git_command(['rev-parse', 'HEAD'])
719 723 return stdout.strip()
720 724
721 725 def _local_clone(self, clone_path, branch_name):
722 726 """
723 727 Create a local clone of the current repo.
724 728 """
725 729 # N.B.(skreft): the --branch option is required as otherwise the shallow
726 730 # clone will only fetch the active branch.
727 731 cmd = ['clone', '--branch', branch_name, '--single-branch',
728 732 self.path, os.path.abspath(clone_path)]
729 733 self.run_git_command(cmd, fail_on_stderr=False)
730 734
731 735 def _local_fetch(self, repository_path, branch_name):
732 736 """
733 737 Fetch a branch from a local repository.
734 738 """
735 739 repository_path = os.path.abspath(repository_path)
736 740 if repository_path == self.path:
737 741 raise ValueError('Cannot fetch from the same repository')
738 742
739 743 cmd = ['fetch', '--no-tags', repository_path, branch_name]
740 744 self.run_git_command(cmd, fail_on_stderr=False)
741 745
742 746 def _last_fetch_heads(self):
743 747 """
744 748 Return the last fetched heads that need merging.
745 749
746 750 The algorithm is defined at
747 751 https://github.com/git/git/blob/v2.1.3/git-pull.sh#L283
748 752 """
749 753 if not self.bare:
750 754 fetch_heads_path = os.path.join(self.path, '.git', 'FETCH_HEAD')
751 755 else:
752 756 fetch_heads_path = os.path.join(self.path, 'FETCH_HEAD')
753 757
754 758 heads = []
755 759 with open(fetch_heads_path) as f:
756 760 for line in f:
757 761 if ' not-for-merge ' in line:
758 762 continue
759 763 line = re.sub('\t.*', '', line, flags=re.DOTALL)
760 764 heads.append(line)
761 765
762 766 return heads
763 767
764 768 def _get_shadow_instance(self, shadow_repository_path, enable_hooks=False):
765 769 return GitRepository(shadow_repository_path)
766 770
767 771 def _local_pull(self, repository_path, branch_name):
768 772 """
769 773 Pull a branch from a local repository.
770 774 """
771 775 if self.bare:
772 776 raise RepositoryError('Cannot pull into a bare git repository')
773 777 # N.B.(skreft): The --ff-only option is to make sure this is a
774 778 # fast-forward (i.e., we are only pulling new changes and there are no
775 779 # conflicts with our current branch)
776 780 # Additionally, that option needs to go before --no-tags, otherwise git
777 781 # pull complains about it being an unknown flag.
778 782 cmd = ['pull', '--ff-only', '--no-tags', repository_path, branch_name]
779 783 self.run_git_command(cmd, fail_on_stderr=False)
780 784
781 785 def _local_merge(self, merge_message, user_name, user_email, heads):
782 786 """
783 787 Merge the given head into the checked out branch.
784 788
785 789 It will force a merge commit.
786 790
787 791 Currently it raises an error if the repo is empty, as it is not possible
788 792 to create a merge commit in an empty repo.
789 793
790 794 :param merge_message: The message to use for the merge commit.
791 795 :param heads: the heads to merge.
792 796 """
793 797 if self.bare:
794 798 raise RepositoryError('Cannot merge into a bare git repository')
795 799
796 800 if not heads:
797 801 return
798 802
799 803 if self.is_empty():
800 804 # TODO(skreft): do somehting more robust in this case.
801 805 raise RepositoryError(
802 806 'Do not know how to merge into empty repositories yet')
803 807
804 808 # N.B.(skreft): the --no-ff option is used to enforce the creation of a
805 809 # commit message. We also specify the user who is doing the merge.
806 810 cmd = ['-c', 'user.name=%s' % safe_str(user_name),
807 811 '-c', 'user.email=%s' % safe_str(user_email),
808 812 'merge', '--no-ff', '-m', safe_str(merge_message)]
809 813 cmd.extend(heads)
810 814 try:
811 815 self.run_git_command(cmd, fail_on_stderr=False)
812 816 except RepositoryError:
813 817 # Cleanup any merge leftovers
814 818 self.run_git_command(['merge', '--abort'], fail_on_stderr=False)
815 819 raise
816 820
817 821 def _local_push(
818 822 self, source_branch, repository_path, target_branch,
819 823 enable_hooks=False, rc_scm_data=None):
820 824 """
821 825 Push the source_branch to the given repository and target_branch.
822 826
823 827 Currently it if the target_branch is not master and the target repo is
824 828 empty, the push will work, but then GitRepository won't be able to find
825 829 the pushed branch or the commits. As the HEAD will be corrupted (i.e.,
826 830 pointing to master, which does not exist).
827 831
828 832 It does not run the hooks in the target repo.
829 833 """
830 834 # TODO(skreft): deal with the case in which the target repo is empty,
831 835 # and the target_branch is not master.
832 836 target_repo = GitRepository(repository_path)
833 837 if (not target_repo.bare and
834 838 target_repo._current_branch() == target_branch):
835 839 # Git prevents pushing to the checked out branch, so simulate it by
836 840 # pulling into the target repository.
837 841 target_repo._local_pull(self.path, source_branch)
838 842 else:
839 843 cmd = ['push', os.path.abspath(repository_path),
840 844 '%s:%s' % (source_branch, target_branch)]
841 845 gitenv = {}
842 846 if rc_scm_data:
843 847 gitenv.update({'RC_SCM_DATA': rc_scm_data})
844 848
845 849 if not enable_hooks:
846 850 gitenv['RC_SKIP_HOOKS'] = '1'
847 851 self.run_git_command(cmd, fail_on_stderr=False, extra_env=gitenv)
848 852
849 853 def _get_new_pr_branch(self, source_branch, target_branch):
850 854 prefix = 'pr_%s-%s_' % (source_branch, target_branch)
851 855 pr_branches = []
852 856 for branch in self.branches:
853 857 if branch.startswith(prefix):
854 858 pr_branches.append(int(branch[len(prefix):]))
855 859
856 860 if not pr_branches:
857 861 branch_id = 0
858 862 else:
859 863 branch_id = max(pr_branches) + 1
860 864
861 865 return '%s%d' % (prefix, branch_id)
862 866
863 867 def _merge_repo(self, shadow_repository_path, target_ref,
864 868 source_repo, source_ref, merge_message,
865 869 merger_name, merger_email, dry_run=False,
866 870 use_rebase=False, close_branch=False):
867 871 if target_ref.commit_id != self.branches[target_ref.name]:
868 872 return MergeResponse(
869 873 False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD)
870 874
871 875 shadow_repo = GitRepository(shadow_repository_path)
872 876 shadow_repo._checkout(target_ref.name)
873 877 shadow_repo._local_pull(self.path, target_ref.name)
874 878 # Need to reload repo to invalidate the cache, or otherwise we cannot
875 879 # retrieve the last target commit.
876 880 shadow_repo = GitRepository(shadow_repository_path)
877 881 if target_ref.commit_id != shadow_repo.branches[target_ref.name]:
878 882 return MergeResponse(
879 883 False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD)
880 884
881 885 pr_branch = shadow_repo._get_new_pr_branch(
882 886 source_ref.name, target_ref.name)
883 887 shadow_repo._checkout(pr_branch, create=True)
884 888 try:
885 889 shadow_repo._local_fetch(source_repo.path, source_ref.name)
886 890 except RepositoryError:
887 891 log.exception('Failure when doing local fetch on git shadow repo')
888 892 return MergeResponse(
889 893 False, False, None, MergeFailureReason.MISSING_SOURCE_REF)
890 894
891 895 merge_ref = None
892 896 merge_failure_reason = MergeFailureReason.NONE
893 897 try:
894 898 shadow_repo._local_merge(merge_message, merger_name, merger_email,
895 899 [source_ref.commit_id])
896 900 merge_possible = True
897 901
898 902 # Need to reload repo to invalidate the cache, or otherwise we
899 903 # cannot retrieve the merge commit.
900 904 shadow_repo = GitRepository(shadow_repository_path)
901 905 merge_commit_id = shadow_repo.branches[pr_branch]
902 906
903 907 # Set a reference pointing to the merge commit. This reference may
904 908 # be used to easily identify the last successful merge commit in
905 909 # the shadow repository.
906 910 shadow_repo.set_refs('refs/heads/pr-merge', merge_commit_id)
907 911 merge_ref = Reference('branch', 'pr-merge', merge_commit_id)
908 912 except RepositoryError:
909 913 log.exception('Failure when doing local merge on git shadow repo')
910 914 merge_possible = False
911 915 merge_failure_reason = MergeFailureReason.MERGE_FAILED
912 916
913 917 if merge_possible and not dry_run:
914 918 try:
915 919 shadow_repo._local_push(
916 920 pr_branch, self.path, target_ref.name, enable_hooks=True,
917 921 rc_scm_data=self.config.get('rhodecode', 'RC_SCM_DATA'))
918 922 merge_succeeded = True
919 923 except RepositoryError:
920 924 log.exception(
921 925 'Failure when doing local push on git shadow repo')
922 926 merge_succeeded = False
923 927 merge_failure_reason = MergeFailureReason.PUSH_FAILED
924 928 else:
925 929 merge_succeeded = False
926 930
927 931 return MergeResponse(
928 932 merge_possible, merge_succeeded, merge_ref,
929 933 merge_failure_reason)
930 934
931 935 def _get_shadow_repository_path(self, workspace_id):
932 936 # The name of the shadow repository must start with '.', so it is
933 937 # skipped by 'rhodecode.lib.utils.get_filesystem_repos'.
934 938 return os.path.join(
935 939 os.path.dirname(self.path),
936 940 '.__shadow_%s_%s' % (os.path.basename(self.path), workspace_id))
937 941
938 942 def _maybe_prepare_merge_workspace(self, workspace_id, target_ref):
939 943 shadow_repository_path = self._get_shadow_repository_path(workspace_id)
940 944 if not os.path.exists(shadow_repository_path):
941 945 self._local_clone(shadow_repository_path, target_ref.name)
942 946
943 947 return shadow_repository_path
944 948
945 949 def cleanup_merge_workspace(self, workspace_id):
946 950 shadow_repository_path = self._get_shadow_repository_path(workspace_id)
947 951 shutil.rmtree(shadow_repository_path, ignore_errors=True)
@@ -1,1269 +1,1269 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import datetime
22 22 import mock
23 23 import os
24 24 import sys
25 25 import shutil
26 26
27 27 import pytest
28 28
29 29 from rhodecode.lib.utils import make_db_config
30 30 from rhodecode.lib.vcs.backends.base import Reference
31 31 from rhodecode.lib.vcs.backends.git import (
32 32 GitRepository, GitCommit, discover_git_version)
33 33 from rhodecode.lib.vcs.exceptions import (
34 34 RepositoryError, VCSError, NodeDoesNotExistError)
35 35 from rhodecode.lib.vcs.nodes import (
36 36 NodeKind, FileNode, DirNode, NodeState, SubModuleNode)
37 37 from rhodecode.tests import TEST_GIT_REPO, TEST_GIT_REPO_CLONE, get_new_dir
38 38 from rhodecode.tests.vcs.base import BackendTestMixin
39 39
40 40
41 41 pytestmark = pytest.mark.backends("git")
42 42
43 43
44 44 def repo_path_generator():
45 45 """
46 46 Return a different path to be used for cloning repos.
47 47 """
48 48 i = 0
49 49 while True:
50 50 i += 1
51 51 yield '%s-%d' % (TEST_GIT_REPO_CLONE, i)
52 52
53 53
54 54 REPO_PATH_GENERATOR = repo_path_generator()
55 55
56 56
57 57 class TestGitRepository:
58 58
59 59 # pylint: disable=protected-access
60 60
61 61 def __check_for_existing_repo(self):
62 62 if os.path.exists(TEST_GIT_REPO_CLONE):
63 63 self.fail('Cannot test git clone repo as location %s already '
64 64 'exists. You should manually remove it first.'
65 65 % TEST_GIT_REPO_CLONE)
66 66
67 67 @pytest.fixture(autouse=True)
68 68 def prepare(self, request, baseapp):
69 69 self.repo = GitRepository(TEST_GIT_REPO, bare=True)
70 70
71 71 def get_clone_repo(self):
72 72 """
73 73 Return a non bare clone of the base repo.
74 74 """
75 75 clone_path = next(REPO_PATH_GENERATOR)
76 76 repo_clone = GitRepository(
77 77 clone_path, create=True, src_url=self.repo.path, bare=False)
78 78
79 79 return repo_clone
80 80
81 81 def get_empty_repo(self, bare=False):
82 82 """
83 83 Return a non bare empty repo.
84 84 """
85 85 return GitRepository(next(REPO_PATH_GENERATOR), create=True, bare=bare)
86 86
87 87 def test_wrong_repo_path(self):
88 wrong_repo_path = '/tmp/errorrepo'
88 wrong_repo_path = '/tmp/errorrepo_git'
89 89 with pytest.raises(RepositoryError):
90 90 GitRepository(wrong_repo_path)
91 91
92 92 def test_repo_clone(self):
93 93 self.__check_for_existing_repo()
94 94 repo = GitRepository(TEST_GIT_REPO)
95 95 repo_clone = GitRepository(
96 96 TEST_GIT_REPO_CLONE,
97 97 src_url=TEST_GIT_REPO, create=True, update_after_clone=True)
98 98 assert len(repo.commit_ids) == len(repo_clone.commit_ids)
99 99 # Checking hashes of commits should be enough
100 100 for commit in repo.get_commits():
101 101 raw_id = commit.raw_id
102 102 assert raw_id == repo_clone.get_commit(raw_id).raw_id
103 103
104 104 def test_repo_clone_without_create(self):
105 105 with pytest.raises(RepositoryError):
106 106 GitRepository(
107 107 TEST_GIT_REPO_CLONE + '_wo_create', src_url=TEST_GIT_REPO)
108 108
109 109 def test_repo_clone_with_update(self):
110 110 repo = GitRepository(TEST_GIT_REPO)
111 111 clone_path = TEST_GIT_REPO_CLONE + '_with_update'
112 112 repo_clone = GitRepository(
113 113 clone_path,
114 114 create=True, src_url=TEST_GIT_REPO, update_after_clone=True)
115 115 assert len(repo.commit_ids) == len(repo_clone.commit_ids)
116 116
117 117 # check if current workdir was updated
118 118 fpath = os.path.join(clone_path, 'MANIFEST.in')
119 119 assert os.path.isfile(fpath)
120 120
121 121 def test_repo_clone_without_update(self):
122 122 repo = GitRepository(TEST_GIT_REPO)
123 123 clone_path = TEST_GIT_REPO_CLONE + '_without_update'
124 124 repo_clone = GitRepository(
125 125 clone_path,
126 126 create=True, src_url=TEST_GIT_REPO, update_after_clone=False)
127 127 assert len(repo.commit_ids) == len(repo_clone.commit_ids)
128 128 # check if current workdir was *NOT* updated
129 129 fpath = os.path.join(clone_path, 'MANIFEST.in')
130 130 # Make sure it's not bare repo
131 131 assert not repo_clone.bare
132 132 assert not os.path.isfile(fpath)
133 133
134 134 def test_repo_clone_into_bare_repo(self):
135 135 repo = GitRepository(TEST_GIT_REPO)
136 136 clone_path = TEST_GIT_REPO_CLONE + '_bare.git'
137 137 repo_clone = GitRepository(
138 138 clone_path, create=True, src_url=repo.path, bare=True)
139 139 assert repo_clone.bare
140 140
141 141 def test_create_repo_is_not_bare_by_default(self):
142 142 repo = GitRepository(get_new_dir('not-bare-by-default'), create=True)
143 143 assert not repo.bare
144 144
145 145 def test_create_bare_repo(self):
146 146 repo = GitRepository(get_new_dir('bare-repo'), create=True, bare=True)
147 147 assert repo.bare
148 148
149 149 def test_update_server_info(self):
150 150 self.repo._update_server_info()
151 151
152 152 def test_fetch(self, vcsbackend_git):
153 153 # Note: This is a git specific part of the API, it's only implemented
154 154 # by the git backend.
155 155 source_repo = vcsbackend_git.repo
156 156 target_repo = vcsbackend_git.create_repo()
157 157 target_repo.fetch(source_repo.path)
158 158 # Note: Get a fresh instance, avoids caching trouble
159 159 target_repo = vcsbackend_git.backend(target_repo.path)
160 160 assert len(source_repo.commit_ids) == len(target_repo.commit_ids)
161 161
162 162 def test_commit_ids(self):
163 163 # there are 112 commits (by now)
164 164 # so we can assume they would be available from now on
165 165 subset = set([
166 166 'c1214f7e79e02fc37156ff215cd71275450cffc3',
167 167 '38b5fe81f109cb111f549bfe9bb6b267e10bc557',
168 168 'fa6600f6848800641328adbf7811fd2372c02ab2',
169 169 '102607b09cdd60e2793929c4f90478be29f85a17',
170 170 '49d3fd156b6f7db46313fac355dca1a0b94a0017',
171 171 '2d1028c054665b962fa3d307adfc923ddd528038',
172 172 'd7e0d30fbcae12c90680eb095a4f5f02505ce501',
173 173 'ff7ca51e58c505fec0dd2491de52c622bb7a806b',
174 174 'dd80b0f6cf5052f17cc738c2951c4f2070200d7f',
175 175 '8430a588b43b5d6da365400117c89400326e7992',
176 176 'd955cd312c17b02143c04fa1099a352b04368118',
177 177 'f67b87e5c629c2ee0ba58f85197e423ff28d735b',
178 178 'add63e382e4aabc9e1afdc4bdc24506c269b7618',
179 179 'f298fe1189f1b69779a4423f40b48edf92a703fc',
180 180 'bd9b619eb41994cac43d67cf4ccc8399c1125808',
181 181 '6e125e7c890379446e98980d8ed60fba87d0f6d1',
182 182 'd4a54db9f745dfeba6933bf5b1e79e15d0af20bd',
183 183 '0b05e4ed56c802098dfc813cbe779b2f49e92500',
184 184 '191caa5b2c81ed17c0794bf7bb9958f4dcb0b87e',
185 185 '45223f8f114c64bf4d6f853e3c35a369a6305520',
186 186 'ca1eb7957a54bce53b12d1a51b13452f95bc7c7e',
187 187 'f5ea29fc42ef67a2a5a7aecff10e1566699acd68',
188 188 '27d48942240f5b91dfda77accd2caac94708cc7d',
189 189 '622f0eb0bafd619d2560c26f80f09e3b0b0d78af',
190 190 'e686b958768ee96af8029fe19c6050b1a8dd3b2b'])
191 191 assert subset.issubset(set(self.repo.commit_ids))
192 192
193 193 def test_slicing(self):
194 194 # 4 1 5 10 95
195 195 for sfrom, sto, size in [(0, 4, 4), (1, 2, 1), (10, 15, 5),
196 196 (10, 20, 10), (5, 100, 95)]:
197 197 commit_ids = list(self.repo[sfrom:sto])
198 198 assert len(commit_ids) == size
199 199 assert commit_ids[0] == self.repo.get_commit(commit_idx=sfrom)
200 200 assert commit_ids[-1] == self.repo.get_commit(commit_idx=sto - 1)
201 201
202 202 def test_branches(self):
203 203 # TODO: Need more tests here
204 204 # Removed (those are 'remotes' branches for cloned repo)
205 205 # assert 'master' in self.repo.branches
206 206 # assert 'gittree' in self.repo.branches
207 207 # assert 'web-branch' in self.repo.branches
208 208 for __, commit_id in self.repo.branches.items():
209 209 assert isinstance(self.repo.get_commit(commit_id), GitCommit)
210 210
211 211 def test_tags(self):
212 212 # TODO: Need more tests here
213 213 assert 'v0.1.1' in self.repo.tags
214 214 assert 'v0.1.2' in self.repo.tags
215 215 for __, commit_id in self.repo.tags.items():
216 216 assert isinstance(self.repo.get_commit(commit_id), GitCommit)
217 217
218 218 def _test_single_commit_cache(self, commit_id):
219 219 commit = self.repo.get_commit(commit_id)
220 220 assert commit_id in self.repo.commits
221 221 assert commit is self.repo.commits[commit_id]
222 222
223 223 def test_initial_commit(self):
224 224 commit_id = self.repo.commit_ids[0]
225 225 init_commit = self.repo.get_commit(commit_id)
226 226 init_author = init_commit.author
227 227
228 228 assert init_commit.message == 'initial import\n'
229 229 assert init_author == 'Marcin Kuzminski <marcin@python-blog.com>'
230 230 assert init_author == init_commit.committer
231 231 for path in ('vcs/__init__.py',
232 232 'vcs/backends/BaseRepository.py',
233 233 'vcs/backends/__init__.py'):
234 234 assert isinstance(init_commit.get_node(path), FileNode)
235 235 for path in ('', 'vcs', 'vcs/backends'):
236 236 assert isinstance(init_commit.get_node(path), DirNode)
237 237
238 238 with pytest.raises(NodeDoesNotExistError):
239 239 init_commit.get_node(path='foobar')
240 240
241 241 node = init_commit.get_node('vcs/')
242 242 assert hasattr(node, 'kind')
243 243 assert node.kind == NodeKind.DIR
244 244
245 245 node = init_commit.get_node('vcs')
246 246 assert hasattr(node, 'kind')
247 247 assert node.kind == NodeKind.DIR
248 248
249 249 node = init_commit.get_node('vcs/__init__.py')
250 250 assert hasattr(node, 'kind')
251 251 assert node.kind == NodeKind.FILE
252 252
253 253 def test_not_existing_commit(self):
254 254 with pytest.raises(RepositoryError):
255 255 self.repo.get_commit('f' * 40)
256 256
257 257 def test_commit10(self):
258 258
259 259 commit10 = self.repo.get_commit(self.repo.commit_ids[9])
260 260 README = """===
261 261 VCS
262 262 ===
263 263
264 264 Various Version Control System management abstraction layer for Python.
265 265
266 266 Introduction
267 267 ------------
268 268
269 269 TODO: To be written...
270 270
271 271 """
272 272 node = commit10.get_node('README.rst')
273 273 assert node.kind == NodeKind.FILE
274 274 assert node.content == README
275 275
276 276 def test_head(self):
277 277 assert self.repo.head == self.repo.get_commit().raw_id
278 278
279 279 def test_checkout_with_create(self):
280 280 repo_clone = self.get_clone_repo()
281 281
282 282 new_branch = 'new_branch'
283 283 assert repo_clone._current_branch() == 'master'
284 284 assert set(repo_clone.branches) == set(('master',))
285 285 repo_clone._checkout(new_branch, create=True)
286 286
287 287 # Branches is a lazy property so we need to recrete the Repo object.
288 288 repo_clone = GitRepository(repo_clone.path)
289 289 assert set(repo_clone.branches) == set(('master', new_branch))
290 290 assert repo_clone._current_branch() == new_branch
291 291
292 292 def test_checkout(self):
293 293 repo_clone = self.get_clone_repo()
294 294
295 295 repo_clone._checkout('new_branch', create=True)
296 296 repo_clone._checkout('master')
297 297
298 298 assert repo_clone._current_branch() == 'master'
299 299
300 300 def test_checkout_same_branch(self):
301 301 repo_clone = self.get_clone_repo()
302 302
303 303 repo_clone._checkout('master')
304 304 assert repo_clone._current_branch() == 'master'
305 305
306 306 def test_checkout_branch_already_exists(self):
307 307 repo_clone = self.get_clone_repo()
308 308
309 309 with pytest.raises(RepositoryError):
310 310 repo_clone._checkout('master', create=True)
311 311
312 312 def test_checkout_bare_repo(self):
313 313 with pytest.raises(RepositoryError):
314 314 self.repo._checkout('master')
315 315
316 316 def test_current_branch_bare_repo(self):
317 317 with pytest.raises(RepositoryError):
318 318 self.repo._current_branch()
319 319
320 320 def test_current_branch_empty_repo(self):
321 321 repo = self.get_empty_repo()
322 322 assert repo._current_branch() is None
323 323
324 324 def test_local_clone(self):
325 325 clone_path = next(REPO_PATH_GENERATOR)
326 326 self.repo._local_clone(clone_path, 'master')
327 327 repo_clone = GitRepository(clone_path)
328 328
329 329 assert self.repo.commit_ids == repo_clone.commit_ids
330 330
331 331 def test_local_clone_with_specific_branch(self):
332 332 source_repo = self.get_clone_repo()
333 333
334 334 # Create a new branch in source repo
335 335 new_branch_commit = source_repo.commit_ids[-3]
336 336 source_repo._checkout(new_branch_commit)
337 337 source_repo._checkout('new_branch', create=True)
338 338
339 339 clone_path = next(REPO_PATH_GENERATOR)
340 340 source_repo._local_clone(clone_path, 'new_branch')
341 341 repo_clone = GitRepository(clone_path)
342 342
343 343 assert source_repo.commit_ids[:-3 + 1] == repo_clone.commit_ids
344 344
345 345 clone_path = next(REPO_PATH_GENERATOR)
346 346 source_repo._local_clone(clone_path, 'master')
347 347 repo_clone = GitRepository(clone_path)
348 348
349 349 assert source_repo.commit_ids == repo_clone.commit_ids
350 350
351 351 def test_local_clone_fails_if_target_exists(self):
352 352 with pytest.raises(RepositoryError):
353 353 self.repo._local_clone(self.repo.path, 'master')
354 354
355 355 def test_local_fetch(self):
356 356 target_repo = self.get_empty_repo()
357 357 source_repo = self.get_clone_repo()
358 358
359 359 # Create a new branch in source repo
360 360 master_commit = source_repo.commit_ids[-1]
361 361 new_branch_commit = source_repo.commit_ids[-3]
362 362 source_repo._checkout(new_branch_commit)
363 363 source_repo._checkout('new_branch', create=True)
364 364
365 365 target_repo._local_fetch(source_repo.path, 'new_branch')
366 366 assert target_repo._last_fetch_heads() == [new_branch_commit]
367 367
368 368 target_repo._local_fetch(source_repo.path, 'master')
369 369 assert target_repo._last_fetch_heads() == [master_commit]
370 370
371 371 def test_local_fetch_from_bare_repo(self):
372 372 target_repo = self.get_empty_repo()
373 373 target_repo._local_fetch(self.repo.path, 'master')
374 374
375 375 master_commit = self.repo.commit_ids[-1]
376 376 assert target_repo._last_fetch_heads() == [master_commit]
377 377
378 378 def test_local_fetch_from_same_repo(self):
379 379 with pytest.raises(ValueError):
380 380 self.repo._local_fetch(self.repo.path, 'master')
381 381
382 382 def test_local_fetch_branch_does_not_exist(self):
383 383 target_repo = self.get_empty_repo()
384 384
385 385 with pytest.raises(RepositoryError):
386 386 target_repo._local_fetch(self.repo.path, 'new_branch')
387 387
388 388 def test_local_pull(self):
389 389 target_repo = self.get_empty_repo()
390 390 source_repo = self.get_clone_repo()
391 391
392 392 # Create a new branch in source repo
393 393 master_commit = source_repo.commit_ids[-1]
394 394 new_branch_commit = source_repo.commit_ids[-3]
395 395 source_repo._checkout(new_branch_commit)
396 396 source_repo._checkout('new_branch', create=True)
397 397
398 398 target_repo._local_pull(source_repo.path, 'new_branch')
399 399 target_repo = GitRepository(target_repo.path)
400 400 assert target_repo.head == new_branch_commit
401 401
402 402 target_repo._local_pull(source_repo.path, 'master')
403 403 target_repo = GitRepository(target_repo.path)
404 404 assert target_repo.head == master_commit
405 405
406 406 def test_local_pull_in_bare_repo(self):
407 407 with pytest.raises(RepositoryError):
408 408 self.repo._local_pull(self.repo.path, 'master')
409 409
410 410 def test_local_merge(self):
411 411 target_repo = self.get_empty_repo()
412 412 source_repo = self.get_clone_repo()
413 413
414 414 # Create a new branch in source repo
415 415 master_commit = source_repo.commit_ids[-1]
416 416 new_branch_commit = source_repo.commit_ids[-3]
417 417 source_repo._checkout(new_branch_commit)
418 418 source_repo._checkout('new_branch', create=True)
419 419
420 420 # This is required as one cannot do a -ff-only merge in an empty repo.
421 421 target_repo._local_pull(source_repo.path, 'new_branch')
422 422
423 423 target_repo._local_fetch(source_repo.path, 'master')
424 424 merge_message = 'Merge message\n\nDescription:...'
425 425 user_name = 'Albert Einstein'
426 426 user_email = 'albert@einstein.com'
427 427 target_repo._local_merge(merge_message, user_name, user_email,
428 428 target_repo._last_fetch_heads())
429 429
430 430 target_repo = GitRepository(target_repo.path)
431 431 assert target_repo.commit_ids[-2] == master_commit
432 432 last_commit = target_repo.get_commit(target_repo.head)
433 433 assert last_commit.message.strip() == merge_message
434 434 assert last_commit.author == '%s <%s>' % (user_name, user_email)
435 435
436 436 assert not os.path.exists(
437 437 os.path.join(target_repo.path, '.git', 'MERGE_HEAD'))
438 438
439 439 def test_local_merge_raises_exception_on_conflict(self, vcsbackend_git):
440 440 target_repo = vcsbackend_git.create_repo(number_of_commits=1)
441 441 vcsbackend_git.ensure_file('README', 'I will conflict with you!!!')
442 442
443 443 target_repo._local_fetch(self.repo.path, 'master')
444 444 with pytest.raises(RepositoryError):
445 445 target_repo._local_merge(
446 446 'merge_message', 'user name', 'user@name.com',
447 447 target_repo._last_fetch_heads())
448 448
449 449 # Check we are not left in an intermediate merge state
450 450 assert not os.path.exists(
451 451 os.path.join(target_repo.path, '.git', 'MERGE_HEAD'))
452 452
453 453 def test_local_merge_into_empty_repo(self):
454 454 target_repo = self.get_empty_repo()
455 455
456 456 # This is required as one cannot do a -ff-only merge in an empty repo.
457 457 target_repo._local_fetch(self.repo.path, 'master')
458 458 with pytest.raises(RepositoryError):
459 459 target_repo._local_merge(
460 460 'merge_message', 'user name', 'user@name.com',
461 461 target_repo._last_fetch_heads())
462 462
463 463 def test_local_merge_in_bare_repo(self):
464 464 with pytest.raises(RepositoryError):
465 465 self.repo._local_merge(
466 466 'merge_message', 'user name', 'user@name.com', None)
467 467
468 468 def test_local_push_non_bare(self):
469 469 target_repo = self.get_empty_repo()
470 470
471 471 pushed_branch = 'pushed_branch'
472 472 self.repo._local_push('master', target_repo.path, pushed_branch)
473 473 # Fix the HEAD of the target repo, or otherwise GitRepository won't
474 474 # report any branches.
475 475 with open(os.path.join(target_repo.path, '.git', 'HEAD'), 'w') as f:
476 476 f.write('ref: refs/heads/%s' % pushed_branch)
477 477
478 478 target_repo = GitRepository(target_repo.path)
479 479
480 480 assert (target_repo.branches[pushed_branch] ==
481 481 self.repo.branches['master'])
482 482
483 483 def test_local_push_bare(self):
484 484 target_repo = self.get_empty_repo(bare=True)
485 485
486 486 pushed_branch = 'pushed_branch'
487 487 self.repo._local_push('master', target_repo.path, pushed_branch)
488 488 # Fix the HEAD of the target repo, or otherwise GitRepository won't
489 489 # report any branches.
490 490 with open(os.path.join(target_repo.path, 'HEAD'), 'w') as f:
491 491 f.write('ref: refs/heads/%s' % pushed_branch)
492 492
493 493 target_repo = GitRepository(target_repo.path)
494 494
495 495 assert (target_repo.branches[pushed_branch] ==
496 496 self.repo.branches['master'])
497 497
498 498 def test_local_push_non_bare_target_branch_is_checked_out(self):
499 499 target_repo = self.get_clone_repo()
500 500
501 501 pushed_branch = 'pushed_branch'
502 502 # Create a new branch in source repo
503 503 new_branch_commit = target_repo.commit_ids[-3]
504 504 target_repo._checkout(new_branch_commit)
505 505 target_repo._checkout(pushed_branch, create=True)
506 506
507 507 self.repo._local_push('master', target_repo.path, pushed_branch)
508 508
509 509 target_repo = GitRepository(target_repo.path)
510 510
511 511 assert (target_repo.branches[pushed_branch] ==
512 512 self.repo.branches['master'])
513 513
514 514 def test_local_push_raises_exception_on_conflict(self, vcsbackend_git):
515 515 target_repo = vcsbackend_git.create_repo(number_of_commits=1)
516 516 with pytest.raises(RepositoryError):
517 517 self.repo._local_push('master', target_repo.path, 'master')
518 518
519 519 def test_hooks_can_be_enabled_via_env_variable_for_local_push(self):
520 520 target_repo = self.get_empty_repo(bare=True)
521 521
522 522 with mock.patch.object(self.repo, 'run_git_command') as run_mock:
523 523 self.repo._local_push(
524 524 'master', target_repo.path, 'master', enable_hooks=True)
525 525 env = run_mock.call_args[1]['extra_env']
526 526 assert 'RC_SKIP_HOOKS' not in env
527 527
528 528 def _add_failing_hook(self, repo_path, hook_name, bare=False):
529 529 path_components = (
530 530 ['hooks', hook_name] if bare else ['.git', 'hooks', hook_name])
531 531 hook_path = os.path.join(repo_path, *path_components)
532 532 with open(hook_path, 'w') as f:
533 533 script_lines = [
534 534 '#!%s' % sys.executable,
535 535 'import os',
536 536 'import sys',
537 537 'if os.environ.get("RC_SKIP_HOOKS"):',
538 538 ' sys.exit(0)',
539 539 'sys.exit(1)',
540 540 ]
541 541 f.write('\n'.join(script_lines))
542 542 os.chmod(hook_path, 0755)
543 543
544 544 def test_local_push_does_not_execute_hook(self):
545 545 target_repo = self.get_empty_repo()
546 546
547 547 pushed_branch = 'pushed_branch'
548 548 self._add_failing_hook(target_repo.path, 'pre-receive')
549 549 self.repo._local_push('master', target_repo.path, pushed_branch)
550 550 # Fix the HEAD of the target repo, or otherwise GitRepository won't
551 551 # report any branches.
552 552 with open(os.path.join(target_repo.path, '.git', 'HEAD'), 'w') as f:
553 553 f.write('ref: refs/heads/%s' % pushed_branch)
554 554
555 555 target_repo = GitRepository(target_repo.path)
556 556
557 557 assert (target_repo.branches[pushed_branch] ==
558 558 self.repo.branches['master'])
559 559
560 560 def test_local_push_executes_hook(self):
561 561 target_repo = self.get_empty_repo(bare=True)
562 562 self._add_failing_hook(target_repo.path, 'pre-receive', bare=True)
563 563 with pytest.raises(RepositoryError):
564 564 self.repo._local_push(
565 565 'master', target_repo.path, 'master', enable_hooks=True)
566 566
567 567 def test_maybe_prepare_merge_workspace(self):
568 568 workspace = self.repo._maybe_prepare_merge_workspace(
569 569 'pr2', Reference('branch', 'master', 'unused'))
570 570
571 571 assert os.path.isdir(workspace)
572 572 workspace_repo = GitRepository(workspace)
573 573 assert workspace_repo.branches == self.repo.branches
574 574
575 575 # Calling it a second time should also succeed
576 576 workspace = self.repo._maybe_prepare_merge_workspace(
577 577 'pr2', Reference('branch', 'master', 'unused'))
578 578 assert os.path.isdir(workspace)
579 579
580 580 def test_cleanup_merge_workspace(self):
581 581 workspace = self.repo._maybe_prepare_merge_workspace(
582 582 'pr3', Reference('branch', 'master', 'unused'))
583 583 self.repo.cleanup_merge_workspace('pr3')
584 584
585 585 assert not os.path.exists(workspace)
586 586
587 587 def test_cleanup_merge_workspace_invalid_workspace_id(self):
588 588 # No assert: because in case of an inexistent workspace this function
589 589 # should still succeed.
590 590 self.repo.cleanup_merge_workspace('pr4')
591 591
592 592 def test_set_refs(self):
593 593 test_ref = 'refs/test-refs/abcde'
594 594 test_commit_id = 'ecb86e1f424f2608262b130db174a7dfd25a6623'
595 595
596 596 self.repo.set_refs(test_ref, test_commit_id)
597 597 stdout, _ = self.repo.run_git_command(['show-ref'])
598 598 assert test_ref in stdout
599 599 assert test_commit_id in stdout
600 600
601 601 def test_remove_ref(self):
602 602 test_ref = 'refs/test-refs/abcde'
603 603 test_commit_id = 'ecb86e1f424f2608262b130db174a7dfd25a6623'
604 604 self.repo.set_refs(test_ref, test_commit_id)
605 605 stdout, _ = self.repo.run_git_command(['show-ref'])
606 606 assert test_ref in stdout
607 607 assert test_commit_id in stdout
608 608
609 609 self.repo.remove_ref(test_ref)
610 610 stdout, _ = self.repo.run_git_command(['show-ref'])
611 611 assert test_ref not in stdout
612 612 assert test_commit_id not in stdout
613 613
614 614
615 615 class TestGitCommit(object):
616 616
617 617 @pytest.fixture(autouse=True)
618 618 def prepare(self):
619 619 self.repo = GitRepository(TEST_GIT_REPO)
620 620
621 621 def test_default_commit(self):
622 622 tip = self.repo.get_commit()
623 623 assert tip == self.repo.get_commit(None)
624 624 assert tip == self.repo.get_commit('tip')
625 625
626 626 def test_root_node(self):
627 627 tip = self.repo.get_commit()
628 628 assert tip.root is tip.get_node('')
629 629
630 630 def test_lazy_fetch(self):
631 631 """
632 632 Test if commit's nodes expands and are cached as we walk through
633 633 the commit. This test is somewhat hard to write as order of tests
634 634 is a key here. Written by running command after command in a shell.
635 635 """
636 636 commit_id = '2a13f185e4525f9d4b59882791a2d397b90d5ddc'
637 637 assert commit_id in self.repo.commit_ids
638 638 commit = self.repo.get_commit(commit_id)
639 639 assert len(commit.nodes) == 0
640 640 root = commit.root
641 641 assert len(commit.nodes) == 1
642 642 assert len(root.nodes) == 8
643 643 # accessing root.nodes updates commit.nodes
644 644 assert len(commit.nodes) == 9
645 645
646 646 docs = root.get_node('docs')
647 647 # we haven't yet accessed anything new as docs dir was already cached
648 648 assert len(commit.nodes) == 9
649 649 assert len(docs.nodes) == 8
650 650 # accessing docs.nodes updates commit.nodes
651 651 assert len(commit.nodes) == 17
652 652
653 653 assert docs is commit.get_node('docs')
654 654 assert docs is root.nodes[0]
655 655 assert docs is root.dirs[0]
656 656 assert docs is commit.get_node('docs')
657 657
658 658 def test_nodes_with_commit(self):
659 659 commit_id = '2a13f185e4525f9d4b59882791a2d397b90d5ddc'
660 660 commit = self.repo.get_commit(commit_id)
661 661 root = commit.root
662 662 docs = root.get_node('docs')
663 663 assert docs is commit.get_node('docs')
664 664 api = docs.get_node('api')
665 665 assert api is commit.get_node('docs/api')
666 666 index = api.get_node('index.rst')
667 667 assert index is commit.get_node('docs/api/index.rst')
668 668 assert index is commit.get_node('docs')\
669 669 .get_node('api')\
670 670 .get_node('index.rst')
671 671
672 672 def test_branch_and_tags(self):
673 673 """
674 674 rev0 = self.repo.commit_ids[0]
675 675 commit0 = self.repo.get_commit(rev0)
676 676 assert commit0.branch == 'master'
677 677 assert commit0.tags == []
678 678
679 679 rev10 = self.repo.commit_ids[10]
680 680 commit10 = self.repo.get_commit(rev10)
681 681 assert commit10.branch == 'master'
682 682 assert commit10.tags == []
683 683
684 684 rev44 = self.repo.commit_ids[44]
685 685 commit44 = self.repo.get_commit(rev44)
686 686 assert commit44.branch == 'web-branch'
687 687
688 688 tip = self.repo.get_commit('tip')
689 689 assert 'tip' in tip.tags
690 690 """
691 691 # Those tests would fail - branches are now going
692 692 # to be changed at main API in order to support git backend
693 693 pass
694 694
695 695 def test_file_size(self):
696 696 to_check = (
697 697 ('c1214f7e79e02fc37156ff215cd71275450cffc3',
698 698 'vcs/backends/BaseRepository.py', 502),
699 699 ('d7e0d30fbcae12c90680eb095a4f5f02505ce501',
700 700 'vcs/backends/hg.py', 854),
701 701 ('6e125e7c890379446e98980d8ed60fba87d0f6d1',
702 702 'setup.py', 1068),
703 703
704 704 ('d955cd312c17b02143c04fa1099a352b04368118',
705 705 'vcs/backends/base.py', 2921),
706 706 ('ca1eb7957a54bce53b12d1a51b13452f95bc7c7e',
707 707 'vcs/backends/base.py', 3936),
708 708 ('f50f42baeed5af6518ef4b0cb2f1423f3851a941',
709 709 'vcs/backends/base.py', 6189),
710 710 )
711 711 for commit_id, path, size in to_check:
712 712 node = self.repo.get_commit(commit_id).get_node(path)
713 713 assert node.is_file()
714 714 assert node.size == size
715 715
716 716 def test_file_history_from_commits(self):
717 717 node = self.repo[10].get_node('setup.py')
718 718 commit_ids = [commit.raw_id for commit in node.history]
719 719 assert ['ff7ca51e58c505fec0dd2491de52c622bb7a806b'] == commit_ids
720 720
721 721 node = self.repo[20].get_node('setup.py')
722 722 node_ids = [commit.raw_id for commit in node.history]
723 723 assert ['191caa5b2c81ed17c0794bf7bb9958f4dcb0b87e',
724 724 'ff7ca51e58c505fec0dd2491de52c622bb7a806b'] == node_ids
725 725
726 726 # special case we check history from commit that has this particular
727 727 # file changed this means we check if it's included as well
728 728 node = self.repo.get_commit('191caa5b2c81ed17c0794bf7bb9958f4dcb0b87e') \
729 729 .get_node('setup.py')
730 730 node_ids = [commit.raw_id for commit in node.history]
731 731 assert ['191caa5b2c81ed17c0794bf7bb9958f4dcb0b87e',
732 732 'ff7ca51e58c505fec0dd2491de52c622bb7a806b'] == node_ids
733 733
734 734 def test_file_history(self):
735 735 # we can only check if those commits are present in the history
736 736 # as we cannot update this test every time file is changed
737 737 files = {
738 738 'setup.py': [
739 739 '54386793436c938cff89326944d4c2702340037d',
740 740 '51d254f0ecf5df2ce50c0b115741f4cf13985dab',
741 741 '998ed409c795fec2012b1c0ca054d99888b22090',
742 742 '5e0eb4c47f56564395f76333f319d26c79e2fb09',
743 743 '0115510b70c7229dbc5dc49036b32e7d91d23acd',
744 744 '7cb3fd1b6d8c20ba89e2264f1c8baebc8a52d36e',
745 745 '2a13f185e4525f9d4b59882791a2d397b90d5ddc',
746 746 '191caa5b2c81ed17c0794bf7bb9958f4dcb0b87e',
747 747 'ff7ca51e58c505fec0dd2491de52c622bb7a806b',
748 748 ],
749 749 'vcs/nodes.py': [
750 750 '33fa3223355104431402a888fa77a4e9956feb3e',
751 751 'fa014c12c26d10ba682fadb78f2a11c24c8118e1',
752 752 'e686b958768ee96af8029fe19c6050b1a8dd3b2b',
753 753 'ab5721ca0a081f26bf43d9051e615af2cc99952f',
754 754 'c877b68d18e792a66b7f4c529ea02c8f80801542',
755 755 '4313566d2e417cb382948f8d9d7c765330356054',
756 756 '6c2303a793671e807d1cfc70134c9ca0767d98c2',
757 757 '54386793436c938cff89326944d4c2702340037d',
758 758 '54000345d2e78b03a99d561399e8e548de3f3203',
759 759 '1c6b3677b37ea064cb4b51714d8f7498f93f4b2b',
760 760 '2d03ca750a44440fb5ea8b751176d1f36f8e8f46',
761 761 '2a08b128c206db48c2f0b8f70df060e6db0ae4f8',
762 762 '30c26513ff1eb8e5ce0e1c6b477ee5dc50e2f34b',
763 763 'ac71e9503c2ca95542839af0ce7b64011b72ea7c',
764 764 '12669288fd13adba2a9b7dd5b870cc23ffab92d2',
765 765 '5a0c84f3e6fe3473e4c8427199d5a6fc71a9b382',
766 766 '12f2f5e2b38e6ff3fbdb5d722efed9aa72ecb0d5',
767 767 '5eab1222a7cd4bfcbabc218ca6d04276d4e27378',
768 768 'f50f42baeed5af6518ef4b0cb2f1423f3851a941',
769 769 'd7e390a45f6aa96f04f5e7f583ad4f867431aa25',
770 770 'f15c21f97864b4f071cddfbf2750ec2e23859414',
771 771 'e906ef056cf539a4e4e5fc8003eaf7cf14dd8ade',
772 772 'ea2b108b48aa8f8c9c4a941f66c1a03315ca1c3b',
773 773 '84dec09632a4458f79f50ddbbd155506c460b4f9',
774 774 '0115510b70c7229dbc5dc49036b32e7d91d23acd',
775 775 '2a13f185e4525f9d4b59882791a2d397b90d5ddc',
776 776 '3bf1c5868e570e39569d094f922d33ced2fa3b2b',
777 777 'b8d04012574729d2c29886e53b1a43ef16dd00a1',
778 778 '6970b057cffe4aab0a792aa634c89f4bebf01441',
779 779 'dd80b0f6cf5052f17cc738c2951c4f2070200d7f',
780 780 'ff7ca51e58c505fec0dd2491de52c622bb7a806b',
781 781 ],
782 782 'vcs/backends/git.py': [
783 783 '4cf116ad5a457530381135e2f4c453e68a1b0105',
784 784 '9a751d84d8e9408e736329767387f41b36935153',
785 785 'cb681fb539c3faaedbcdf5ca71ca413425c18f01',
786 786 '428f81bb652bcba8d631bce926e8834ff49bdcc6',
787 787 '180ab15aebf26f98f714d8c68715e0f05fa6e1c7',
788 788 '2b8e07312a2e89e92b90426ab97f349f4bce2a3a',
789 789 '50e08c506174d8645a4bb517dd122ac946a0f3bf',
790 790 '54000345d2e78b03a99d561399e8e548de3f3203',
791 791 ],
792 792 }
793 793 for path, commit_ids in files.items():
794 794 node = self.repo.get_commit(commit_ids[0]).get_node(path)
795 795 node_ids = [commit.raw_id for commit in node.history]
796 796 assert set(commit_ids).issubset(set(node_ids)), (
797 797 "We assumed that %s is subset of commit_ids for which file %s "
798 798 "has been changed, and history of that node returned: %s"
799 799 % (commit_ids, path, node_ids))
800 800
801 801 def test_file_annotate(self):
802 802 files = {
803 803 'vcs/backends/__init__.py': {
804 804 'c1214f7e79e02fc37156ff215cd71275450cffc3': {
805 805 'lines_no': 1,
806 806 'commits': [
807 807 'c1214f7e79e02fc37156ff215cd71275450cffc3',
808 808 ],
809 809 },
810 810 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647': {
811 811 'lines_no': 21,
812 812 'commits': [
813 813 '49d3fd156b6f7db46313fac355dca1a0b94a0017',
814 814 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
815 815 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
816 816 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
817 817 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
818 818 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
819 819 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
820 820 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
821 821 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
822 822 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
823 823 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
824 824 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
825 825 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
826 826 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
827 827 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
828 828 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
829 829 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
830 830 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
831 831 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
832 832 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
833 833 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
834 834 ],
835 835 },
836 836 'e29b67bd158580fc90fc5e9111240b90e6e86064': {
837 837 'lines_no': 32,
838 838 'commits': [
839 839 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
840 840 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
841 841 '5eab1222a7cd4bfcbabc218ca6d04276d4e27378',
842 842 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
843 843 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
844 844 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
845 845 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
846 846 '54000345d2e78b03a99d561399e8e548de3f3203',
847 847 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
848 848 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
849 849 '78c3f0c23b7ee935ec276acb8b8212444c33c396',
850 850 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
851 851 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
852 852 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
853 853 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
854 854 '2a13f185e4525f9d4b59882791a2d397b90d5ddc',
855 855 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
856 856 '78c3f0c23b7ee935ec276acb8b8212444c33c396',
857 857 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
858 858 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
859 859 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
860 860 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
861 861 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
862 862 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
863 863 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
864 864 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
865 865 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
866 866 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
867 867 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
868 868 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
869 869 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
870 870 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
871 871 ],
872 872 },
873 873 },
874 874 }
875 875
876 876 for fname, commit_dict in files.items():
877 877 for commit_id, __ in commit_dict.items():
878 878 commit = self.repo.get_commit(commit_id)
879 879
880 880 l1_1 = [x[1] for x in commit.get_file_annotate(fname)]
881 881 l1_2 = [x[2]().raw_id for x in commit.get_file_annotate(fname)]
882 882 assert l1_1 == l1_2
883 883 l1 = l1_1
884 884 l2 = files[fname][commit_id]['commits']
885 885 assert l1 == l2, (
886 886 "The lists of commit_ids for %s@commit_id %s"
887 887 "from annotation list should match each other, "
888 888 "got \n%s \nvs \n%s " % (fname, commit_id, l1, l2))
889 889
890 890 def test_files_state(self):
891 891 """
892 892 Tests state of FileNodes.
893 893 """
894 894 node = self.repo\
895 895 .get_commit('e6ea6d16e2f26250124a1f4b4fe37a912f9d86a0')\
896 896 .get_node('vcs/utils/diffs.py')
897 897 assert node.state, NodeState.ADDED
898 898 assert node.added
899 899 assert not node.changed
900 900 assert not node.not_changed
901 901 assert not node.removed
902 902
903 903 node = self.repo\
904 904 .get_commit('33fa3223355104431402a888fa77a4e9956feb3e')\
905 905 .get_node('.hgignore')
906 906 assert node.state, NodeState.CHANGED
907 907 assert not node.added
908 908 assert node.changed
909 909 assert not node.not_changed
910 910 assert not node.removed
911 911
912 912 node = self.repo\
913 913 .get_commit('e29b67bd158580fc90fc5e9111240b90e6e86064')\
914 914 .get_node('setup.py')
915 915 assert node.state, NodeState.NOT_CHANGED
916 916 assert not node.added
917 917 assert not node.changed
918 918 assert node.not_changed
919 919 assert not node.removed
920 920
921 921 # If node has REMOVED state then trying to fetch it would raise
922 922 # CommitError exception
923 923 commit = self.repo.get_commit(
924 924 'fa6600f6848800641328adbf7811fd2372c02ab2')
925 925 path = 'vcs/backends/BaseRepository.py'
926 926 with pytest.raises(NodeDoesNotExistError):
927 927 commit.get_node(path)
928 928 # but it would be one of ``removed`` (commit's attribute)
929 929 assert path in [rf.path for rf in commit.removed]
930 930
931 931 commit = self.repo.get_commit(
932 932 '54386793436c938cff89326944d4c2702340037d')
933 933 changed = [
934 934 'setup.py', 'tests/test_nodes.py', 'vcs/backends/hg.py',
935 935 'vcs/nodes.py']
936 936 assert set(changed) == set([f.path for f in commit.changed])
937 937
938 938 def test_unicode_branch_refs(self):
939 939 unicode_branches = {
940 940 'refs/heads/unicode': '6c0ce52b229aa978889e91b38777f800e85f330b',
941 941 u'refs/heads/uniΓ§ΓΆβˆ‚e': 'ΓΌrl',
942 942 }
943 943 with mock.patch(
944 944 ("rhodecode.lib.vcs.backends.git.repository"
945 945 ".GitRepository._refs"),
946 946 unicode_branches):
947 947 branches = self.repo.branches
948 948
949 949 assert 'unicode' in branches
950 950 assert u'uniΓ§ΓΆβˆ‚e' in branches
951 951
952 952 def test_unicode_tag_refs(self):
953 953 unicode_tags = {
954 954 'refs/tags/unicode': '6c0ce52b229aa978889e91b38777f800e85f330b',
955 955 u'refs/tags/uniΓ§ΓΆβˆ‚e': '6c0ce52b229aa978889e91b38777f800e85f330b',
956 956 }
957 957 with mock.patch(
958 958 ("rhodecode.lib.vcs.backends.git.repository"
959 959 ".GitRepository._refs"),
960 960 unicode_tags):
961 961 tags = self.repo.tags
962 962
963 963 assert 'unicode' in tags
964 964 assert u'uniΓ§ΓΆβˆ‚e' in tags
965 965
966 966 def test_commit_message_is_unicode(self):
967 967 for commit in self.repo:
968 968 assert type(commit.message) == unicode
969 969
970 970 def test_commit_author_is_unicode(self):
971 971 for commit in self.repo:
972 972 assert type(commit.author) == unicode
973 973
974 974 def test_repo_files_content_is_unicode(self):
975 975 commit = self.repo.get_commit()
976 976 for node in commit.get_node('/'):
977 977 if node.is_file():
978 978 assert type(node.content) == unicode
979 979
980 980 def test_wrong_path(self):
981 981 # There is 'setup.py' in the root dir but not there:
982 982 path = 'foo/bar/setup.py'
983 983 tip = self.repo.get_commit()
984 984 with pytest.raises(VCSError):
985 985 tip.get_node(path)
986 986
987 987 @pytest.mark.parametrize("author_email, commit_id", [
988 988 ('marcin@python-blog.com', 'c1214f7e79e02fc37156ff215cd71275450cffc3'),
989 989 ('lukasz.balcerzak@python-center.pl',
990 990 'ff7ca51e58c505fec0dd2491de52c622bb7a806b'),
991 991 ('none@none', '8430a588b43b5d6da365400117c89400326e7992'),
992 992 ])
993 993 def test_author_email(self, author_email, commit_id):
994 994 commit = self.repo.get_commit(commit_id)
995 995 assert author_email == commit.author_email
996 996
997 997 @pytest.mark.parametrize("author, commit_id", [
998 998 ('Marcin Kuzminski', 'c1214f7e79e02fc37156ff215cd71275450cffc3'),
999 999 ('Lukasz Balcerzak', 'ff7ca51e58c505fec0dd2491de52c622bb7a806b'),
1000 1000 ('marcink', '8430a588b43b5d6da365400117c89400326e7992'),
1001 1001 ])
1002 1002 def test_author_username(self, author, commit_id):
1003 1003 commit = self.repo.get_commit(commit_id)
1004 1004 assert author == commit.author_name
1005 1005
1006 1006
1007 1007 class TestLargeFileRepo(object):
1008 1008
1009 1009 def test_large_file(self, backend_git):
1010 1010 conf = make_db_config()
1011 1011 repo = backend_git.create_test_repo('largefiles', conf)
1012 1012
1013 1013 tip = repo.scm_instance().get_commit()
1014 1014
1015 1015 # extract stored LF node into the origin cache
1016 1016 lfs_store = os.path.join(repo.repo_path, repo.repo_name, 'lfs_store')
1017 1017
1018 1018 oid = '7b331c02e313c7599d5a90212e17e6d3cb729bd2e1c9b873c302a63c95a2f9bf'
1019 1019 oid_path = os.path.join(lfs_store, oid)
1020 1020 oid_destination = os.path.join(
1021 1021 conf.get('vcs_git_lfs', 'store_location'), oid)
1022 1022 shutil.copy(oid_path, oid_destination)
1023 1023
1024 1024 node = tip.get_node('1MB.zip')
1025 1025
1026 1026 lf_node = node.get_largefile_node()
1027 1027
1028 1028 assert lf_node.is_largefile() is True
1029 1029 assert lf_node.size == 1024000
1030 1030 assert lf_node.name == '1MB.zip'
1031 1031
1032 1032
1033 1033 class TestGitSpecificWithRepo(BackendTestMixin):
1034 1034
1035 1035 @classmethod
1036 1036 def _get_commits(cls):
1037 1037 return [
1038 1038 {
1039 1039 'message': 'Initial',
1040 1040 'author': 'Joe Doe <joe.doe@example.com>',
1041 1041 'date': datetime.datetime(2010, 1, 1, 20),
1042 1042 'added': [
1043 1043 FileNode('foobar/static/js/admin/base.js', content='base'),
1044 1044 FileNode(
1045 1045 'foobar/static/admin', content='admin',
1046 1046 mode=0120000), # this is a link
1047 1047 FileNode('foo', content='foo'),
1048 1048 ],
1049 1049 },
1050 1050 {
1051 1051 'message': 'Second',
1052 1052 'author': 'Joe Doe <joe.doe@example.com>',
1053 1053 'date': datetime.datetime(2010, 1, 1, 22),
1054 1054 'added': [
1055 1055 FileNode('foo2', content='foo2'),
1056 1056 ],
1057 1057 },
1058 1058 ]
1059 1059
1060 1060 def test_paths_slow_traversing(self):
1061 1061 commit = self.repo.get_commit()
1062 1062 assert commit.get_node('foobar').get_node('static').get_node('js')\
1063 1063 .get_node('admin').get_node('base.js').content == 'base'
1064 1064
1065 1065 def test_paths_fast_traversing(self):
1066 1066 commit = self.repo.get_commit()
1067 1067 assert (
1068 1068 commit.get_node('foobar/static/js/admin/base.js').content ==
1069 1069 'base')
1070 1070
1071 1071 def test_get_diff_runs_git_command_with_hashes(self):
1072 1072 self.repo.run_git_command = mock.Mock(return_value=['', ''])
1073 1073 self.repo.get_diff(self.repo[0], self.repo[1])
1074 1074 self.repo.run_git_command.assert_called_once_with(
1075 1075 ['diff', '-U3', '--full-index', '--binary', '-p', '-M',
1076 1076 '--abbrev=40', self.repo._get_commit_id(0),
1077 1077 self.repo._get_commit_id(1)])
1078 1078
1079 1079 def test_get_diff_runs_git_command_with_str_hashes(self):
1080 1080 self.repo.run_git_command = mock.Mock(return_value=['', ''])
1081 1081 self.repo.get_diff(self.repo.EMPTY_COMMIT, self.repo[1])
1082 1082 self.repo.run_git_command.assert_called_once_with(
1083 1083 ['show', '-U3', '--full-index', '--binary', '-p', '-M',
1084 1084 '--abbrev=40', self.repo._get_commit_id(1)])
1085 1085
1086 1086 def test_get_diff_runs_git_command_with_path_if_its_given(self):
1087 1087 self.repo.run_git_command = mock.Mock(return_value=['', ''])
1088 1088 self.repo.get_diff(self.repo[0], self.repo[1], 'foo')
1089 1089 self.repo.run_git_command.assert_called_once_with(
1090 1090 ['diff', '-U3', '--full-index', '--binary', '-p', '-M',
1091 1091 '--abbrev=40', self.repo._get_commit_id(0),
1092 1092 self.repo._get_commit_id(1), '--', 'foo'])
1093 1093
1094 1094
1095 1095 class TestGitRegression(BackendTestMixin):
1096 1096
1097 1097 @classmethod
1098 1098 def _get_commits(cls):
1099 1099 return [
1100 1100 {
1101 1101 'message': 'Initial',
1102 1102 'author': 'Joe Doe <joe.doe@example.com>',
1103 1103 'date': datetime.datetime(2010, 1, 1, 20),
1104 1104 'added': [
1105 1105 FileNode('bot/__init__.py', content='base'),
1106 1106 FileNode('bot/templates/404.html', content='base'),
1107 1107 FileNode('bot/templates/500.html', content='base'),
1108 1108 ],
1109 1109 },
1110 1110 {
1111 1111 'message': 'Second',
1112 1112 'author': 'Joe Doe <joe.doe@example.com>',
1113 1113 'date': datetime.datetime(2010, 1, 1, 22),
1114 1114 'added': [
1115 1115 FileNode('bot/build/migrations/1.py', content='foo2'),
1116 1116 FileNode('bot/build/migrations/2.py', content='foo2'),
1117 1117 FileNode(
1118 1118 'bot/build/static/templates/f.html', content='foo2'),
1119 1119 FileNode(
1120 1120 'bot/build/static/templates/f1.html', content='foo2'),
1121 1121 FileNode('bot/build/templates/err.html', content='foo2'),
1122 1122 FileNode('bot/build/templates/err2.html', content='foo2'),
1123 1123 ],
1124 1124 },
1125 1125 ]
1126 1126
1127 1127 @pytest.mark.parametrize("path, expected_paths", [
1128 1128 ('bot', [
1129 1129 'bot/build',
1130 1130 'bot/templates',
1131 1131 'bot/__init__.py']),
1132 1132 ('bot/build', [
1133 1133 'bot/build/migrations',
1134 1134 'bot/build/static',
1135 1135 'bot/build/templates']),
1136 1136 ('bot/build/static', [
1137 1137 'bot/build/static/templates']),
1138 1138 ('bot/build/static/templates', [
1139 1139 'bot/build/static/templates/f.html',
1140 1140 'bot/build/static/templates/f1.html']),
1141 1141 ('bot/build/templates', [
1142 1142 'bot/build/templates/err.html',
1143 1143 'bot/build/templates/err2.html']),
1144 1144 ('bot/templates/', [
1145 1145 'bot/templates/404.html',
1146 1146 'bot/templates/500.html']),
1147 1147 ])
1148 1148 def test_similar_paths(self, path, expected_paths):
1149 1149 commit = self.repo.get_commit()
1150 1150 paths = [n.path for n in commit.get_nodes(path)]
1151 1151 assert paths == expected_paths
1152 1152
1153 1153
1154 1154 class TestDiscoverGitVersion:
1155 1155
1156 1156 def test_returns_git_version(self, baseapp):
1157 1157 version = discover_git_version()
1158 1158 assert version
1159 1159
1160 1160 def test_returns_empty_string_without_vcsserver(self):
1161 1161 mock_connection = mock.Mock()
1162 1162 mock_connection.discover_git_version = mock.Mock(
1163 1163 side_effect=Exception)
1164 1164 with mock.patch('rhodecode.lib.vcs.connection.Git', mock_connection):
1165 1165 version = discover_git_version()
1166 1166 assert version == ''
1167 1167
1168 1168
1169 1169 class TestGetSubmoduleUrl(object):
1170 1170 def test_submodules_file_found(self):
1171 1171 commit = GitCommit(repository=mock.Mock(), raw_id='abcdef12', idx=1)
1172 1172 node = mock.Mock()
1173 1173 with mock.patch.object(
1174 1174 commit, 'get_node', return_value=node) as get_node_mock:
1175 1175 node.content = (
1176 1176 '[submodule "subrepo1"]\n'
1177 1177 '\tpath = subrepo1\n'
1178 1178 '\turl = https://code.rhodecode.com/dulwich\n'
1179 1179 )
1180 1180 result = commit._get_submodule_url('subrepo1')
1181 1181 get_node_mock.assert_called_once_with('.gitmodules')
1182 1182 assert result == 'https://code.rhodecode.com/dulwich'
1183 1183
1184 1184 def test_complex_submodule_path(self):
1185 1185 commit = GitCommit(repository=mock.Mock(), raw_id='abcdef12', idx=1)
1186 1186 node = mock.Mock()
1187 1187 with mock.patch.object(
1188 1188 commit, 'get_node', return_value=node) as get_node_mock:
1189 1189 node.content = (
1190 1190 '[submodule "complex/subrepo/path"]\n'
1191 1191 '\tpath = complex/subrepo/path\n'
1192 1192 '\turl = https://code.rhodecode.com/dulwich\n'
1193 1193 )
1194 1194 result = commit._get_submodule_url('complex/subrepo/path')
1195 1195 get_node_mock.assert_called_once_with('.gitmodules')
1196 1196 assert result == 'https://code.rhodecode.com/dulwich'
1197 1197
1198 1198 def test_submodules_file_not_found(self):
1199 1199 commit = GitCommit(repository=mock.Mock(), raw_id='abcdef12', idx=1)
1200 1200 with mock.patch.object(
1201 1201 commit, 'get_node', side_effect=NodeDoesNotExistError):
1202 1202 result = commit._get_submodule_url('complex/subrepo/path')
1203 1203 assert result is None
1204 1204
1205 1205 def test_path_not_found(self):
1206 1206 commit = GitCommit(repository=mock.Mock(), raw_id='abcdef12', idx=1)
1207 1207 node = mock.Mock()
1208 1208 with mock.patch.object(
1209 1209 commit, 'get_node', return_value=node) as get_node_mock:
1210 1210 node.content = (
1211 1211 '[submodule "subrepo1"]\n'
1212 1212 '\tpath = subrepo1\n'
1213 1213 '\turl = https://code.rhodecode.com/dulwich\n'
1214 1214 )
1215 1215 result = commit._get_submodule_url('subrepo2')
1216 1216 get_node_mock.assert_called_once_with('.gitmodules')
1217 1217 assert result is None
1218 1218
1219 1219 def test_returns_cached_values(self):
1220 1220 commit = GitCommit(repository=mock.Mock(), raw_id='abcdef12', idx=1)
1221 1221 node = mock.Mock()
1222 1222 with mock.patch.object(
1223 1223 commit, 'get_node', return_value=node) as get_node_mock:
1224 1224 node.content = (
1225 1225 '[submodule "subrepo1"]\n'
1226 1226 '\tpath = subrepo1\n'
1227 1227 '\turl = https://code.rhodecode.com/dulwich\n'
1228 1228 )
1229 1229 for _ in range(3):
1230 1230 commit._get_submodule_url('subrepo1')
1231 1231 get_node_mock.assert_called_once_with('.gitmodules')
1232 1232
1233 1233 def test_get_node_returns_a_link(self):
1234 1234 repository = mock.Mock()
1235 1235 repository.alias = 'git'
1236 1236 commit = GitCommit(repository=repository, raw_id='abcdef12', idx=1)
1237 1237 submodule_url = 'https://code.rhodecode.com/dulwich'
1238 1238 get_id_patch = mock.patch.object(
1239 1239 commit, '_get_id_for_path', return_value=(1, 'link'))
1240 1240 get_submodule_patch = mock.patch.object(
1241 1241 commit, '_get_submodule_url', return_value=submodule_url)
1242 1242
1243 1243 with get_id_patch, get_submodule_patch as submodule_mock:
1244 1244 node = commit.get_node('/abcde')
1245 1245
1246 1246 submodule_mock.assert_called_once_with('/abcde')
1247 1247 assert type(node) == SubModuleNode
1248 1248 assert node.url == submodule_url
1249 1249
1250 1250 def test_get_nodes_returns_links(self):
1251 1251 repository = mock.MagicMock()
1252 1252 repository.alias = 'git'
1253 1253 repository._remote.tree_items.return_value = [
1254 1254 ('subrepo', 'stat', 1, 'link')
1255 1255 ]
1256 1256 commit = GitCommit(repository=repository, raw_id='abcdef12', idx=1)
1257 1257 submodule_url = 'https://code.rhodecode.com/dulwich'
1258 1258 get_id_patch = mock.patch.object(
1259 1259 commit, '_get_id_for_path', return_value=(1, 'tree'))
1260 1260 get_submodule_patch = mock.patch.object(
1261 1261 commit, '_get_submodule_url', return_value=submodule_url)
1262 1262
1263 1263 with get_id_patch, get_submodule_patch as submodule_mock:
1264 1264 nodes = commit.get_nodes('/abcde')
1265 1265
1266 1266 submodule_mock.assert_called_once_with('/abcde/subrepo')
1267 1267 assert len(nodes) == 1
1268 1268 assert type(nodes[0]) == SubModuleNode
1269 1269 assert nodes[0].url == submodule_url
@@ -1,1180 +1,1180 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import os
22 22
23 23 import mock
24 24 import pytest
25 25
26 26 from rhodecode.lib.utils import make_db_config
27 27 from rhodecode.lib.vcs import backends
28 28 from rhodecode.lib.vcs.backends.base import (
29 29 Reference, MergeResponse, MergeFailureReason)
30 30 from rhodecode.lib.vcs.backends.hg import MercurialRepository, MercurialCommit
31 31 from rhodecode.lib.vcs.exceptions import (
32 32 RepositoryError, VCSError, NodeDoesNotExistError, CommitDoesNotExistError)
33 33 from rhodecode.lib.vcs.nodes import FileNode, NodeKind, NodeState
34 34 from rhodecode.tests import TEST_HG_REPO, TEST_HG_REPO_CLONE
35 35
36 36
37 37 pytestmark = pytest.mark.backends("hg")
38 38
39 39
40 40 def repo_path_generator():
41 41 """
42 42 Return a different path to be used for cloning repos.
43 43 """
44 44 i = 0
45 45 while True:
46 46 i += 1
47 47 yield '%s-%d' % (TEST_HG_REPO_CLONE, i)
48 48
49 49
50 50 REPO_PATH_GENERATOR = repo_path_generator()
51 51
52 52
53 53 @pytest.fixture(scope='class', autouse=True)
54 54 def repo(request, baseapp):
55 55 repo = MercurialRepository(TEST_HG_REPO)
56 56 if request.cls:
57 57 request.cls.repo = repo
58 58 return repo
59 59
60 60
61 61 class TestMercurialRepository:
62 62
63 63 # pylint: disable=protected-access
64 64
65 65 def get_clone_repo(self):
66 66 """
67 67 Return a clone of the base repo.
68 68 """
69 69 clone_path = next(REPO_PATH_GENERATOR)
70 70 repo_clone = MercurialRepository(
71 71 clone_path, create=True, src_url=self.repo.path)
72 72
73 73 return repo_clone
74 74
75 75 def get_empty_repo(self):
76 76 """
77 77 Return an empty repo.
78 78 """
79 79 return MercurialRepository(next(REPO_PATH_GENERATOR), create=True)
80 80
81 81 def test_wrong_repo_path(self):
82 wrong_repo_path = '/tmp/errorrepo'
82 wrong_repo_path = '/tmp/errorrepo_hg'
83 83 with pytest.raises(RepositoryError):
84 84 MercurialRepository(wrong_repo_path)
85 85
86 86 def test_unicode_path_repo(self):
87 87 with pytest.raises(VCSError):
88 88 MercurialRepository(u'iShouldFail')
89 89
90 90 def test_unicode_commit_id(self):
91 91 with pytest.raises(CommitDoesNotExistError):
92 92 self.repo.get_commit(u'unicode-commit-id')
93 93 with pytest.raises(CommitDoesNotExistError):
94 94 self.repo.get_commit(u'unΓ­cΓΈde-spéçial-chΓ€rΓ₯cter-commit-id')
95 95
96 96 def test_unicode_bookmark(self):
97 97 self.repo.bookmark(u'unicode-bookmark')
98 98 self.repo.bookmark(u'unΓ­cΓΈde-spéçial-chΓ€rΓ₯cter-bookmark')
99 99
100 100 def test_unicode_branch(self):
101 101 with pytest.raises(KeyError):
102 102 self.repo.branches[u'unicode-branch']
103 103 with pytest.raises(KeyError):
104 104 self.repo.branches[u'unΓ­cΓΈde-spéçial-chΓ€rΓ₯cter-branch']
105 105
106 106 def test_repo_clone(self):
107 107 if os.path.exists(TEST_HG_REPO_CLONE):
108 108 self.fail(
109 109 'Cannot test mercurial clone repo as location %s already '
110 110 'exists. You should manually remove it first.'
111 111 % TEST_HG_REPO_CLONE)
112 112
113 113 repo = MercurialRepository(TEST_HG_REPO)
114 114 repo_clone = MercurialRepository(TEST_HG_REPO_CLONE,
115 115 src_url=TEST_HG_REPO)
116 116 assert len(repo.commit_ids) == len(repo_clone.commit_ids)
117 117 # Checking hashes of commits should be enough
118 118 for commit in repo.get_commits():
119 119 raw_id = commit.raw_id
120 120 assert raw_id == repo_clone.get_commit(raw_id).raw_id
121 121
122 122 def test_repo_clone_with_update(self):
123 123 repo = MercurialRepository(TEST_HG_REPO)
124 124 repo_clone = MercurialRepository(
125 125 TEST_HG_REPO_CLONE + '_w_update',
126 126 src_url=TEST_HG_REPO, update_after_clone=True)
127 127 assert len(repo.commit_ids) == len(repo_clone.commit_ids)
128 128
129 129 # check if current workdir was updated
130 130 assert os.path.isfile(
131 131 os.path.join(TEST_HG_REPO_CLONE + '_w_update', 'MANIFEST.in'))
132 132
133 133 def test_repo_clone_without_update(self):
134 134 repo = MercurialRepository(TEST_HG_REPO)
135 135 repo_clone = MercurialRepository(
136 136 TEST_HG_REPO_CLONE + '_wo_update',
137 137 src_url=TEST_HG_REPO, update_after_clone=False)
138 138 assert len(repo.commit_ids) == len(repo_clone.commit_ids)
139 139 assert not os.path.isfile(
140 140 os.path.join(TEST_HG_REPO_CLONE + '_wo_update', 'MANIFEST.in'))
141 141
142 142 def test_commit_ids(self):
143 143 # there are 21 commits at bitbucket now
144 144 # so we can assume they would be available from now on
145 145 subset = set([
146 146 'b986218ba1c9b0d6a259fac9b050b1724ed8e545',
147 147 '3d8f361e72ab303da48d799ff1ac40d5ac37c67e',
148 148 '6cba7170863a2411822803fa77a0a264f1310b35',
149 149 '56349e29c2af3ac913b28bde9a2c6154436e615b',
150 150 '2dda4e345facb0ccff1a191052dd1606dba6781d',
151 151 '6fff84722075f1607a30f436523403845f84cd9e',
152 152 '7d4bc8ec6be56c0f10425afb40b6fc315a4c25e7',
153 153 '3803844fdbd3b711175fc3da9bdacfcd6d29a6fb',
154 154 'dc5d2c0661b61928834a785d3e64a3f80d3aad9c',
155 155 'be90031137367893f1c406e0a8683010fd115b79',
156 156 'db8e58be770518cbb2b1cdfa69146e47cd481481',
157 157 '84478366594b424af694a6c784cb991a16b87c21',
158 158 '17f8e105dddb9f339600389c6dc7175d395a535c',
159 159 '20a662e756499bde3095ffc9bc0643d1def2d0eb',
160 160 '2e319b85e70a707bba0beff866d9f9de032aa4f9',
161 161 '786facd2c61deb9cf91e9534735124fb8fc11842',
162 162 '94593d2128d38210a2fcd1aabff6dda0d6d9edf8',
163 163 'aa6a0de05b7612707db567078e130a6cd114a9a7',
164 164 'eada5a770da98ab0dd7325e29d00e0714f228d09'
165 165 ])
166 166 assert subset.issubset(set(self.repo.commit_ids))
167 167
168 168 # check if we have the proper order of commits
169 169 org = [
170 170 'b986218ba1c9b0d6a259fac9b050b1724ed8e545',
171 171 '3d8f361e72ab303da48d799ff1ac40d5ac37c67e',
172 172 '6cba7170863a2411822803fa77a0a264f1310b35',
173 173 '56349e29c2af3ac913b28bde9a2c6154436e615b',
174 174 '2dda4e345facb0ccff1a191052dd1606dba6781d',
175 175 '6fff84722075f1607a30f436523403845f84cd9e',
176 176 '7d4bc8ec6be56c0f10425afb40b6fc315a4c25e7',
177 177 '3803844fdbd3b711175fc3da9bdacfcd6d29a6fb',
178 178 'dc5d2c0661b61928834a785d3e64a3f80d3aad9c',
179 179 'be90031137367893f1c406e0a8683010fd115b79',
180 180 'db8e58be770518cbb2b1cdfa69146e47cd481481',
181 181 '84478366594b424af694a6c784cb991a16b87c21',
182 182 '17f8e105dddb9f339600389c6dc7175d395a535c',
183 183 '20a662e756499bde3095ffc9bc0643d1def2d0eb',
184 184 '2e319b85e70a707bba0beff866d9f9de032aa4f9',
185 185 '786facd2c61deb9cf91e9534735124fb8fc11842',
186 186 '94593d2128d38210a2fcd1aabff6dda0d6d9edf8',
187 187 'aa6a0de05b7612707db567078e130a6cd114a9a7',
188 188 'eada5a770da98ab0dd7325e29d00e0714f228d09',
189 189 '2c1885c735575ca478bf9e17b0029dca68824458',
190 190 'd9bcd465040bf869799b09ad732c04e0eea99fe9',
191 191 '469e9c847fe1f6f7a697b8b25b4bc5b48780c1a7',
192 192 '4fb8326d78e5120da2c7468dcf7098997be385da',
193 193 '62b4a097164940bd66030c4db51687f3ec035eed',
194 194 '536c1a19428381cfea92ac44985304f6a8049569',
195 195 '965e8ab3c44b070cdaa5bf727ddef0ada980ecc4',
196 196 '9bb326a04ae5d98d437dece54be04f830cf1edd9',
197 197 'f8940bcb890a98c4702319fbe36db75ea309b475',
198 198 'ff5ab059786ebc7411e559a2cc309dfae3625a3b',
199 199 '6b6ad5f82ad5bb6190037671bd254bd4e1f4bf08',
200 200 'ee87846a61c12153b51543bf860e1026c6d3dcba',
201 201 ]
202 202 assert org == self.repo.commit_ids[:31]
203 203
204 204 def test_iter_slice(self):
205 205 sliced = list(self.repo[:10])
206 206 itered = list(self.repo)[:10]
207 207 assert sliced == itered
208 208
209 209 def test_slicing(self):
210 210 # 4 1 5 10 95
211 211 for sfrom, sto, size in [(0, 4, 4), (1, 2, 1), (10, 15, 5),
212 212 (10, 20, 10), (5, 100, 95)]:
213 213 indexes = list(self.repo[sfrom:sto])
214 214 assert len(indexes) == size
215 215 assert indexes[0] == self.repo.get_commit(commit_idx=sfrom)
216 216 assert indexes[-1] == self.repo.get_commit(commit_idx=sto - 1)
217 217
218 218 def test_branches(self):
219 219 # TODO: Need more tests here
220 220
221 221 # active branches
222 222 assert 'default' in self.repo.branches
223 223 assert 'stable' in self.repo.branches
224 224
225 225 # closed
226 226 assert 'git' in self.repo._get_branches(closed=True)
227 227 assert 'web' in self.repo._get_branches(closed=True)
228 228
229 229 for name, id in self.repo.branches.items():
230 230 assert isinstance(self.repo.get_commit(id), MercurialCommit)
231 231
232 232 def test_tip_in_tags(self):
233 233 # tip is always a tag
234 234 assert 'tip' in self.repo.tags
235 235
236 236 def test_tip_commit_in_tags(self):
237 237 tip = self.repo.get_commit()
238 238 assert self.repo.tags['tip'] == tip.raw_id
239 239
240 240 def test_initial_commit(self):
241 241 init_commit = self.repo.get_commit(commit_idx=0)
242 242 init_author = init_commit.author
243 243
244 244 assert init_commit.message == 'initial import'
245 245 assert init_author == 'Marcin Kuzminski <marcin@python-blog.com>'
246 246 assert init_author == init_commit.committer
247 247 assert sorted(init_commit._file_paths) == sorted([
248 248 'vcs/__init__.py',
249 249 'vcs/backends/BaseRepository.py',
250 250 'vcs/backends/__init__.py',
251 251 ])
252 252 assert sorted(init_commit._dir_paths) == sorted(
253 253 ['', 'vcs', 'vcs/backends'])
254 254
255 255 assert init_commit._dir_paths + init_commit._file_paths == \
256 256 init_commit._paths
257 257
258 258 with pytest.raises(NodeDoesNotExistError):
259 259 init_commit.get_node(path='foobar')
260 260
261 261 node = init_commit.get_node('vcs/')
262 262 assert hasattr(node, 'kind')
263 263 assert node.kind == NodeKind.DIR
264 264
265 265 node = init_commit.get_node('vcs')
266 266 assert hasattr(node, 'kind')
267 267 assert node.kind == NodeKind.DIR
268 268
269 269 node = init_commit.get_node('vcs/__init__.py')
270 270 assert hasattr(node, 'kind')
271 271 assert node.kind == NodeKind.FILE
272 272
273 273 def test_not_existing_commit(self):
274 274 # rawid
275 275 with pytest.raises(RepositoryError):
276 276 self.repo.get_commit('abcd' * 10)
277 277 # shortid
278 278 with pytest.raises(RepositoryError):
279 279 self.repo.get_commit('erro' * 4)
280 280 # numeric
281 281 with pytest.raises(RepositoryError):
282 282 self.repo.get_commit(commit_idx=self.repo.count() + 1)
283 283
284 284 # Small chance we ever get to this one
285 285 idx = pow(2, 30)
286 286 with pytest.raises(RepositoryError):
287 287 self.repo.get_commit(commit_idx=idx)
288 288
289 289 def test_commit10(self):
290 290 commit10 = self.repo.get_commit(commit_idx=10)
291 291 README = """===
292 292 VCS
293 293 ===
294 294
295 295 Various Version Control System management abstraction layer for Python.
296 296
297 297 Introduction
298 298 ------------
299 299
300 300 TODO: To be written...
301 301
302 302 """
303 303 node = commit10.get_node('README.rst')
304 304 assert node.kind == NodeKind.FILE
305 305 assert node.content == README
306 306
307 307 def test_local_clone(self):
308 308 clone_path = next(REPO_PATH_GENERATOR)
309 309 self.repo._local_clone(clone_path)
310 310 repo_clone = MercurialRepository(clone_path)
311 311
312 312 assert self.repo.commit_ids == repo_clone.commit_ids
313 313
314 314 def test_local_clone_fails_if_target_exists(self):
315 315 with pytest.raises(RepositoryError):
316 316 self.repo._local_clone(self.repo.path)
317 317
318 318 def test_update(self):
319 319 repo_clone = self.get_clone_repo()
320 320 branches = repo_clone.branches
321 321
322 322 repo_clone._update('default')
323 323 assert branches['default'] == repo_clone._identify()
324 324 repo_clone._update('stable')
325 325 assert branches['stable'] == repo_clone._identify()
326 326
327 327 def test_local_pull_branch(self):
328 328 target_repo = self.get_empty_repo()
329 329 source_repo = self.get_clone_repo()
330 330
331 331 default = Reference(
332 332 'branch', 'default', source_repo.branches['default'])
333 333 target_repo._local_pull(source_repo.path, default)
334 334 target_repo = MercurialRepository(target_repo.path)
335 335 assert (target_repo.branches['default'] ==
336 336 source_repo.branches['default'])
337 337
338 338 stable = Reference('branch', 'stable', source_repo.branches['stable'])
339 339 target_repo._local_pull(source_repo.path, stable)
340 340 target_repo = MercurialRepository(target_repo.path)
341 341 assert target_repo.branches['stable'] == source_repo.branches['stable']
342 342
343 343 def test_local_pull_bookmark(self):
344 344 target_repo = self.get_empty_repo()
345 345 source_repo = self.get_clone_repo()
346 346
347 347 commits = list(source_repo.get_commits(branch_name='default'))
348 348 foo1_id = commits[-5].raw_id
349 349 foo1 = Reference('book', 'foo1', foo1_id)
350 350 source_repo._update(foo1_id)
351 351 source_repo.bookmark('foo1')
352 352
353 353 foo2_id = commits[-3].raw_id
354 354 foo2 = Reference('book', 'foo2', foo2_id)
355 355 source_repo._update(foo2_id)
356 356 source_repo.bookmark('foo2')
357 357
358 358 target_repo._local_pull(source_repo.path, foo1)
359 359 target_repo = MercurialRepository(target_repo.path)
360 360 assert target_repo.branches['default'] == commits[-5].raw_id
361 361
362 362 target_repo._local_pull(source_repo.path, foo2)
363 363 target_repo = MercurialRepository(target_repo.path)
364 364 assert target_repo.branches['default'] == commits[-3].raw_id
365 365
366 366 def test_local_pull_commit(self):
367 367 target_repo = self.get_empty_repo()
368 368 source_repo = self.get_clone_repo()
369 369
370 370 commits = list(source_repo.get_commits(branch_name='default'))
371 371 commit_id = commits[-5].raw_id
372 372 commit = Reference('rev', commit_id, commit_id)
373 373 target_repo._local_pull(source_repo.path, commit)
374 374 target_repo = MercurialRepository(target_repo.path)
375 375 assert target_repo.branches['default'] == commit_id
376 376
377 377 commit_id = commits[-3].raw_id
378 378 commit = Reference('rev', commit_id, commit_id)
379 379 target_repo._local_pull(source_repo.path, commit)
380 380 target_repo = MercurialRepository(target_repo.path)
381 381 assert target_repo.branches['default'] == commit_id
382 382
383 383 def test_local_pull_from_same_repo(self):
384 384 reference = Reference('branch', 'default', None)
385 385 with pytest.raises(ValueError):
386 386 self.repo._local_pull(self.repo.path, reference)
387 387
388 388 def test_validate_pull_reference_raises_on_missing_reference(
389 389 self, vcsbackend_hg):
390 390 target_repo = vcsbackend_hg.create_repo(number_of_commits=1)
391 391 reference = Reference(
392 392 'book', 'invalid_reference', 'a' * 40)
393 393
394 394 with pytest.raises(CommitDoesNotExistError):
395 395 target_repo._validate_pull_reference(reference)
396 396
397 397 def test_heads(self):
398 398 assert set(self.repo._heads()) == set(self.repo.branches.values())
399 399
400 400 def test_ancestor(self):
401 401 commits = [
402 402 c.raw_id for c in self.repo.get_commits(branch_name='default')]
403 403 assert self.repo._ancestor(commits[-3], commits[-5]) == commits[-5]
404 404 assert self.repo._ancestor(commits[-5], commits[-3]) == commits[-5]
405 405
406 406 def test_local_push(self):
407 407 target_repo = self.get_empty_repo()
408 408
409 409 revisions = list(self.repo.get_commits(branch_name='default'))
410 410 revision = revisions[-5].raw_id
411 411 self.repo._local_push(revision, target_repo.path)
412 412
413 413 target_repo = MercurialRepository(target_repo.path)
414 414
415 415 assert target_repo.branches['default'] == revision
416 416
417 417 def test_hooks_can_be_enabled_for_local_push(self):
418 418 revision = 'deadbeef'
419 419 repo_path = 'test_group/test_repo'
420 420 with mock.patch.object(self.repo, '_remote') as remote_mock:
421 421 self.repo._local_push(revision, repo_path, enable_hooks=True)
422 422 remote_mock.push.assert_called_once_with(
423 423 [revision], repo_path, hooks=True, push_branches=False)
424 424
425 425 def test_local_merge(self, vcsbackend_hg):
426 426 target_repo = vcsbackend_hg.create_repo(number_of_commits=1)
427 427 source_repo = vcsbackend_hg.clone_repo(target_repo)
428 428 vcsbackend_hg.add_file(target_repo, 'README_MERGE1', 'Version 1')
429 429 target_repo = MercurialRepository(target_repo.path)
430 430 target_rev = target_repo.branches['default']
431 431 target_ref = Reference(
432 432 type='branch', name='default', commit_id=target_rev)
433 433 vcsbackend_hg.add_file(source_repo, 'README_MERGE2', 'Version 2')
434 434 source_repo = MercurialRepository(source_repo.path)
435 435 source_rev = source_repo.branches['default']
436 436 source_ref = Reference(
437 437 type='branch', name='default', commit_id=source_rev)
438 438
439 439 target_repo._local_pull(source_repo.path, source_ref)
440 440
441 441 merge_message = 'Merge message\n\nDescription:...'
442 442 user_name = 'Albert Einstein'
443 443 user_email = 'albert@einstein.com'
444 444 merge_commit_id, needs_push = target_repo._local_merge(
445 445 target_ref, merge_message, user_name, user_email, source_ref)
446 446 assert needs_push
447 447
448 448 target_repo = MercurialRepository(target_repo.path)
449 449 assert target_repo.commit_ids[-3] == target_rev
450 450 assert target_repo.commit_ids[-2] == source_rev
451 451 last_commit = target_repo.get_commit(merge_commit_id)
452 452 assert last_commit.message.strip() == merge_message
453 453 assert last_commit.author == '%s <%s>' % (user_name, user_email)
454 454
455 455 assert not os.path.exists(
456 456 os.path.join(target_repo.path, '.hg', 'merge', 'state'))
457 457
458 458 def test_local_merge_source_is_fast_forward(self, vcsbackend_hg):
459 459 target_repo = vcsbackend_hg.create_repo(number_of_commits=1)
460 460 source_repo = vcsbackend_hg.clone_repo(target_repo)
461 461 target_rev = target_repo.branches['default']
462 462 target_ref = Reference(
463 463 type='branch', name='default', commit_id=target_rev)
464 464 vcsbackend_hg.add_file(source_repo, 'README_MERGE2', 'Version 2')
465 465 source_repo = MercurialRepository(source_repo.path)
466 466 source_rev = source_repo.branches['default']
467 467 source_ref = Reference(
468 468 type='branch', name='default', commit_id=source_rev)
469 469
470 470 target_repo._local_pull(source_repo.path, source_ref)
471 471
472 472 merge_message = 'Merge message\n\nDescription:...'
473 473 user_name = 'Albert Einstein'
474 474 user_email = 'albert@einstein.com'
475 475 merge_commit_id, needs_push = target_repo._local_merge(
476 476 target_ref, merge_message, user_name, user_email, source_ref)
477 477 assert merge_commit_id == source_rev
478 478 assert needs_push
479 479
480 480 target_repo = MercurialRepository(target_repo.path)
481 481 assert target_repo.commit_ids[-2] == target_rev
482 482 assert target_repo.commit_ids[-1] == source_rev
483 483
484 484 assert not os.path.exists(
485 485 os.path.join(target_repo.path, '.hg', 'merge', 'state'))
486 486
487 487 def test_local_merge_source_is_integrated(self, vcsbackend_hg):
488 488 target_repo = vcsbackend_hg.create_repo(number_of_commits=1)
489 489 target_rev = target_repo.branches['default']
490 490 target_ref = Reference(
491 491 type='branch', name='default', commit_id=target_rev)
492 492
493 493 merge_message = 'Merge message\n\nDescription:...'
494 494 user_name = 'Albert Einstein'
495 495 user_email = 'albert@einstein.com'
496 496 merge_commit_id, needs_push = target_repo._local_merge(
497 497 target_ref, merge_message, user_name, user_email, target_ref)
498 498 assert merge_commit_id == target_rev
499 499 assert not needs_push
500 500
501 501 target_repo = MercurialRepository(target_repo.path)
502 502 assert target_repo.commit_ids[-1] == target_rev
503 503
504 504 assert not os.path.exists(
505 505 os.path.join(target_repo.path, '.hg', 'merge', 'state'))
506 506
507 507 def test_local_merge_raises_exception_on_conflict(self, vcsbackend_hg):
508 508 target_repo = vcsbackend_hg.create_repo(number_of_commits=1)
509 509 source_repo = vcsbackend_hg.clone_repo(target_repo)
510 510 vcsbackend_hg.add_file(target_repo, 'README_MERGE', 'Version 1')
511 511 target_repo = MercurialRepository(target_repo.path)
512 512 target_rev = target_repo.branches['default']
513 513 target_ref = Reference(
514 514 type='branch', name='default', commit_id=target_rev)
515 515 vcsbackend_hg.add_file(source_repo, 'README_MERGE', 'Version 2')
516 516 source_repo = MercurialRepository(source_repo.path)
517 517 source_rev = source_repo.branches['default']
518 518 source_ref = Reference(
519 519 type='branch', name='default', commit_id=source_rev)
520 520
521 521 target_repo._local_pull(source_repo.path, source_ref)
522 522 with pytest.raises(RepositoryError):
523 523 target_repo._local_merge(
524 524 target_ref, 'merge_message', 'user name', 'user@name.com',
525 525 source_ref)
526 526
527 527 # Check we are not left in an intermediate merge state
528 528 assert not os.path.exists(
529 529 os.path.join(target_repo.path, '.hg', 'merge', 'state'))
530 530
531 531 def test_local_merge_of_two_branches_of_the_same_repo(self, backend_hg):
532 532 commits = [
533 533 {'message': 'a'},
534 534 {'message': 'b', 'branch': 'b'},
535 535 ]
536 536 repo = backend_hg.create_repo(commits)
537 537 commit_ids = backend_hg.commit_ids
538 538 target_ref = Reference(
539 539 type='branch', name='default', commit_id=commit_ids['a'])
540 540 source_ref = Reference(
541 541 type='branch', name='b', commit_id=commit_ids['b'])
542 542 merge_message = 'Merge message\n\nDescription:...'
543 543 user_name = 'Albert Einstein'
544 544 user_email = 'albert@einstein.com'
545 545 vcs_repo = repo.scm_instance()
546 546 merge_commit_id, needs_push = vcs_repo._local_merge(
547 547 target_ref, merge_message, user_name, user_email, source_ref)
548 548 assert merge_commit_id != source_ref.commit_id
549 549 assert needs_push is True
550 550 commit = vcs_repo.get_commit(merge_commit_id)
551 551 assert commit.merge is True
552 552 assert commit.message == merge_message
553 553
554 554 def test_maybe_prepare_merge_workspace(self):
555 555 workspace = self.repo._maybe_prepare_merge_workspace('pr2', 'unused')
556 556
557 557 assert os.path.isdir(workspace)
558 558 workspace_repo = MercurialRepository(workspace)
559 559 assert workspace_repo.branches == self.repo.branches
560 560
561 561 # Calling it a second time should also succeed
562 562 workspace = self.repo._maybe_prepare_merge_workspace('pr2', 'unused')
563 563 assert os.path.isdir(workspace)
564 564
565 565 def test_cleanup_merge_workspace(self):
566 566 workspace = self.repo._maybe_prepare_merge_workspace('pr3', 'unused')
567 567 self.repo.cleanup_merge_workspace('pr3')
568 568
569 569 assert not os.path.exists(workspace)
570 570
571 571 def test_cleanup_merge_workspace_invalid_workspace_id(self):
572 572 # No assert: because in case of an inexistent workspace this function
573 573 # should still succeed.
574 574 self.repo.cleanup_merge_workspace('pr4')
575 575
576 576 def test_merge_target_is_bookmark(self, vcsbackend_hg):
577 577 target_repo = vcsbackend_hg.create_repo(number_of_commits=1)
578 578 source_repo = vcsbackend_hg.clone_repo(target_repo)
579 579 vcsbackend_hg.add_file(target_repo, 'README_MERGE1', 'Version 1')
580 580 vcsbackend_hg.add_file(source_repo, 'README_MERGE2', 'Version 2')
581 581 imc = source_repo.in_memory_commit
582 582 imc.add(FileNode('file_x', content=source_repo.name))
583 583 imc.commit(
584 584 message=u'Automatic commit from repo merge test',
585 585 author=u'Automatic')
586 586 target_commit = target_repo.get_commit()
587 587 source_commit = source_repo.get_commit()
588 588 default_branch = target_repo.DEFAULT_BRANCH_NAME
589 589 bookmark_name = 'bookmark'
590 590 target_repo._update(default_branch)
591 591 target_repo.bookmark(bookmark_name)
592 592 target_ref = Reference('book', bookmark_name, target_commit.raw_id)
593 593 source_ref = Reference('branch', default_branch, source_commit.raw_id)
594 594 workspace = 'test-merge'
595 595
596 596 merge_response = target_repo.merge(
597 597 target_ref, source_repo, source_ref, workspace,
598 598 'test user', 'test@rhodecode.com', 'merge message 1',
599 599 dry_run=False)
600 600 expected_merge_response = MergeResponse(
601 601 True, True, merge_response.merge_ref,
602 602 MergeFailureReason.NONE)
603 603 assert merge_response == expected_merge_response
604 604
605 605 target_repo = backends.get_backend(vcsbackend_hg.alias)(
606 606 target_repo.path)
607 607 target_commits = list(target_repo.get_commits())
608 608 commit_ids = [c.raw_id for c in target_commits[:-1]]
609 609 assert source_ref.commit_id in commit_ids
610 610 assert target_ref.commit_id in commit_ids
611 611
612 612 merge_commit = target_commits[-1]
613 613 assert merge_commit.raw_id == merge_response.merge_ref.commit_id
614 614 assert merge_commit.message.strip() == 'merge message 1'
615 615 assert merge_commit.author == 'test user <test@rhodecode.com>'
616 616
617 617 # Check the bookmark was updated in the target repo
618 618 assert (
619 619 target_repo.bookmarks[bookmark_name] ==
620 620 merge_response.merge_ref.commit_id)
621 621
622 622 def test_merge_source_is_bookmark(self, vcsbackend_hg):
623 623 target_repo = vcsbackend_hg.create_repo(number_of_commits=1)
624 624 source_repo = vcsbackend_hg.clone_repo(target_repo)
625 625 imc = source_repo.in_memory_commit
626 626 imc.add(FileNode('file_x', content=source_repo.name))
627 627 imc.commit(
628 628 message=u'Automatic commit from repo merge test',
629 629 author=u'Automatic')
630 630 target_commit = target_repo.get_commit()
631 631 source_commit = source_repo.get_commit()
632 632 default_branch = target_repo.DEFAULT_BRANCH_NAME
633 633 bookmark_name = 'bookmark'
634 634 target_ref = Reference('branch', default_branch, target_commit.raw_id)
635 635 source_repo._update(default_branch)
636 636 source_repo.bookmark(bookmark_name)
637 637 source_ref = Reference('book', bookmark_name, source_commit.raw_id)
638 638 workspace = 'test-merge'
639 639
640 640 merge_response = target_repo.merge(
641 641 target_ref, source_repo, source_ref, workspace,
642 642 'test user', 'test@rhodecode.com', 'merge message 1',
643 643 dry_run=False)
644 644 expected_merge_response = MergeResponse(
645 645 True, True, merge_response.merge_ref,
646 646 MergeFailureReason.NONE)
647 647 assert merge_response == expected_merge_response
648 648
649 649 target_repo = backends.get_backend(vcsbackend_hg.alias)(
650 650 target_repo.path)
651 651 target_commits = list(target_repo.get_commits())
652 652 commit_ids = [c.raw_id for c in target_commits]
653 653 assert source_ref.commit_id == commit_ids[-1]
654 654 assert target_ref.commit_id == commit_ids[-2]
655 655
656 656 def test_merge_target_has_multiple_heads(self, vcsbackend_hg):
657 657 target_repo = vcsbackend_hg.create_repo(number_of_commits=2)
658 658 source_repo = vcsbackend_hg.clone_repo(target_repo)
659 659 vcsbackend_hg.add_file(target_repo, 'README_MERGE1', 'Version 1')
660 660 vcsbackend_hg.add_file(source_repo, 'README_MERGE2', 'Version 2')
661 661
662 662 # add an extra head to the target repo
663 663 imc = target_repo.in_memory_commit
664 664 imc.add(FileNode('file_x', content='foo'))
665 665 commits = list(target_repo.get_commits())
666 666 imc.commit(
667 667 message=u'Automatic commit from repo merge test',
668 668 author=u'Automatic', parents=commits[0:1])
669 669
670 670 target_commit = target_repo.get_commit()
671 671 source_commit = source_repo.get_commit()
672 672 default_branch = target_repo.DEFAULT_BRANCH_NAME
673 673 target_repo._update(default_branch)
674 674
675 675 target_ref = Reference('branch', default_branch, target_commit.raw_id)
676 676 source_ref = Reference('branch', default_branch, source_commit.raw_id)
677 677 workspace = 'test-merge'
678 678
679 679 assert len(target_repo._heads(branch='default')) == 2
680 680 expected_merge_response = MergeResponse(
681 681 False, False, None,
682 682 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS)
683 683 merge_response = target_repo.merge(
684 684 target_ref, source_repo, source_ref, workspace,
685 685 'test user', 'test@rhodecode.com', 'merge message 1',
686 686 dry_run=False)
687 687 assert merge_response == expected_merge_response
688 688
689 689 def test_merge_rebase_source_is_updated_bookmark(self, vcsbackend_hg):
690 690 target_repo = vcsbackend_hg.create_repo(number_of_commits=1)
691 691 source_repo = vcsbackend_hg.clone_repo(target_repo)
692 692 vcsbackend_hg.add_file(target_repo, 'README_MERGE1', 'Version 1')
693 693 vcsbackend_hg.add_file(source_repo, 'README_MERGE2', 'Version 2')
694 694 imc = source_repo.in_memory_commit
695 695 imc.add(FileNode('file_x', content=source_repo.name))
696 696 imc.commit(
697 697 message=u'Automatic commit from repo merge test',
698 698 author=u'Automatic')
699 699 target_commit = target_repo.get_commit()
700 700 source_commit = source_repo.get_commit()
701 701
702 702 vcsbackend_hg.add_file(source_repo, 'LICENSE', 'LICENSE Info')
703 703
704 704 default_branch = target_repo.DEFAULT_BRANCH_NAME
705 705 bookmark_name = 'bookmark'
706 706 source_repo._update(default_branch)
707 707 source_repo.bookmark(bookmark_name)
708 708
709 709 target_ref = Reference('branch', default_branch, target_commit.raw_id)
710 710 source_ref = Reference('book', bookmark_name, source_commit.raw_id)
711 711 workspace = 'test-merge'
712 712
713 713 merge_response = target_repo.merge(
714 714 target_ref, source_repo, source_ref, workspace,
715 715 'test user', 'test@rhodecode.com', 'merge message 1',
716 716 dry_run=False, use_rebase=True)
717 717
718 718 expected_merge_response = MergeResponse(
719 719 True, True, merge_response.merge_ref,
720 720 MergeFailureReason.NONE)
721 721 assert merge_response == expected_merge_response
722 722
723 723 target_repo = backends.get_backend(vcsbackend_hg.alias)(
724 724 target_repo.path)
725 725 last_commit = target_repo.get_commit()
726 726 assert last_commit.message == source_commit.message
727 727 assert last_commit.author == source_commit.author
728 728 # This checks that we effectively did a rebase
729 729 assert last_commit.raw_id != source_commit.raw_id
730 730
731 731 # Check the target has only 4 commits: 2 were already in target and
732 732 # only two should have been added
733 733 assert len(target_repo.commit_ids) == 2 + 2
734 734
735 735
736 736 class TestGetShadowInstance(object):
737 737
738 738 @pytest.fixture
739 739 def repo(self, vcsbackend_hg, monkeypatch):
740 740 repo = vcsbackend_hg.repo
741 741 monkeypatch.setattr(repo, 'config', mock.Mock())
742 742 monkeypatch.setattr('rhodecode.lib.vcs.connection.Hg', mock.Mock())
743 743 return repo
744 744
745 745 def test_passes_config(self, repo):
746 746 shadow = repo._get_shadow_instance(repo.path)
747 747 assert shadow.config == repo.config.copy()
748 748
749 749 def test_disables_hooks(self, repo):
750 750 shadow = repo._get_shadow_instance(repo.path)
751 751 shadow.config.clear_section.assert_called_once_with('hooks')
752 752
753 753 def test_allows_to_keep_hooks(self, repo):
754 754 shadow = repo._get_shadow_instance(repo.path, enable_hooks=True)
755 755 assert not shadow.config.clear_section.called
756 756
757 757
758 758 class TestMercurialCommit(object):
759 759
760 760 def _test_equality(self, commit):
761 761 idx = commit.idx
762 762 assert commit == self.repo.get_commit(commit_idx=idx)
763 763
764 764 def test_equality(self):
765 765 indexes = [0, 10, 20]
766 766 commits = [self.repo.get_commit(commit_idx=idx) for idx in indexes]
767 767 for commit in commits:
768 768 self._test_equality(commit)
769 769
770 770 def test_default_commit(self):
771 771 tip = self.repo.get_commit('tip')
772 772 assert tip == self.repo.get_commit()
773 773 assert tip == self.repo.get_commit(commit_id=None)
774 774 assert tip == self.repo.get_commit(commit_idx=None)
775 775 assert tip == list(self.repo[-1:])[0]
776 776
777 777 def test_root_node(self):
778 778 tip = self.repo.get_commit('tip')
779 779 assert tip.root is tip.get_node('')
780 780
781 781 def test_lazy_fetch(self):
782 782 """
783 783 Test if commit's nodes expands and are cached as we walk through
784 784 the commit. This test is somewhat hard to write as order of tests
785 785 is a key here. Written by running command after command in a shell.
786 786 """
787 787 commit = self.repo.get_commit(commit_idx=45)
788 788 assert len(commit.nodes) == 0
789 789 root = commit.root
790 790 assert len(commit.nodes) == 1
791 791 assert len(root.nodes) == 8
792 792 # accessing root.nodes updates commit.nodes
793 793 assert len(commit.nodes) == 9
794 794
795 795 docs = root.get_node('docs')
796 796 # we haven't yet accessed anything new as docs dir was already cached
797 797 assert len(commit.nodes) == 9
798 798 assert len(docs.nodes) == 8
799 799 # accessing docs.nodes updates commit.nodes
800 800 assert len(commit.nodes) == 17
801 801
802 802 assert docs is commit.get_node('docs')
803 803 assert docs is root.nodes[0]
804 804 assert docs is root.dirs[0]
805 805 assert docs is commit.get_node('docs')
806 806
807 807 def test_nodes_with_commit(self):
808 808 commit = self.repo.get_commit(commit_idx=45)
809 809 root = commit.root
810 810 docs = root.get_node('docs')
811 811 assert docs is commit.get_node('docs')
812 812 api = docs.get_node('api')
813 813 assert api is commit.get_node('docs/api')
814 814 index = api.get_node('index.rst')
815 815 assert index is commit.get_node('docs/api/index.rst')
816 816 assert index is commit.get_node(
817 817 'docs').get_node('api').get_node('index.rst')
818 818
819 819 def test_branch_and_tags(self):
820 820 commit0 = self.repo.get_commit(commit_idx=0)
821 821 assert commit0.branch == 'default'
822 822 assert commit0.tags == []
823 823
824 824 commit10 = self.repo.get_commit(commit_idx=10)
825 825 assert commit10.branch == 'default'
826 826 assert commit10.tags == []
827 827
828 828 commit44 = self.repo.get_commit(commit_idx=44)
829 829 assert commit44.branch == 'web'
830 830
831 831 tip = self.repo.get_commit('tip')
832 832 assert 'tip' in tip.tags
833 833
834 834 def test_bookmarks(self):
835 835 commit0 = self.repo.get_commit(commit_idx=0)
836 836 assert commit0.bookmarks == []
837 837
838 838 def _test_file_size(self, idx, path, size):
839 839 node = self.repo.get_commit(commit_idx=idx).get_node(path)
840 840 assert node.is_file()
841 841 assert node.size == size
842 842
843 843 def test_file_size(self):
844 844 to_check = (
845 845 (10, 'setup.py', 1068),
846 846 (20, 'setup.py', 1106),
847 847 (60, 'setup.py', 1074),
848 848
849 849 (10, 'vcs/backends/base.py', 2921),
850 850 (20, 'vcs/backends/base.py', 3936),
851 851 (60, 'vcs/backends/base.py', 6189),
852 852 )
853 853 for idx, path, size in to_check:
854 854 self._test_file_size(idx, path, size)
855 855
856 856 def test_file_history_from_commits(self):
857 857 node = self.repo[10].get_node('setup.py')
858 858 commit_ids = [commit.raw_id for commit in node.history]
859 859 assert ['3803844fdbd3b711175fc3da9bdacfcd6d29a6fb'] == commit_ids
860 860
861 861 node = self.repo[20].get_node('setup.py')
862 862 node_ids = [commit.raw_id for commit in node.history]
863 863 assert ['eada5a770da98ab0dd7325e29d00e0714f228d09',
864 864 '3803844fdbd3b711175fc3da9bdacfcd6d29a6fb'] == node_ids
865 865
866 866 # special case we check history from commit that has this particular
867 867 # file changed this means we check if it's included as well
868 868 node = self.repo.get_commit('eada5a770da98ab0dd7325e29d00e0714f228d09')\
869 869 .get_node('setup.py')
870 870 node_ids = [commit.raw_id for commit in node.history]
871 871 assert ['eada5a770da98ab0dd7325e29d00e0714f228d09',
872 872 '3803844fdbd3b711175fc3da9bdacfcd6d29a6fb'] == node_ids
873 873
874 874 def test_file_history(self):
875 875 # we can only check if those commits are present in the history
876 876 # as we cannot update this test every time file is changed
877 877 files = {
878 878 'setup.py': [7, 18, 45, 46, 47, 69, 77],
879 879 'vcs/nodes.py': [
880 880 7, 8, 24, 26, 30, 45, 47, 49, 56, 57, 58, 59, 60, 61, 73, 76],
881 881 'vcs/backends/hg.py': [
882 882 4, 5, 6, 11, 12, 13, 14, 15, 16, 21, 22, 23, 26, 27, 28, 30,
883 883 31, 33, 35, 36, 37, 38, 39, 40, 41, 44, 45, 47, 48, 49, 53, 54,
884 884 55, 58, 60, 61, 67, 68, 69, 70, 73, 77, 78, 79, 82],
885 885 }
886 886 for path, indexes in files.items():
887 887 tip = self.repo.get_commit(commit_idx=indexes[-1])
888 888 node = tip.get_node(path)
889 889 node_indexes = [commit.idx for commit in node.history]
890 890 assert set(indexes).issubset(set(node_indexes)), (
891 891 "We assumed that %s is subset of commits for which file %s "
892 892 "has been changed, and history of that node returned: %s"
893 893 % (indexes, path, node_indexes))
894 894
895 895 def test_file_annotate(self):
896 896 files = {
897 897 'vcs/backends/__init__.py': {
898 898 89: {
899 899 'lines_no': 31,
900 900 'commits': [
901 901 32, 32, 61, 32, 32, 37, 32, 32, 32, 44,
902 902 37, 37, 37, 37, 45, 37, 44, 37, 37, 37,
903 903 32, 32, 32, 32, 37, 32, 37, 37, 32,
904 904 32, 32
905 905 ]
906 906 },
907 907 20: {
908 908 'lines_no': 1,
909 909 'commits': [4]
910 910 },
911 911 55: {
912 912 'lines_no': 31,
913 913 'commits': [
914 914 32, 32, 45, 32, 32, 37, 32, 32, 32, 44,
915 915 37, 37, 37, 37, 45, 37, 44, 37, 37, 37,
916 916 32, 32, 32, 32, 37, 32, 37, 37, 32,
917 917 32, 32
918 918 ]
919 919 }
920 920 },
921 921 'vcs/exceptions.py': {
922 922 89: {
923 923 'lines_no': 18,
924 924 'commits': [
925 925 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
926 926 16, 16, 17, 16, 16, 18, 18, 18
927 927 ]
928 928 },
929 929 20: {
930 930 'lines_no': 18,
931 931 'commits': [
932 932 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
933 933 16, 16, 17, 16, 16, 18, 18, 18
934 934 ]
935 935 },
936 936 55: {
937 937 'lines_no': 18,
938 938 'commits': [
939 939 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
940 940 17, 16, 16, 18, 18, 18
941 941 ]
942 942 }
943 943 },
944 944 'MANIFEST.in': {
945 945 89: {
946 946 'lines_no': 5,
947 947 'commits': [7, 7, 7, 71, 71]
948 948 },
949 949 20: {
950 950 'lines_no': 3,
951 951 'commits': [7, 7, 7]
952 952 },
953 953 55: {
954 954 'lines_no': 3,
955 955 'commits': [7, 7, 7]
956 956 }
957 957 }
958 958 }
959 959
960 960 for fname, commit_dict in files.items():
961 961 for idx, __ in commit_dict.items():
962 962 commit = self.repo.get_commit(commit_idx=idx)
963 963 l1_1 = [x[1] for x in commit.get_file_annotate(fname)]
964 964 l1_2 = [x[2]().raw_id for x in commit.get_file_annotate(fname)]
965 965 assert l1_1 == l1_2
966 966 l1 = l1_2 = [
967 967 x[2]().idx for x in commit.get_file_annotate(fname)]
968 968 l2 = files[fname][idx]['commits']
969 969 assert l1 == l2, (
970 970 "The lists of commit for %s@commit_id%s"
971 971 "from annotation list should match each other,"
972 972 "got \n%s \nvs \n%s " % (fname, idx, l1, l2))
973 973
974 974 def test_commit_state(self):
975 975 """
976 976 Tests which files have been added/changed/removed at particular commit
977 977 """
978 978
979 979 # commit_id 46ad32a4f974:
980 980 # hg st --rev 46ad32a4f974
981 981 # changed: 13
982 982 # added: 20
983 983 # removed: 1
984 984 changed = set([
985 985 '.hgignore', 'README.rst', 'docs/conf.py', 'docs/index.rst',
986 986 'setup.py', 'tests/test_hg.py', 'tests/test_nodes.py',
987 987 'vcs/__init__.py', 'vcs/backends/__init__.py',
988 988 'vcs/backends/base.py', 'vcs/backends/hg.py', 'vcs/nodes.py',
989 989 'vcs/utils/__init__.py'])
990 990
991 991 added = set([
992 992 'docs/api/backends/hg.rst', 'docs/api/backends/index.rst',
993 993 'docs/api/index.rst', 'docs/api/nodes.rst',
994 994 'docs/api/web/index.rst', 'docs/api/web/simplevcs.rst',
995 995 'docs/installation.rst', 'docs/quickstart.rst', 'setup.cfg',
996 996 'vcs/utils/baseui_config.py', 'vcs/utils/web.py',
997 997 'vcs/web/__init__.py', 'vcs/web/exceptions.py',
998 998 'vcs/web/simplevcs/__init__.py', 'vcs/web/simplevcs/exceptions.py',
999 999 'vcs/web/simplevcs/middleware.py', 'vcs/web/simplevcs/models.py',
1000 1000 'vcs/web/simplevcs/settings.py', 'vcs/web/simplevcs/utils.py',
1001 1001 'vcs/web/simplevcs/views.py'])
1002 1002
1003 1003 removed = set(['docs/api.rst'])
1004 1004
1005 1005 commit64 = self.repo.get_commit('46ad32a4f974')
1006 1006 assert set((node.path for node in commit64.added)) == added
1007 1007 assert set((node.path for node in commit64.changed)) == changed
1008 1008 assert set((node.path for node in commit64.removed)) == removed
1009 1009
1010 1010 # commit_id b090f22d27d6:
1011 1011 # hg st --rev b090f22d27d6
1012 1012 # changed: 13
1013 1013 # added: 20
1014 1014 # removed: 1
1015 1015 commit88 = self.repo.get_commit('b090f22d27d6')
1016 1016 assert set((node.path for node in commit88.added)) == set()
1017 1017 assert set((node.path for node in commit88.changed)) == \
1018 1018 set(['.hgignore'])
1019 1019 assert set((node.path for node in commit88.removed)) == set()
1020 1020
1021 1021 #
1022 1022 # 85:
1023 1023 # added: 2 [
1024 1024 # 'vcs/utils/diffs.py', 'vcs/web/simplevcs/views/diffs.py']
1025 1025 # changed: 4 ['vcs/web/simplevcs/models.py', ...]
1026 1026 # removed: 1 ['vcs/utils/web.py']
1027 1027 commit85 = self.repo.get_commit(commit_idx=85)
1028 1028 assert set((node.path for node in commit85.added)) == set([
1029 1029 'vcs/utils/diffs.py',
1030 1030 'vcs/web/simplevcs/views/diffs.py'])
1031 1031 assert set((node.path for node in commit85.changed)) == set([
1032 1032 'vcs/web/simplevcs/models.py',
1033 1033 'vcs/web/simplevcs/utils.py',
1034 1034 'vcs/web/simplevcs/views/__init__.py',
1035 1035 'vcs/web/simplevcs/views/repository.py',
1036 1036 ])
1037 1037 assert set((node.path for node in commit85.removed)) == \
1038 1038 set(['vcs/utils/web.py'])
1039 1039
1040 1040 def test_files_state(self):
1041 1041 """
1042 1042 Tests state of FileNodes.
1043 1043 """
1044 1044 commit = self.repo.get_commit(commit_idx=85)
1045 1045 node = commit.get_node('vcs/utils/diffs.py')
1046 1046 assert node.state, NodeState.ADDED
1047 1047 assert node.added
1048 1048 assert not node.changed
1049 1049 assert not node.not_changed
1050 1050 assert not node.removed
1051 1051
1052 1052 commit = self.repo.get_commit(commit_idx=88)
1053 1053 node = commit.get_node('.hgignore')
1054 1054 assert node.state, NodeState.CHANGED
1055 1055 assert not node.added
1056 1056 assert node.changed
1057 1057 assert not node.not_changed
1058 1058 assert not node.removed
1059 1059
1060 1060 commit = self.repo.get_commit(commit_idx=85)
1061 1061 node = commit.get_node('setup.py')
1062 1062 assert node.state, NodeState.NOT_CHANGED
1063 1063 assert not node.added
1064 1064 assert not node.changed
1065 1065 assert node.not_changed
1066 1066 assert not node.removed
1067 1067
1068 1068 # If node has REMOVED state then trying to fetch it would raise
1069 1069 # CommitError exception
1070 1070 commit = self.repo.get_commit(commit_idx=2)
1071 1071 path = 'vcs/backends/BaseRepository.py'
1072 1072 with pytest.raises(NodeDoesNotExistError):
1073 1073 commit.get_node(path)
1074 1074 # but it would be one of ``removed`` (commit's attribute)
1075 1075 assert path in [rf.path for rf in commit.removed]
1076 1076
1077 1077 def test_commit_message_is_unicode(self):
1078 1078 for cm in self.repo:
1079 1079 assert type(cm.message) == unicode
1080 1080
1081 1081 def test_commit_author_is_unicode(self):
1082 1082 for cm in self.repo:
1083 1083 assert type(cm.author) == unicode
1084 1084
1085 1085 def test_repo_files_content_is_unicode(self):
1086 1086 test_commit = self.repo.get_commit(commit_idx=100)
1087 1087 for node in test_commit.get_node('/'):
1088 1088 if node.is_file():
1089 1089 assert type(node.content) == unicode
1090 1090
1091 1091 def test_wrong_path(self):
1092 1092 # There is 'setup.py' in the root dir but not there:
1093 1093 path = 'foo/bar/setup.py'
1094 1094 with pytest.raises(VCSError):
1095 1095 self.repo.get_commit().get_node(path)
1096 1096
1097 1097 def test_author_email(self):
1098 1098 assert 'marcin@python-blog.com' == \
1099 1099 self.repo.get_commit('b986218ba1c9').author_email
1100 1100 assert 'lukasz.balcerzak@python-center.pl' == \
1101 1101 self.repo.get_commit('3803844fdbd3').author_email
1102 1102 assert '' == self.repo.get_commit('84478366594b').author_email
1103 1103
1104 1104 def test_author_username(self):
1105 1105 assert 'Marcin Kuzminski' == \
1106 1106 self.repo.get_commit('b986218ba1c9').author_name
1107 1107 assert 'Lukasz Balcerzak' == \
1108 1108 self.repo.get_commit('3803844fdbd3').author_name
1109 1109 assert 'marcink' == \
1110 1110 self.repo.get_commit('84478366594b').author_name
1111 1111
1112 1112
1113 1113 class TestLargeFileRepo(object):
1114 1114
1115 1115 def test_large_file(self, backend_hg):
1116 1116 repo = backend_hg.create_test_repo('largefiles', make_db_config())
1117 1117
1118 1118 tip = repo.scm_instance().get_commit()
1119 1119 node = tip.get_node('.hglf/thisfileislarge')
1120 1120
1121 1121 lf_node = node.get_largefile_node()
1122 1122
1123 1123 assert lf_node.is_largefile() is True
1124 1124 assert lf_node.size == 1024000
1125 1125 assert lf_node.name == '.hglf/thisfileislarge'
1126 1126
1127 1127
1128 1128 class TestGetBranchName(object):
1129 1129 def test_returns_ref_name_when_type_is_branch(self):
1130 1130 ref = self._create_ref('branch', 'fake-name')
1131 1131 result = self.repo._get_branch_name(ref)
1132 1132 assert result == ref.name
1133 1133
1134 1134 @pytest.mark.parametrize("type_", ("book", "tag"))
1135 1135 def test_queries_remote_when_type_is_not_branch(self, type_):
1136 1136 ref = self._create_ref(type_, 'wrong-fake-name')
1137 1137 with mock.patch.object(self.repo, "_remote") as remote_mock:
1138 1138 remote_mock.ctx_branch.return_value = "fake-name"
1139 1139 result = self.repo._get_branch_name(ref)
1140 1140 assert result == "fake-name"
1141 1141 remote_mock.ctx_branch.assert_called_once_with(ref.commit_id)
1142 1142
1143 1143 def _create_ref(self, type_, name):
1144 1144 ref = mock.Mock()
1145 1145 ref.type = type_
1146 1146 ref.name = 'wrong-fake-name'
1147 1147 ref.commit_id = "deadbeef"
1148 1148 return ref
1149 1149
1150 1150
1151 1151 class TestIsTheSameBranch(object):
1152 1152 def test_returns_true_when_branches_are_equal(self):
1153 1153 source_ref = mock.Mock(name="source-ref")
1154 1154 target_ref = mock.Mock(name="target-ref")
1155 1155 branch_name_patcher = mock.patch.object(
1156 1156 self.repo, "_get_branch_name", return_value="default")
1157 1157 with branch_name_patcher as branch_name_mock:
1158 1158 result = self.repo._is_the_same_branch(source_ref, target_ref)
1159 1159
1160 1160 expected_calls = [mock.call(source_ref), mock.call(target_ref)]
1161 1161 assert branch_name_mock.call_args_list == expected_calls
1162 1162 assert result is True
1163 1163
1164 1164 def test_returns_false_when_branches_are_not_equal(self):
1165 1165 source_ref = mock.Mock(name="source-ref")
1166 1166 source_ref.name = "source-branch"
1167 1167 target_ref = mock.Mock(name="target-ref")
1168 1168 source_ref.name = "target-branch"
1169 1169
1170 1170 def side_effect(ref):
1171 1171 return ref.name
1172 1172
1173 1173 branch_name_patcher = mock.patch.object(
1174 1174 self.repo, "_get_branch_name", side_effect=side_effect)
1175 1175 with branch_name_patcher as branch_name_mock:
1176 1176 result = self.repo._is_the_same_branch(source_ref, target_ref)
1177 1177
1178 1178 expected_calls = [mock.call(source_ref), mock.call(target_ref)]
1179 1179 assert branch_name_mock.call_args_list == expected_calls
1180 1180 assert result is False
General Comments 0
You need to be logged in to leave comments. Login now