##// END OF EJS Templates
git: added identify command for checking the current checkout commit
marcink -
r2057:971549ce default
parent child Browse files
Show More
@@ -1,933 +1,946 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2014-2017 RhodeCode GmbH
3 # Copyright (C) 2014-2017 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
29
30 from zope.cachedescriptors.property import Lazy as LazyProperty
30 from zope.cachedescriptors.property import Lazy as LazyProperty
31
31
32 from rhodecode.lib.compat import OrderedDict
32 from rhodecode.lib.compat import OrderedDict
33 from rhodecode.lib.datelib import (
33 from rhodecode.lib.datelib import (
34 utcdate_fromtimestamp, makedate, date_astimestamp)
34 utcdate_fromtimestamp, makedate, date_astimestamp)
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, Reference)
39 MergeFailureReason, Reference)
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.exceptions import (
43 from rhodecode.lib.vcs.exceptions import (
44 CommitDoesNotExistError, EmptyRepositoryError,
44 CommitDoesNotExistError, EmptyRepositoryError,
45 RepositoryError, TagAlreadyExistError, TagDoesNotExistError, VCSError)
45 RepositoryError, TagAlreadyExistError, TagDoesNotExistError, VCSError)
46
46
47
47
48 SHA_PATTERN = re.compile(r'^[[0-9a-fA-F]{12}|[0-9a-fA-F]{40}]$')
48 SHA_PATTERN = re.compile(r'^[[0-9a-fA-F]{12}|[0-9a-fA-F]{40}]$')
49
49
50 log = logging.getLogger(__name__)
50 log = logging.getLogger(__name__)
51
51
52
52
53 class GitRepository(BaseRepository):
53 class GitRepository(BaseRepository):
54 """
54 """
55 Git repository backend.
55 Git repository backend.
56 """
56 """
57 DEFAULT_BRANCH_NAME = 'master'
57 DEFAULT_BRANCH_NAME = 'master'
58
58
59 contact = BaseRepository.DEFAULT_CONTACT
59 contact = BaseRepository.DEFAULT_CONTACT
60
60
61 def __init__(self, repo_path, config=None, create=False, src_url=None,
61 def __init__(self, repo_path, config=None, create=False, src_url=None,
62 update_after_clone=False, with_wire=None, bare=False):
62 update_after_clone=False, with_wire=None, bare=False):
63
63
64 self.path = safe_str(os.path.abspath(repo_path))
64 self.path = safe_str(os.path.abspath(repo_path))
65 self.config = config if config else Config()
65 self.config = config if config else Config()
66 self._remote = connection.Git(
66 self._remote = connection.Git(
67 self.path, self.config, with_wire=with_wire)
67 self.path, self.config, with_wire=with_wire)
68
68
69 self._init_repo(create, src_url, update_after_clone, bare)
69 self._init_repo(create, src_url, update_after_clone, bare)
70
70
71 # caches
71 # caches
72 self._commit_ids = {}
72 self._commit_ids = {}
73
73
74 self.bookmarks = {}
74 self.bookmarks = {}
75
75
76 @LazyProperty
76 @LazyProperty
77 def bare(self):
77 def bare(self):
78 return self._remote.bare()
78 return self._remote.bare()
79
79
80 @LazyProperty
80 @LazyProperty
81 def head(self):
81 def head(self):
82 return self._remote.head()
82 return self._remote.head()
83
83
84 @LazyProperty
84 @LazyProperty
85 def commit_ids(self):
85 def commit_ids(self):
86 """
86 """
87 Returns list of commit ids, in ascending order. Being lazy
87 Returns list of commit ids, in ascending order. Being lazy
88 attribute allows external tools to inject commit ids from cache.
88 attribute allows external tools to inject commit ids from cache.
89 """
89 """
90 commit_ids = self._get_all_commit_ids()
90 commit_ids = self._get_all_commit_ids()
91 self._rebuild_cache(commit_ids)
91 self._rebuild_cache(commit_ids)
92 return commit_ids
92 return commit_ids
93
93
94 def _rebuild_cache(self, commit_ids):
94 def _rebuild_cache(self, commit_ids):
95 self._commit_ids = dict((commit_id, index)
95 self._commit_ids = dict((commit_id, index)
96 for index, commit_id in enumerate(commit_ids))
96 for index, commit_id in enumerate(commit_ids))
97
97
98 def run_git_command(self, cmd, **opts):
98 def run_git_command(self, cmd, **opts):
99 """
99 """
100 Runs given ``cmd`` as git command and returns tuple
100 Runs given ``cmd`` as git command and returns tuple
101 (stdout, stderr).
101 (stdout, stderr).
102
102
103 :param cmd: git command to be executed
103 :param cmd: git command to be executed
104 :param opts: env options to pass into Subprocess command
104 :param opts: env options to pass into Subprocess command
105 """
105 """
106 if not isinstance(cmd, list):
106 if not isinstance(cmd, list):
107 raise ValueError('cmd must be a list, got %s instead' % type(cmd))
107 raise ValueError('cmd must be a list, got %s instead' % type(cmd))
108
108
109 out, err = self._remote.run_git_command(cmd, **opts)
109 out, err = self._remote.run_git_command(cmd, **opts)
110 if err:
110 if err:
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_commit_id(self, commit_id_or_idx):
208 def _get_commit_id(self, commit_id_or_idx):
209 def is_null(value):
209 def is_null(value):
210 return len(value) == commit_id_or_idx.count('0')
210 return len(value) == commit_id_or_idx.count('0')
211
211
212 if self.is_empty():
212 if self.is_empty():
213 raise EmptyRepositoryError("There are no commits yet")
213 raise EmptyRepositoryError("There are no commits yet")
214
214
215 if commit_id_or_idx in (None, '', 'tip', 'HEAD', 'head', -1):
215 if commit_id_or_idx in (None, '', 'tip', 'HEAD', 'head', -1):
216 return self.commit_ids[-1]
216 return self.commit_ids[-1]
217
217
218 is_bstr = isinstance(commit_id_or_idx, (str, unicode))
218 is_bstr = isinstance(commit_id_or_idx, (str, unicode))
219 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)
220 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)):
221 try:
221 try:
222 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)]
223 except Exception:
223 except Exception:
224 msg = "Commit %s does not exist for %s" % (
224 msg = "Commit %s does not exist for %s" % (
225 commit_id_or_idx, self)
225 commit_id_or_idx, self)
226 raise CommitDoesNotExistError(msg)
226 raise CommitDoesNotExistError(msg)
227
227
228 elif is_bstr:
228 elif is_bstr:
229 # check full path ref, eg. refs/heads/master
229 # check full path ref, eg. refs/heads/master
230 ref_id = self._refs.get(commit_id_or_idx)
230 ref_id = self._refs.get(commit_id_or_idx)
231 if ref_id:
231 if ref_id:
232 return ref_id
232 return ref_id
233
233
234 # check branch name
234 # check branch name
235 branch_ids = self.branches.values()
235 branch_ids = self.branches.values()
236 ref_id = self._refs.get('refs/heads/%s' % commit_id_or_idx)
236 ref_id = self._refs.get('refs/heads/%s' % commit_id_or_idx)
237 if ref_id:
237 if ref_id:
238 return ref_id
238 return ref_id
239
239
240 # check tag name
240 # check tag name
241 ref_id = self._refs.get('refs/tags/%s' % commit_id_or_idx)
241 ref_id = self._refs.get('refs/tags/%s' % commit_id_or_idx)
242 if ref_id:
242 if ref_id:
243 return ref_id
243 return ref_id
244
244
245 if (not SHA_PATTERN.match(commit_id_or_idx) or
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 try:
272 try:
273 return self.get_commit().date
273 return self.get_commit().date
274 except RepositoryError:
274 except RepositoryError:
275 tzoffset = makedate()[1]
275 tzoffset = makedate()[1]
276 return utcdate_fromtimestamp(self._get_fs_mtime(), tzoffset)
276 return utcdate_fromtimestamp(self._get_fs_mtime(), tzoffset)
277
277
278 def _get_fs_mtime(self):
278 def _get_fs_mtime(self):
279 idx_loc = '' if self.bare else '.git'
279 idx_loc = '' if self.bare else '.git'
280 # fallback to filesystem
280 # fallback to filesystem
281 in_path = os.path.join(self.path, idx_loc, "index")
281 in_path = os.path.join(self.path, idx_loc, "index")
282 he_path = os.path.join(self.path, idx_loc, "HEAD")
282 he_path = os.path.join(self.path, idx_loc, "HEAD")
283 if os.path.exists(in_path):
283 if os.path.exists(in_path):
284 return os.stat(in_path).st_mtime
284 return os.stat(in_path).st_mtime
285 else:
285 else:
286 return os.stat(he_path).st_mtime
286 return os.stat(he_path).st_mtime
287
287
288 @LazyProperty
288 @LazyProperty
289 def description(self):
289 def description(self):
290 description = self._remote.get_description()
290 description = self._remote.get_description()
291 return safe_unicode(description or self.DEFAULT_DESCRIPTION)
291 return safe_unicode(description or self.DEFAULT_DESCRIPTION)
292
292
293 def _get_refs_entries(self, prefix='', reverse=False, strip_prefix=True):
293 def _get_refs_entries(self, prefix='', reverse=False, strip_prefix=True):
294 if self.is_empty():
294 if self.is_empty():
295 return OrderedDict()
295 return OrderedDict()
296
296
297 result = []
297 result = []
298 for ref, sha in self._refs.iteritems():
298 for ref, sha in self._refs.iteritems():
299 if ref.startswith(prefix):
299 if ref.startswith(prefix):
300 ref_name = ref
300 ref_name = ref
301 if strip_prefix:
301 if strip_prefix:
302 ref_name = ref[len(prefix):]
302 ref_name = ref[len(prefix):]
303 result.append((safe_unicode(ref_name), sha))
303 result.append((safe_unicode(ref_name), sha))
304
304
305 def get_name(entry):
305 def get_name(entry):
306 return entry[0]
306 return entry[0]
307
307
308 return OrderedDict(sorted(result, key=get_name, reverse=reverse))
308 return OrderedDict(sorted(result, key=get_name, reverse=reverse))
309
309
310 def _get_branches(self):
310 def _get_branches(self):
311 return self._get_refs_entries(prefix='refs/heads/', strip_prefix=True)
311 return self._get_refs_entries(prefix='refs/heads/', strip_prefix=True)
312
312
313 @LazyProperty
313 @LazyProperty
314 def branches(self):
314 def branches(self):
315 return self._get_branches()
315 return self._get_branches()
316
316
317 @LazyProperty
317 @LazyProperty
318 def branches_closed(self):
318 def branches_closed(self):
319 return {}
319 return {}
320
320
321 @LazyProperty
321 @LazyProperty
322 def branches_all(self):
322 def branches_all(self):
323 all_branches = {}
323 all_branches = {}
324 all_branches.update(self.branches)
324 all_branches.update(self.branches)
325 all_branches.update(self.branches_closed)
325 all_branches.update(self.branches_closed)
326 return all_branches
326 return all_branches
327
327
328 @LazyProperty
328 @LazyProperty
329 def tags(self):
329 def tags(self):
330 return self._get_tags()
330 return self._get_tags()
331
331
332 def _get_tags(self):
332 def _get_tags(self):
333 return self._get_refs_entries(
333 return self._get_refs_entries(
334 prefix='refs/tags/', strip_prefix=True, reverse=True)
334 prefix='refs/tags/', strip_prefix=True, reverse=True)
335
335
336 def tag(self, name, user, commit_id=None, message=None, date=None,
336 def tag(self, name, user, commit_id=None, message=None, date=None,
337 **kwargs):
337 **kwargs):
338 # TODO: fix this method to apply annotated tags correct with message
338 # TODO: fix this method to apply annotated tags correct with message
339 """
339 """
340 Creates and returns a tag for the given ``commit_id``.
340 Creates and returns a tag for the given ``commit_id``.
341
341
342 :param name: name for new tag
342 :param name: name for new tag
343 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
343 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
344 :param commit_id: commit id for which new tag would be created
344 :param commit_id: commit id for which new tag would be created
345 :param message: message of the tag's commit
345 :param message: message of the tag's commit
346 :param date: date of tag's commit
346 :param date: date of tag's commit
347
347
348 :raises TagAlreadyExistError: if tag with same name already exists
348 :raises TagAlreadyExistError: if tag with same name already exists
349 """
349 """
350 if name in self.tags:
350 if name in self.tags:
351 raise TagAlreadyExistError("Tag %s already exists" % name)
351 raise TagAlreadyExistError("Tag %s already exists" % name)
352 commit = self.get_commit(commit_id=commit_id)
352 commit = self.get_commit(commit_id=commit_id)
353 message = message or "Added tag %s for commit %s" % (
353 message = message or "Added tag %s for commit %s" % (
354 name, commit.raw_id)
354 name, commit.raw_id)
355 self._remote.set_refs('refs/tags/%s' % name, commit._commit['id'])
355 self._remote.set_refs('refs/tags/%s' % name, commit._commit['id'])
356
356
357 self._refs = self._get_refs()
357 self._refs = self._get_refs()
358 self.tags = self._get_tags()
358 self.tags = self._get_tags()
359 return commit
359 return commit
360
360
361 def remove_tag(self, name, user, message=None, date=None):
361 def remove_tag(self, name, user, message=None, date=None):
362 """
362 """
363 Removes tag with the given ``name``.
363 Removes tag with the given ``name``.
364
364
365 :param name: name of the tag to be removed
365 :param name: name of the tag to be removed
366 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
366 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
367 :param message: message of the tag's removal commit
367 :param message: message of the tag's removal commit
368 :param date: date of tag's removal commit
368 :param date: date of tag's removal commit
369
369
370 :raises TagDoesNotExistError: if tag with given name does not exists
370 :raises TagDoesNotExistError: if tag with given name does not exists
371 """
371 """
372 if name not in self.tags:
372 if name not in self.tags:
373 raise TagDoesNotExistError("Tag %s does not exist" % name)
373 raise TagDoesNotExistError("Tag %s does not exist" % name)
374 tagpath = vcspath.join(
374 tagpath = vcspath.join(
375 self._remote.get_refs_path(), 'refs', 'tags', name)
375 self._remote.get_refs_path(), 'refs', 'tags', name)
376 try:
376 try:
377 os.remove(tagpath)
377 os.remove(tagpath)
378 self._refs = self._get_refs()
378 self._refs = self._get_refs()
379 self.tags = self._get_tags()
379 self.tags = self._get_tags()
380 except OSError as e:
380 except OSError as e:
381 raise RepositoryError(e.strerror)
381 raise RepositoryError(e.strerror)
382
382
383 def _get_refs(self):
383 def _get_refs(self):
384 return self._remote.get_refs()
384 return self._remote.get_refs()
385
385
386 @LazyProperty
386 @LazyProperty
387 def _refs(self):
387 def _refs(self):
388 return self._get_refs()
388 return self._get_refs()
389
389
390 @property
390 @property
391 def _ref_tree(self):
391 def _ref_tree(self):
392 node = tree = {}
392 node = tree = {}
393 for ref, sha in self._refs.iteritems():
393 for ref, sha in self._refs.iteritems():
394 path = ref.split('/')
394 path = ref.split('/')
395 for bit in path[:-1]:
395 for bit in path[:-1]:
396 node = node.setdefault(bit, {})
396 node = node.setdefault(bit, {})
397 node[path[-1]] = sha
397 node[path[-1]] = sha
398 node = tree
398 node = tree
399 return tree
399 return tree
400
400
401 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
401 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
402 """
402 """
403 Returns `GitCommit` object representing commit from git repository
403 Returns `GitCommit` object representing commit from git repository
404 at the given `commit_id` or head (most recent commit) if None given.
404 at the given `commit_id` or head (most recent commit) if None given.
405 """
405 """
406 if commit_id is not None:
406 if commit_id is not None:
407 self._validate_commit_id(commit_id)
407 self._validate_commit_id(commit_id)
408 elif commit_idx is not None:
408 elif commit_idx is not None:
409 self._validate_commit_idx(commit_idx)
409 self._validate_commit_idx(commit_idx)
410 commit_id = commit_idx
410 commit_id = commit_idx
411 commit_id = self._get_commit_id(commit_id)
411 commit_id = self._get_commit_id(commit_id)
412 try:
412 try:
413 # Need to call remote to translate id for tagging scenario
413 # Need to call remote to translate id for tagging scenario
414 commit_id = self._remote.get_object(commit_id)["commit_id"]
414 commit_id = self._remote.get_object(commit_id)["commit_id"]
415 idx = self._commit_ids[commit_id]
415 idx = self._commit_ids[commit_id]
416 except KeyError:
416 except KeyError:
417 raise RepositoryError("Cannot get object with id %s" % commit_id)
417 raise RepositoryError("Cannot get object with id %s" % commit_id)
418
418
419 return GitCommit(self, commit_id, idx, pre_load=pre_load)
419 return GitCommit(self, commit_id, idx, pre_load=pre_load)
420
420
421 def get_commits(
421 def get_commits(
422 self, start_id=None, end_id=None, start_date=None, end_date=None,
422 self, start_id=None, end_id=None, start_date=None, end_date=None,
423 branch_name=None, pre_load=None):
423 branch_name=None, pre_load=None):
424 """
424 """
425 Returns generator of `GitCommit` objects from start to end (both
425 Returns generator of `GitCommit` objects from start to end (both
426 are inclusive), in ascending date order.
426 are inclusive), in ascending date order.
427
427
428 :param start_id: None, str(commit_id)
428 :param start_id: None, str(commit_id)
429 :param end_id: None, str(commit_id)
429 :param end_id: None, str(commit_id)
430 :param start_date: if specified, commits with commit date less than
430 :param start_date: if specified, commits with commit date less than
431 ``start_date`` would be filtered out from returned set
431 ``start_date`` would be filtered out from returned set
432 :param end_date: if specified, commits with commit date greater than
432 :param end_date: if specified, commits with commit date greater than
433 ``end_date`` would be filtered out from returned set
433 ``end_date`` would be filtered out from returned set
434 :param branch_name: if specified, commits not reachable from given
434 :param branch_name: if specified, commits not reachable from given
435 branch would be filtered out from returned set
435 branch would be filtered out from returned set
436
436
437 :raise BranchDoesNotExistError: If given `branch_name` does not
437 :raise BranchDoesNotExistError: If given `branch_name` does not
438 exist.
438 exist.
439 :raise CommitDoesNotExistError: If commits for given `start` or
439 :raise CommitDoesNotExistError: If commits for given `start` or
440 `end` could not be found.
440 `end` could not be found.
441
441
442 """
442 """
443 if self.is_empty():
443 if self.is_empty():
444 raise EmptyRepositoryError("There are no commits yet")
444 raise EmptyRepositoryError("There are no commits yet")
445 self._validate_branch_name(branch_name)
445 self._validate_branch_name(branch_name)
446
446
447 if start_id is not None:
447 if start_id is not None:
448 self._validate_commit_id(start_id)
448 self._validate_commit_id(start_id)
449 if end_id is not None:
449 if end_id is not None:
450 self._validate_commit_id(end_id)
450 self._validate_commit_id(end_id)
451
451
452 start_raw_id = self._get_commit_id(start_id)
452 start_raw_id = self._get_commit_id(start_id)
453 start_pos = self._commit_ids[start_raw_id] if start_id else None
453 start_pos = self._commit_ids[start_raw_id] if start_id else None
454 end_raw_id = self._get_commit_id(end_id)
454 end_raw_id = self._get_commit_id(end_id)
455 end_pos = max(0, self._commit_ids[end_raw_id]) if end_id else None
455 end_pos = max(0, self._commit_ids[end_raw_id]) if end_id else None
456
456
457 if None not in [start_id, end_id] and start_pos > end_pos:
457 if None not in [start_id, end_id] and start_pos > end_pos:
458 raise RepositoryError(
458 raise RepositoryError(
459 "Start commit '%s' cannot be after end commit '%s'" %
459 "Start commit '%s' cannot be after end commit '%s'" %
460 (start_id, end_id))
460 (start_id, end_id))
461
461
462 if end_pos is not None:
462 if end_pos is not None:
463 end_pos += 1
463 end_pos += 1
464
464
465 filter_ = []
465 filter_ = []
466 if branch_name:
466 if branch_name:
467 filter_.append({'branch_name': branch_name})
467 filter_.append({'branch_name': branch_name})
468 if start_date and not end_date:
468 if start_date and not end_date:
469 filter_.append({'since': start_date})
469 filter_.append({'since': start_date})
470 if end_date and not start_date:
470 if end_date and not start_date:
471 filter_.append({'until': end_date})
471 filter_.append({'until': end_date})
472 if start_date and end_date:
472 if start_date and end_date:
473 filter_.append({'since': start_date})
473 filter_.append({'since': start_date})
474 filter_.append({'until': end_date})
474 filter_.append({'until': end_date})
475
475
476 # if start_pos or end_pos:
476 # if start_pos or end_pos:
477 # filter_.append({'start': start_pos})
477 # filter_.append({'start': start_pos})
478 # filter_.append({'end': end_pos})
478 # filter_.append({'end': end_pos})
479
479
480 if filter_:
480 if filter_:
481 revfilters = {
481 revfilters = {
482 'branch_name': branch_name,
482 'branch_name': branch_name,
483 'since': start_date.strftime('%m/%d/%y %H:%M:%S') if start_date else None,
483 'since': start_date.strftime('%m/%d/%y %H:%M:%S') if start_date else None,
484 'until': end_date.strftime('%m/%d/%y %H:%M:%S') if end_date else None,
484 'until': end_date.strftime('%m/%d/%y %H:%M:%S') if end_date else None,
485 'start': start_pos,
485 'start': start_pos,
486 'end': end_pos,
486 'end': end_pos,
487 }
487 }
488 commit_ids = self._get_all_commit_ids(filters=revfilters)
488 commit_ids = self._get_all_commit_ids(filters=revfilters)
489
489
490 # pure python stuff, it's slow due to walker walking whole repo
490 # pure python stuff, it's slow due to walker walking whole repo
491 # def get_revs(walker):
491 # def get_revs(walker):
492 # for walker_entry in walker:
492 # for walker_entry in walker:
493 # yield walker_entry.commit.id
493 # yield walker_entry.commit.id
494 # revfilters = {}
494 # revfilters = {}
495 # commit_ids = list(reversed(list(get_revs(self._repo.get_walker(**revfilters)))))
495 # commit_ids = list(reversed(list(get_revs(self._repo.get_walker(**revfilters)))))
496 else:
496 else:
497 commit_ids = self.commit_ids
497 commit_ids = self.commit_ids
498
498
499 if start_pos or end_pos:
499 if start_pos or end_pos:
500 commit_ids = commit_ids[start_pos: end_pos]
500 commit_ids = commit_ids[start_pos: end_pos]
501
501
502 return CollectionGenerator(self, commit_ids, pre_load=pre_load)
502 return CollectionGenerator(self, commit_ids, pre_load=pre_load)
503
503
504 def get_diff(
504 def get_diff(
505 self, commit1, commit2, path='', ignore_whitespace=False,
505 self, commit1, commit2, path='', ignore_whitespace=False,
506 context=3, path1=None):
506 context=3, path1=None):
507 """
507 """
508 Returns (git like) *diff*, as plain text. Shows changes introduced by
508 Returns (git like) *diff*, as plain text. Shows changes introduced by
509 ``commit2`` since ``commit1``.
509 ``commit2`` since ``commit1``.
510
510
511 :param commit1: Entry point from which diff is shown. Can be
511 :param commit1: Entry point from which diff is shown. Can be
512 ``self.EMPTY_COMMIT`` - in this case, patch showing all
512 ``self.EMPTY_COMMIT`` - in this case, patch showing all
513 the changes since empty state of the repository until ``commit2``
513 the changes since empty state of the repository until ``commit2``
514 :param commit2: Until which commits changes should be shown.
514 :param commit2: Until which commits changes should be shown.
515 :param ignore_whitespace: If set to ``True``, would not show whitespace
515 :param ignore_whitespace: If set to ``True``, would not show whitespace
516 changes. Defaults to ``False``.
516 changes. Defaults to ``False``.
517 :param context: How many lines before/after changed lines should be
517 :param context: How many lines before/after changed lines should be
518 shown. Defaults to ``3``.
518 shown. Defaults to ``3``.
519 """
519 """
520 self._validate_diff_commits(commit1, commit2)
520 self._validate_diff_commits(commit1, commit2)
521 if path1 is not None and path1 != path:
521 if path1 is not None and path1 != path:
522 raise ValueError("Diff of two different paths not supported.")
522 raise ValueError("Diff of two different paths not supported.")
523
523
524 flags = [
524 flags = [
525 '-U%s' % context, '--full-index', '--binary', '-p',
525 '-U%s' % context, '--full-index', '--binary', '-p',
526 '-M', '--abbrev=40']
526 '-M', '--abbrev=40']
527 if ignore_whitespace:
527 if ignore_whitespace:
528 flags.append('-w')
528 flags.append('-w')
529
529
530 if commit1 == self.EMPTY_COMMIT:
530 if commit1 == self.EMPTY_COMMIT:
531 cmd = ['show'] + flags + [commit2.raw_id]
531 cmd = ['show'] + flags + [commit2.raw_id]
532 else:
532 else:
533 cmd = ['diff'] + flags + [commit1.raw_id, commit2.raw_id]
533 cmd = ['diff'] + flags + [commit1.raw_id, commit2.raw_id]
534
534
535 if path:
535 if path:
536 cmd.extend(['--', path])
536 cmd.extend(['--', path])
537
537
538 stdout, __ = self.run_git_command(cmd)
538 stdout, __ = self.run_git_command(cmd)
539 # If we used 'show' command, strip first few lines (until actual diff
539 # If we used 'show' command, strip first few lines (until actual diff
540 # starts)
540 # starts)
541 if commit1 == self.EMPTY_COMMIT:
541 if commit1 == self.EMPTY_COMMIT:
542 lines = stdout.splitlines()
542 lines = stdout.splitlines()
543 x = 0
543 x = 0
544 for line in lines:
544 for line in lines:
545 if line.startswith('diff'):
545 if line.startswith('diff'):
546 break
546 break
547 x += 1
547 x += 1
548 # Append new line just like 'diff' command do
548 # Append new line just like 'diff' command do
549 stdout = '\n'.join(lines[x:]) + '\n'
549 stdout = '\n'.join(lines[x:]) + '\n'
550 return GitDiff(stdout)
550 return GitDiff(stdout)
551
551
552 def strip(self, commit_id, branch_name):
552 def strip(self, commit_id, branch_name):
553 commit = self.get_commit(commit_id=commit_id)
553 commit = self.get_commit(commit_id=commit_id)
554 if commit.merge:
554 if commit.merge:
555 raise Exception('Cannot reset to merge commit')
555 raise Exception('Cannot reset to merge commit')
556
556
557 # parent is going to be the new head now
557 # parent is going to be the new head now
558 commit = commit.parents[0]
558 commit = commit.parents[0]
559 self._remote.set_refs('refs/heads/%s' % branch_name, commit.raw_id)
559 self._remote.set_refs('refs/heads/%s' % branch_name, commit.raw_id)
560
560
561 self.commit_ids = self._get_all_commit_ids()
561 self.commit_ids = self._get_all_commit_ids()
562 self._rebuild_cache(self.commit_ids)
562 self._rebuild_cache(self.commit_ids)
563
563
564 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
564 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
565 if commit_id1 == commit_id2:
565 if commit_id1 == commit_id2:
566 return commit_id1
566 return commit_id1
567
567
568 if self != repo2:
568 if self != repo2:
569 commits = self._remote.get_missing_revs(
569 commits = self._remote.get_missing_revs(
570 commit_id1, commit_id2, repo2.path)
570 commit_id1, commit_id2, repo2.path)
571 if commits:
571 if commits:
572 commit = repo2.get_commit(commits[-1])
572 commit = repo2.get_commit(commits[-1])
573 if commit.parents:
573 if commit.parents:
574 ancestor_id = commit.parents[0].raw_id
574 ancestor_id = commit.parents[0].raw_id
575 else:
575 else:
576 ancestor_id = None
576 ancestor_id = None
577 else:
577 else:
578 # no commits from other repo, ancestor_id is the commit_id2
578 # no commits from other repo, ancestor_id is the commit_id2
579 ancestor_id = commit_id2
579 ancestor_id = commit_id2
580 else:
580 else:
581 output, __ = self.run_git_command(
581 output, __ = self.run_git_command(
582 ['merge-base', commit_id1, commit_id2])
582 ['merge-base', commit_id1, commit_id2])
583 ancestor_id = re.findall(r'[0-9a-fA-F]{40}', output)[0]
583 ancestor_id = re.findall(r'[0-9a-fA-F]{40}', output)[0]
584
584
585 return ancestor_id
585 return ancestor_id
586
586
587 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
587 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
588 repo1 = self
588 repo1 = self
589 ancestor_id = None
589 ancestor_id = None
590
590
591 if commit_id1 == commit_id2:
591 if commit_id1 == commit_id2:
592 commits = []
592 commits = []
593 elif repo1 != repo2:
593 elif repo1 != repo2:
594 missing_ids = self._remote.get_missing_revs(commit_id1, commit_id2,
594 missing_ids = self._remote.get_missing_revs(commit_id1, commit_id2,
595 repo2.path)
595 repo2.path)
596 commits = [
596 commits = [
597 repo2.get_commit(commit_id=commit_id, pre_load=pre_load)
597 repo2.get_commit(commit_id=commit_id, pre_load=pre_load)
598 for commit_id in reversed(missing_ids)]
598 for commit_id in reversed(missing_ids)]
599 else:
599 else:
600 output, __ = repo1.run_git_command(
600 output, __ = repo1.run_git_command(
601 ['log', '--reverse', '--pretty=format: %H', '-s',
601 ['log', '--reverse', '--pretty=format: %H', '-s',
602 '%s..%s' % (commit_id1, commit_id2)])
602 '%s..%s' % (commit_id1, commit_id2)])
603 commits = [
603 commits = [
604 repo1.get_commit(commit_id=commit_id, pre_load=pre_load)
604 repo1.get_commit(commit_id=commit_id, pre_load=pre_load)
605 for commit_id in re.findall(r'[0-9a-fA-F]{40}', output)]
605 for commit_id in re.findall(r'[0-9a-fA-F]{40}', output)]
606
606
607 return commits
607 return commits
608
608
609 @LazyProperty
609 @LazyProperty
610 def in_memory_commit(self):
610 def in_memory_commit(self):
611 """
611 """
612 Returns ``GitInMemoryCommit`` object for this repository.
612 Returns ``GitInMemoryCommit`` object for this repository.
613 """
613 """
614 return GitInMemoryCommit(self)
614 return GitInMemoryCommit(self)
615
615
616 def clone(self, url, update_after_clone=True, bare=False):
616 def clone(self, url, update_after_clone=True, bare=False):
617 """
617 """
618 Tries to clone commits from external location.
618 Tries to clone commits from external location.
619
619
620 :param update_after_clone: If set to ``False``, git won't checkout
620 :param update_after_clone: If set to ``False``, git won't checkout
621 working directory
621 working directory
622 :param bare: If set to ``True``, repository would be cloned into
622 :param bare: If set to ``True``, repository would be cloned into
623 *bare* git repository (no working directory at all).
623 *bare* git repository (no working directory at all).
624 """
624 """
625 # init_bare and init expect empty dir created to proceed
625 # init_bare and init expect empty dir created to proceed
626 if not os.path.exists(self.path):
626 if not os.path.exists(self.path):
627 os.mkdir(self.path)
627 os.mkdir(self.path)
628
628
629 if bare:
629 if bare:
630 self._remote.init_bare()
630 self._remote.init_bare()
631 else:
631 else:
632 self._remote.init()
632 self._remote.init()
633
633
634 deferred = '^{}'
634 deferred = '^{}'
635 valid_refs = ('refs/heads', 'refs/tags', 'HEAD')
635 valid_refs = ('refs/heads', 'refs/tags', 'HEAD')
636
636
637 return self._remote.clone(
637 return self._remote.clone(
638 url, deferred, valid_refs, update_after_clone)
638 url, deferred, valid_refs, update_after_clone)
639
639
640 def pull(self, url, commit_ids=None):
640 def pull(self, url, commit_ids=None):
641 """
641 """
642 Tries to pull changes from external location. We use fetch here since
642 Tries to pull changes from external location. We use fetch here since
643 pull in get does merges and we want to be compatible with hg backend so
643 pull in get does merges and we want to be compatible with hg backend so
644 pull == fetch in this case
644 pull == fetch in this case
645 """
645 """
646 self.fetch(url, commit_ids=commit_ids)
646 self.fetch(url, commit_ids=commit_ids)
647
647
648 def fetch(self, url, commit_ids=None):
648 def fetch(self, url, commit_ids=None):
649 """
649 """
650 Tries to fetch changes from external location.
650 Tries to fetch changes from external location.
651 """
651 """
652 refs = None
652 refs = None
653
653
654 if commit_ids is not None:
654 if commit_ids is not None:
655 remote_refs = self._remote.get_remote_refs(url)
655 remote_refs = self._remote.get_remote_refs(url)
656 refs = [
656 refs = [
657 ref for ref in remote_refs if remote_refs[ref] in commit_ids]
657 ref for ref in remote_refs if remote_refs[ref] in commit_ids]
658 self._remote.fetch(url, refs=refs)
658 self._remote.fetch(url, refs=refs)
659
659
660 def set_refs(self, ref_name, commit_id):
660 def set_refs(self, ref_name, commit_id):
661 self._remote.set_refs(ref_name, commit_id)
661 self._remote.set_refs(ref_name, commit_id)
662
662
663 def remove_ref(self, ref_name):
663 def remove_ref(self, ref_name):
664 self._remote.remove_ref(ref_name)
664 self._remote.remove_ref(ref_name)
665
665
666 def _update_server_info(self):
666 def _update_server_info(self):
667 """
667 """
668 runs gits update-server-info command in this repo instance
668 runs gits update-server-info command in this repo instance
669 """
669 """
670 self._remote.update_server_info()
670 self._remote.update_server_info()
671
671
672 def _current_branch(self):
672 def _current_branch(self):
673 """
673 """
674 Return the name of the current branch.
674 Return the name of the current branch.
675
675
676 It only works for non bare repositories (i.e. repositories with a
676 It only works for non bare repositories (i.e. repositories with a
677 working copy)
677 working copy)
678 """
678 """
679 if self.bare:
679 if self.bare:
680 raise RepositoryError('Bare git repos do not have active branches')
680 raise RepositoryError('Bare git repos do not have active branches')
681
681
682 if self.is_empty():
682 if self.is_empty():
683 return None
683 return None
684
684
685 stdout, _ = self.run_git_command(['rev-parse', '--abbrev-ref', 'HEAD'])
685 stdout, _ = self.run_git_command(['rev-parse', '--abbrev-ref', 'HEAD'])
686 return stdout.strip()
686 return stdout.strip()
687
687
688 def _checkout(self, branch_name, create=False):
688 def _checkout(self, branch_name, create=False):
689 """
689 """
690 Checkout a branch in the working directory.
690 Checkout a branch in the working directory.
691
691
692 It tries to create the branch if create is True, failing if the branch
692 It tries to create the branch if create is True, failing if the branch
693 already exists.
693 already exists.
694
694
695 It only works for non bare repositories (i.e. repositories with a
695 It only works for non bare repositories (i.e. repositories with a
696 working copy)
696 working copy)
697 """
697 """
698 if self.bare:
698 if self.bare:
699 raise RepositoryError('Cannot checkout branches in a bare git repo')
699 raise RepositoryError('Cannot checkout branches in a bare git repo')
700
700
701 cmd = ['checkout']
701 cmd = ['checkout']
702 if create:
702 if create:
703 cmd.append('-b')
703 cmd.append('-b')
704 cmd.append(branch_name)
704 cmd.append(branch_name)
705 self.run_git_command(cmd, fail_on_stderr=False)
705 self.run_git_command(cmd, fail_on_stderr=False)
706
706
707 def _identify(self):
708 """
709 Return the current state of the working directory.
710 """
711 if self.bare:
712 raise RepositoryError('Bare git repos do not have active branches')
713
714 if self.is_empty():
715 return None
716
717 stdout, _ = self.run_git_command(['rev-parse', 'HEAD'])
718 return stdout.strip()
719
707 def _local_clone(self, clone_path, branch_name):
720 def _local_clone(self, clone_path, branch_name):
708 """
721 """
709 Create a local clone of the current repo.
722 Create a local clone of the current repo.
710 """
723 """
711 # N.B.(skreft): the --branch option is required as otherwise the shallow
724 # N.B.(skreft): the --branch option is required as otherwise the shallow
712 # clone will only fetch the active branch.
725 # clone will only fetch the active branch.
713 cmd = ['clone', '--branch', branch_name, '--single-branch',
726 cmd = ['clone', '--branch', branch_name, '--single-branch',
714 self.path, os.path.abspath(clone_path)]
727 self.path, os.path.abspath(clone_path)]
715 self.run_git_command(cmd, fail_on_stderr=False)
728 self.run_git_command(cmd, fail_on_stderr=False)
716
729
717 def _local_fetch(self, repository_path, branch_name):
730 def _local_fetch(self, repository_path, branch_name):
718 """
731 """
719 Fetch a branch from a local repository.
732 Fetch a branch from a local repository.
720 """
733 """
721 repository_path = os.path.abspath(repository_path)
734 repository_path = os.path.abspath(repository_path)
722 if repository_path == self.path:
735 if repository_path == self.path:
723 raise ValueError('Cannot fetch from the same repository')
736 raise ValueError('Cannot fetch from the same repository')
724
737
725 cmd = ['fetch', '--no-tags', repository_path, branch_name]
738 cmd = ['fetch', '--no-tags', repository_path, branch_name]
726 self.run_git_command(cmd, fail_on_stderr=False)
739 self.run_git_command(cmd, fail_on_stderr=False)
727
740
728 def _last_fetch_heads(self):
741 def _last_fetch_heads(self):
729 """
742 """
730 Return the last fetched heads that need merging.
743 Return the last fetched heads that need merging.
731
744
732 The algorithm is defined at
745 The algorithm is defined at
733 https://github.com/git/git/blob/v2.1.3/git-pull.sh#L283
746 https://github.com/git/git/blob/v2.1.3/git-pull.sh#L283
734 """
747 """
735 if not self.bare:
748 if not self.bare:
736 fetch_heads_path = os.path.join(self.path, '.git', 'FETCH_HEAD')
749 fetch_heads_path = os.path.join(self.path, '.git', 'FETCH_HEAD')
737 else:
750 else:
738 fetch_heads_path = os.path.join(self.path, 'FETCH_HEAD')
751 fetch_heads_path = os.path.join(self.path, 'FETCH_HEAD')
739
752
740 heads = []
753 heads = []
741 with open(fetch_heads_path) as f:
754 with open(fetch_heads_path) as f:
742 for line in f:
755 for line in f:
743 if ' not-for-merge ' in line:
756 if ' not-for-merge ' in line:
744 continue
757 continue
745 line = re.sub('\t.*', '', line, flags=re.DOTALL)
758 line = re.sub('\t.*', '', line, flags=re.DOTALL)
746 heads.append(line)
759 heads.append(line)
747
760
748 return heads
761 return heads
749
762
750 def _get_shadow_instance(self, shadow_repository_path, enable_hooks=False):
763 def _get_shadow_instance(self, shadow_repository_path, enable_hooks=False):
751 return GitRepository(shadow_repository_path)
764 return GitRepository(shadow_repository_path)
752
765
753 def _local_pull(self, repository_path, branch_name):
766 def _local_pull(self, repository_path, branch_name):
754 """
767 """
755 Pull a branch from a local repository.
768 Pull a branch from a local repository.
756 """
769 """
757 if self.bare:
770 if self.bare:
758 raise RepositoryError('Cannot pull into a bare git repository')
771 raise RepositoryError('Cannot pull into a bare git repository')
759 # N.B.(skreft): The --ff-only option is to make sure this is a
772 # N.B.(skreft): The --ff-only option is to make sure this is a
760 # fast-forward (i.e., we are only pulling new changes and there are no
773 # fast-forward (i.e., we are only pulling new changes and there are no
761 # conflicts with our current branch)
774 # conflicts with our current branch)
762 # Additionally, that option needs to go before --no-tags, otherwise git
775 # Additionally, that option needs to go before --no-tags, otherwise git
763 # pull complains about it being an unknown flag.
776 # pull complains about it being an unknown flag.
764 cmd = ['pull', '--ff-only', '--no-tags', repository_path, branch_name]
777 cmd = ['pull', '--ff-only', '--no-tags', repository_path, branch_name]
765 self.run_git_command(cmd, fail_on_stderr=False)
778 self.run_git_command(cmd, fail_on_stderr=False)
766
779
767 def _local_merge(self, merge_message, user_name, user_email, heads):
780 def _local_merge(self, merge_message, user_name, user_email, heads):
768 """
781 """
769 Merge the given head into the checked out branch.
782 Merge the given head into the checked out branch.
770
783
771 It will force a merge commit.
784 It will force a merge commit.
772
785
773 Currently it raises an error if the repo is empty, as it is not possible
786 Currently it raises an error if the repo is empty, as it is not possible
774 to create a merge commit in an empty repo.
787 to create a merge commit in an empty repo.
775
788
776 :param merge_message: The message to use for the merge commit.
789 :param merge_message: The message to use for the merge commit.
777 :param heads: the heads to merge.
790 :param heads: the heads to merge.
778 """
791 """
779 if self.bare:
792 if self.bare:
780 raise RepositoryError('Cannot merge into a bare git repository')
793 raise RepositoryError('Cannot merge into a bare git repository')
781
794
782 if not heads:
795 if not heads:
783 return
796 return
784
797
785 if self.is_empty():
798 if self.is_empty():
786 # TODO(skreft): do somehting more robust in this case.
799 # TODO(skreft): do somehting more robust in this case.
787 raise RepositoryError(
800 raise RepositoryError(
788 'Do not know how to merge into empty repositories yet')
801 'Do not know how to merge into empty repositories yet')
789
802
790 # N.B.(skreft): the --no-ff option is used to enforce the creation of a
803 # N.B.(skreft): the --no-ff option is used to enforce the creation of a
791 # commit message. We also specify the user who is doing the merge.
804 # commit message. We also specify the user who is doing the merge.
792 cmd = ['-c', 'user.name=%s' % safe_str(user_name),
805 cmd = ['-c', 'user.name=%s' % safe_str(user_name),
793 '-c', 'user.email=%s' % safe_str(user_email),
806 '-c', 'user.email=%s' % safe_str(user_email),
794 'merge', '--no-ff', '-m', safe_str(merge_message)]
807 'merge', '--no-ff', '-m', safe_str(merge_message)]
795 cmd.extend(heads)
808 cmd.extend(heads)
796 try:
809 try:
797 self.run_git_command(cmd, fail_on_stderr=False)
810 self.run_git_command(cmd, fail_on_stderr=False)
798 except RepositoryError:
811 except RepositoryError:
799 # Cleanup any merge leftovers
812 # Cleanup any merge leftovers
800 self.run_git_command(['merge', '--abort'], fail_on_stderr=False)
813 self.run_git_command(['merge', '--abort'], fail_on_stderr=False)
801 raise
814 raise
802
815
803 def _local_push(
816 def _local_push(
804 self, source_branch, repository_path, target_branch,
817 self, source_branch, repository_path, target_branch,
805 enable_hooks=False, rc_scm_data=None):
818 enable_hooks=False, rc_scm_data=None):
806 """
819 """
807 Push the source_branch to the given repository and target_branch.
820 Push the source_branch to the given repository and target_branch.
808
821
809 Currently it if the target_branch is not master and the target repo is
822 Currently it if the target_branch is not master and the target repo is
810 empty, the push will work, but then GitRepository won't be able to find
823 empty, the push will work, but then GitRepository won't be able to find
811 the pushed branch or the commits. As the HEAD will be corrupted (i.e.,
824 the pushed branch or the commits. As the HEAD will be corrupted (i.e.,
812 pointing to master, which does not exist).
825 pointing to master, which does not exist).
813
826
814 It does not run the hooks in the target repo.
827 It does not run the hooks in the target repo.
815 """
828 """
816 # TODO(skreft): deal with the case in which the target repo is empty,
829 # TODO(skreft): deal with the case in which the target repo is empty,
817 # and the target_branch is not master.
830 # and the target_branch is not master.
818 target_repo = GitRepository(repository_path)
831 target_repo = GitRepository(repository_path)
819 if (not target_repo.bare and
832 if (not target_repo.bare and
820 target_repo._current_branch() == target_branch):
833 target_repo._current_branch() == target_branch):
821 # Git prevents pushing to the checked out branch, so simulate it by
834 # Git prevents pushing to the checked out branch, so simulate it by
822 # pulling into the target repository.
835 # pulling into the target repository.
823 target_repo._local_pull(self.path, source_branch)
836 target_repo._local_pull(self.path, source_branch)
824 else:
837 else:
825 cmd = ['push', os.path.abspath(repository_path),
838 cmd = ['push', os.path.abspath(repository_path),
826 '%s:%s' % (source_branch, target_branch)]
839 '%s:%s' % (source_branch, target_branch)]
827 gitenv = {}
840 gitenv = {}
828 if rc_scm_data:
841 if rc_scm_data:
829 gitenv.update({'RC_SCM_DATA': rc_scm_data})
842 gitenv.update({'RC_SCM_DATA': rc_scm_data})
830
843
831 if not enable_hooks:
844 if not enable_hooks:
832 gitenv['RC_SKIP_HOOKS'] = '1'
845 gitenv['RC_SKIP_HOOKS'] = '1'
833 self.run_git_command(cmd, fail_on_stderr=False, extra_env=gitenv)
846 self.run_git_command(cmd, fail_on_stderr=False, extra_env=gitenv)
834
847
835 def _get_new_pr_branch(self, source_branch, target_branch):
848 def _get_new_pr_branch(self, source_branch, target_branch):
836 prefix = 'pr_%s-%s_' % (source_branch, target_branch)
849 prefix = 'pr_%s-%s_' % (source_branch, target_branch)
837 pr_branches = []
850 pr_branches = []
838 for branch in self.branches:
851 for branch in self.branches:
839 if branch.startswith(prefix):
852 if branch.startswith(prefix):
840 pr_branches.append(int(branch[len(prefix):]))
853 pr_branches.append(int(branch[len(prefix):]))
841
854
842 if not pr_branches:
855 if not pr_branches:
843 branch_id = 0
856 branch_id = 0
844 else:
857 else:
845 branch_id = max(pr_branches) + 1
858 branch_id = max(pr_branches) + 1
846
859
847 return '%s%d' % (prefix, branch_id)
860 return '%s%d' % (prefix, branch_id)
848
861
849 def _merge_repo(self, shadow_repository_path, target_ref,
862 def _merge_repo(self, shadow_repository_path, target_ref,
850 source_repo, source_ref, merge_message,
863 source_repo, source_ref, merge_message,
851 merger_name, merger_email, dry_run=False,
864 merger_name, merger_email, dry_run=False,
852 use_rebase=False, close_branch=False):
865 use_rebase=False, close_branch=False):
853 if target_ref.commit_id != self.branches[target_ref.name]:
866 if target_ref.commit_id != self.branches[target_ref.name]:
854 return MergeResponse(
867 return MergeResponse(
855 False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD)
868 False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD)
856
869
857 shadow_repo = GitRepository(shadow_repository_path)
870 shadow_repo = GitRepository(shadow_repository_path)
858 shadow_repo._checkout(target_ref.name)
871 shadow_repo._checkout(target_ref.name)
859 shadow_repo._local_pull(self.path, target_ref.name)
872 shadow_repo._local_pull(self.path, target_ref.name)
860 # Need to reload repo to invalidate the cache, or otherwise we cannot
873 # Need to reload repo to invalidate the cache, or otherwise we cannot
861 # retrieve the last target commit.
874 # retrieve the last target commit.
862 shadow_repo = GitRepository(shadow_repository_path)
875 shadow_repo = GitRepository(shadow_repository_path)
863 if target_ref.commit_id != shadow_repo.branches[target_ref.name]:
876 if target_ref.commit_id != shadow_repo.branches[target_ref.name]:
864 return MergeResponse(
877 return MergeResponse(
865 False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD)
878 False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD)
866
879
867 pr_branch = shadow_repo._get_new_pr_branch(
880 pr_branch = shadow_repo._get_new_pr_branch(
868 source_ref.name, target_ref.name)
881 source_ref.name, target_ref.name)
869 shadow_repo._checkout(pr_branch, create=True)
882 shadow_repo._checkout(pr_branch, create=True)
870 try:
883 try:
871 shadow_repo._local_fetch(source_repo.path, source_ref.name)
884 shadow_repo._local_fetch(source_repo.path, source_ref.name)
872 except RepositoryError:
885 except RepositoryError:
873 log.exception('Failure when doing local fetch on git shadow repo')
886 log.exception('Failure when doing local fetch on git shadow repo')
874 return MergeResponse(
887 return MergeResponse(
875 False, False, None, MergeFailureReason.MISSING_SOURCE_REF)
888 False, False, None, MergeFailureReason.MISSING_SOURCE_REF)
876
889
877 merge_ref = None
890 merge_ref = None
878 merge_failure_reason = MergeFailureReason.NONE
891 merge_failure_reason = MergeFailureReason.NONE
879 try:
892 try:
880 shadow_repo._local_merge(merge_message, merger_name, merger_email,
893 shadow_repo._local_merge(merge_message, merger_name, merger_email,
881 [source_ref.commit_id])
894 [source_ref.commit_id])
882 merge_possible = True
895 merge_possible = True
883
896
884 # Need to reload repo to invalidate the cache, or otherwise we
897 # Need to reload repo to invalidate the cache, or otherwise we
885 # cannot retrieve the merge commit.
898 # cannot retrieve the merge commit.
886 shadow_repo = GitRepository(shadow_repository_path)
899 shadow_repo = GitRepository(shadow_repository_path)
887 merge_commit_id = shadow_repo.branches[pr_branch]
900 merge_commit_id = shadow_repo.branches[pr_branch]
888
901
889 # Set a reference pointing to the merge commit. This reference may
902 # Set a reference pointing to the merge commit. This reference may
890 # be used to easily identify the last successful merge commit in
903 # be used to easily identify the last successful merge commit in
891 # the shadow repository.
904 # the shadow repository.
892 shadow_repo.set_refs('refs/heads/pr-merge', merge_commit_id)
905 shadow_repo.set_refs('refs/heads/pr-merge', merge_commit_id)
893 merge_ref = Reference('branch', 'pr-merge', merge_commit_id)
906 merge_ref = Reference('branch', 'pr-merge', merge_commit_id)
894 except RepositoryError:
907 except RepositoryError:
895 log.exception('Failure when doing local merge on git shadow repo')
908 log.exception('Failure when doing local merge on git shadow repo')
896 merge_possible = False
909 merge_possible = False
897 merge_failure_reason = MergeFailureReason.MERGE_FAILED
910 merge_failure_reason = MergeFailureReason.MERGE_FAILED
898
911
899 if merge_possible and not dry_run:
912 if merge_possible and not dry_run:
900 try:
913 try:
901 shadow_repo._local_push(
914 shadow_repo._local_push(
902 pr_branch, self.path, target_ref.name, enable_hooks=True,
915 pr_branch, self.path, target_ref.name, enable_hooks=True,
903 rc_scm_data=self.config.get('rhodecode', 'RC_SCM_DATA'))
916 rc_scm_data=self.config.get('rhodecode', 'RC_SCM_DATA'))
904 merge_succeeded = True
917 merge_succeeded = True
905 except RepositoryError:
918 except RepositoryError:
906 log.exception(
919 log.exception(
907 'Failure when doing local push on git shadow repo')
920 'Failure when doing local push on git shadow repo')
908 merge_succeeded = False
921 merge_succeeded = False
909 merge_failure_reason = MergeFailureReason.PUSH_FAILED
922 merge_failure_reason = MergeFailureReason.PUSH_FAILED
910 else:
923 else:
911 merge_succeeded = False
924 merge_succeeded = False
912
925
913 return MergeResponse(
926 return MergeResponse(
914 merge_possible, merge_succeeded, merge_ref,
927 merge_possible, merge_succeeded, merge_ref,
915 merge_failure_reason)
928 merge_failure_reason)
916
929
917 def _get_shadow_repository_path(self, workspace_id):
930 def _get_shadow_repository_path(self, workspace_id):
918 # The name of the shadow repository must start with '.', so it is
931 # The name of the shadow repository must start with '.', so it is
919 # skipped by 'rhodecode.lib.utils.get_filesystem_repos'.
932 # skipped by 'rhodecode.lib.utils.get_filesystem_repos'.
920 return os.path.join(
933 return os.path.join(
921 os.path.dirname(self.path),
934 os.path.dirname(self.path),
922 '.__shadow_%s_%s' % (os.path.basename(self.path), workspace_id))
935 '.__shadow_%s_%s' % (os.path.basename(self.path), workspace_id))
923
936
924 def _maybe_prepare_merge_workspace(self, workspace_id, target_ref):
937 def _maybe_prepare_merge_workspace(self, workspace_id, target_ref):
925 shadow_repository_path = self._get_shadow_repository_path(workspace_id)
938 shadow_repository_path = self._get_shadow_repository_path(workspace_id)
926 if not os.path.exists(shadow_repository_path):
939 if not os.path.exists(shadow_repository_path):
927 self._local_clone(shadow_repository_path, target_ref.name)
940 self._local_clone(shadow_repository_path, target_ref.name)
928
941
929 return shadow_repository_path
942 return shadow_repository_path
930
943
931 def cleanup_merge_workspace(self, workspace_id):
944 def cleanup_merge_workspace(self, workspace_id):
932 shadow_repository_path = self._get_shadow_repository_path(workspace_id)
945 shadow_repository_path = self._get_shadow_repository_path(workspace_id)
933 shutil.rmtree(shadow_repository_path, ignore_errors=True)
946 shutil.rmtree(shadow_repository_path, ignore_errors=True)
General Comments 0
You need to be logged in to leave comments. Login now