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