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