##// END OF EJS Templates
fix(git): fixed strip command to always require a valid branch name for git....
super-admin -
r5253:65ddf04b default
parent child Browse files
Show More
@@ -1,102 +1,105 b''
1 # Copyright (C) 2017-2023 RhodeCode GmbH
1 # Copyright (C) 2017-2023 RhodeCode GmbH
2 #
2 #
3 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
15 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 import logging
19 import logging
20
20
21
21
22 from rhodecode.apps._base import RepoAppView
22 from rhodecode.apps._base import RepoAppView
23 from rhodecode.lib import audit_logger
23 from rhodecode.lib import audit_logger
24 from rhodecode.lib import helpers as h
24 from rhodecode.lib import helpers as h
25 from rhodecode.lib.auth import (
25 from rhodecode.lib.auth import (
26 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
26 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
27 from rhodecode.lib.ext_json import json
27 from rhodecode.lib.ext_json import json
28
28
29 log = logging.getLogger(__name__)
29 log = logging.getLogger(__name__)
30
30
31
31
32 class RepoStripView(RepoAppView):
32 class RepoStripView(RepoAppView):
33 def load_default_context(self):
33 def load_default_context(self):
34 c = self._get_local_tmpl_context()
34 c = self._get_local_tmpl_context()
35
35
36
36
37 return c
37 return c
38
38
39 @LoginRequired()
39 @LoginRequired()
40 @HasRepoPermissionAnyDecorator('repository.admin')
40 @HasRepoPermissionAnyDecorator('repository.admin')
41 def strip(self):
41 def strip(self):
42 c = self.load_default_context()
42 c = self.load_default_context()
43 c.active = 'strip'
43 c.active = 'strip'
44 c.strip_limit = 10
44 c.strip_limit = 10
45
45
46 return self._get_template_context(c)
46 return self._get_template_context(c)
47
47
48 @LoginRequired()
48 @LoginRequired()
49 @HasRepoPermissionAnyDecorator('repository.admin')
49 @HasRepoPermissionAnyDecorator('repository.admin')
50 @CSRFRequired()
50 @CSRFRequired()
51 def strip_check(self):
51 def strip_check(self):
52 from rhodecode.lib.vcs.backends.base import EmptyCommit
52 from rhodecode.lib.vcs.backends.base import EmptyCommit
53 data = {}
53 data = {}
54 rp = self.request.POST
54 rp = self.request.POST
55 for i in range(1, 11):
55 for i in range(1, 11):
56 chset = 'changeset_id-%d' % (i,)
56 changeset_id_key = f'changeset_id-{i}'
57 check = rp.get(chset)
57 changeset_id_branch_key = f'changeset_id_branch-{i}'
58 check = rp.get(changeset_id_key)
58
59
59 if check:
60 if check:
60 data[i] = self.db_repo.get_commit(rp[chset])
61 data[i] = self.db_repo.get_commit(rp[changeset_id_key])
61 if isinstance(data[i], EmptyCommit):
62 if isinstance(data[i], EmptyCommit):
62 data[i] = {'rev': None, 'commit': h.escape(rp[chset])}
63 data[i] = {'rev': None, 'commit': h.escape(rp[changeset_id_key])}
63 else:
64 else:
64 data[i] = {'rev': data[i].raw_id, 'branch': data[i].branch,
65 rp_branch = rp.get(changeset_id_branch_key)
66 data[i] = {'rev': data[i].raw_id, 'branch': data[i].branch or rp_branch,
65 'author': h.escape(data[i].author),
67 'author': h.escape(data[i].author),
66 'comment': h.escape(data[i].message)}
68 'comment': h.escape(data[i].message)}
67 else:
69 else:
68 break
70 break
69 return data
71 return data
70
72
71 @LoginRequired()
73 @LoginRequired()
72 @HasRepoPermissionAnyDecorator('repository.admin')
74 @HasRepoPermissionAnyDecorator('repository.admin')
73 @CSRFRequired()
75 @CSRFRequired()
74 def strip_execute(self):
76 def strip_execute(self):
75 from rhodecode.model.scm import ScmModel
77 from rhodecode.model.scm import ScmModel
76
78
77 c = self.load_default_context()
79 c = self.load_default_context()
78 user = self._rhodecode_user
80 user = self._rhodecode_user
79 rp = self.request.POST
81 rp = self.request.POST
80 data = {}
82 data = {}
83
81 for idx in rp:
84 for idx in rp:
82 commit = json.loads(rp[idx])
85 commit = json.loads(rp[idx])
83 # If someone put two times the same branch
86 # If someone put two times the same branch
84 if commit['branch'] in data.keys():
87 if commit['branch'] in data.keys():
85 continue
88 continue
86 try:
89 try:
87 ScmModel().strip(
90 ScmModel().strip(
88 repo=self.db_repo,
91 repo=self.db_repo,
89 commit_id=commit['rev'], branch=commit['branch'])
92 commit_id=commit['rev'], branch=commit['branch'])
90 log.info('Stripped commit %s from repo `%s` by %s',
93 log.info('Stripped commit %s from repo `%s` by %s',
91 commit['rev'], self.db_repo_name, user)
94 commit['rev'], self.db_repo_name, user)
92 data[commit['rev']] = True
95 data[commit['rev']] = True
93
96
94 audit_logger.store_web(
97 audit_logger.store_web(
95 'repo.commit.strip', action_data={'commit_id': commit['rev']},
98 'repo.commit.strip', action_data={'commit_id': commit['rev']},
96 repo=self.db_repo, user=self._rhodecode_user, commit=True)
99 repo=self.db_repo, user=self._rhodecode_user, commit=True)
97
100
98 except Exception as e:
101 except Exception as e:
99 data[commit['rev']] = False
102 data[commit['rev']] = False
100 log.debug('Stripped commit %s from repo `%s` failed by %s, exeption %s',
103 log.debug('Stripped commit %s from repo `%s` failed by %s, exeption %s',
101 commit['rev'], self.db_repo_name, user, e.message)
104 commit['rev'], self.db_repo_name, user, e)
102 return data
105 return data
@@ -1,1050 +1,1053 b''
1 # Copyright (C) 2014-2023 RhodeCode GmbH
1 # Copyright (C) 2014-2023 RhodeCode GmbH
2 #
2 #
3 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
15 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 """
19 """
20 GIT repository module
20 GIT repository module
21 """
21 """
22
22
23 import logging
23 import logging
24 import os
24 import os
25 import re
25 import re
26
26
27 from zope.cachedescriptors.property import Lazy as LazyProperty
27 from zope.cachedescriptors.property import Lazy as LazyProperty
28
28
29 from collections import OrderedDict
29 from collections import OrderedDict
30 from rhodecode.lib.datelib import (
30 from rhodecode.lib.datelib import (
31 utcdate_fromtimestamp, makedate, date_astimestamp)
31 utcdate_fromtimestamp, makedate, date_astimestamp)
32 from rhodecode.lib.hash_utils import safe_str
32 from rhodecode.lib.hash_utils import safe_str
33 from rhodecode.lib.utils2 import CachedProperty
33 from rhodecode.lib.utils2 import CachedProperty
34 from rhodecode.lib.vcs import connection, path as vcspath
34 from rhodecode.lib.vcs import connection, path as vcspath
35 from rhodecode.lib.vcs.backends.base import (
35 from rhodecode.lib.vcs.backends.base import (
36 BaseRepository, CollectionGenerator, Config, MergeResponse,
36 BaseRepository, CollectionGenerator, Config, MergeResponse,
37 MergeFailureReason, Reference)
37 MergeFailureReason, Reference)
38 from rhodecode.lib.vcs.backends.git.commit import GitCommit
38 from rhodecode.lib.vcs.backends.git.commit import GitCommit
39 from rhodecode.lib.vcs.backends.git.diff import GitDiff
39 from rhodecode.lib.vcs.backends.git.diff import GitDiff
40 from rhodecode.lib.vcs.backends.git.inmemory import GitInMemoryCommit
40 from rhodecode.lib.vcs.backends.git.inmemory import GitInMemoryCommit
41 from rhodecode.lib.vcs.exceptions import (
41 from rhodecode.lib.vcs.exceptions import (
42 CommitDoesNotExistError, EmptyRepositoryError,
42 CommitDoesNotExistError, EmptyRepositoryError,
43 RepositoryError, TagAlreadyExistError, TagDoesNotExistError, VCSError, UnresolvedFilesInRepo)
43 RepositoryError, TagAlreadyExistError, TagDoesNotExistError, VCSError, UnresolvedFilesInRepo)
44
44
45
45
46 SHA_PATTERN = re.compile(r'^([0-9a-fA-F]{12}|[0-9a-fA-F]{40})$')
46 SHA_PATTERN = re.compile(r'^([0-9a-fA-F]{12}|[0-9a-fA-F]{40})$')
47
47
48 log = logging.getLogger(__name__)
48 log = logging.getLogger(__name__)
49
49
50
50
51 class GitRepository(BaseRepository):
51 class GitRepository(BaseRepository):
52 """
52 """
53 Git repository backend.
53 Git repository backend.
54 """
54 """
55 DEFAULT_BRANCH_NAME = os.environ.get('GIT_DEFAULT_BRANCH_NAME') or 'master'
55 DEFAULT_BRANCH_NAME = os.environ.get('GIT_DEFAULT_BRANCH_NAME') or 'master'
56 DEFAULT_REF = f'branch:{DEFAULT_BRANCH_NAME}'
56 DEFAULT_REF = f'branch:{DEFAULT_BRANCH_NAME}'
57
57
58 contact = BaseRepository.DEFAULT_CONTACT
58 contact = BaseRepository.DEFAULT_CONTACT
59
59
60 def __init__(self, repo_path, config=None, create=False, src_url=None,
60 def __init__(self, repo_path, config=None, create=False, src_url=None,
61 do_workspace_checkout=False, with_wire=None, bare=False):
61 do_workspace_checkout=False, with_wire=None, bare=False):
62
62
63 self.path = safe_str(os.path.abspath(repo_path))
63 self.path = safe_str(os.path.abspath(repo_path))
64 self.config = config if config else self.get_default_config()
64 self.config = config if config else self.get_default_config()
65 self.with_wire = with_wire or {"cache": False} # default should not use cache
65 self.with_wire = with_wire or {"cache": False} # default should not use cache
66
66
67 self._init_repo(create, src_url, do_workspace_checkout, bare)
67 self._init_repo(create, src_url, do_workspace_checkout, bare)
68
68
69 # caches
69 # caches
70 self._commit_ids = {}
70 self._commit_ids = {}
71
71
72 @LazyProperty
72 @LazyProperty
73 def _remote(self):
73 def _remote(self):
74 repo_id = self.path
74 repo_id = self.path
75 return connection.Git(self.path, repo_id, self.config, with_wire=self.with_wire)
75 return connection.Git(self.path, repo_id, self.config, with_wire=self.with_wire)
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 @CachedProperty
85 @CachedProperty
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 = {commit_id: index
96 self._commit_ids = {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(f'cmd must be a list, got {type(cmd)} instead')
108 raise ValueError(f'cmd must be a list, got {type(cmd)} instead')
109
109
110 skip_stderr_log = opts.pop('skip_stderr_log', False)
110 skip_stderr_log = opts.pop('skip_stderr_log', False)
111 out, err = self._remote.run_git_command(cmd, **opts)
111 out, err = self._remote.run_git_command(cmd, **opts)
112 if err and not skip_stderr_log:
112 if err and not skip_stderr_log:
113 log.debug('Stderr output of git command "%s":\n%s', cmd, err)
113 log.debug('Stderr output of git command "%s":\n%s', cmd, err)
114 return out, err
114 return out, err
115
115
116 @staticmethod
116 @staticmethod
117 def check_url(url, config):
117 def check_url(url, config):
118 """
118 """
119 Function will check given url and try to verify if it's a valid
119 Function will check given url and try to verify if it's a valid
120 link. Sometimes it may happened that git will issue basic
120 link. Sometimes it may happened that git will issue basic
121 auth request that can cause whole API to hang when used from python
121 auth request that can cause whole API to hang when used from python
122 or other external calls.
122 or other external calls.
123
123
124 On failures it'll raise urllib2.HTTPError, exception is also thrown
124 On failures it'll raise urllib2.HTTPError, exception is also thrown
125 when the return code is non 200
125 when the return code is non 200
126 """
126 """
127 # check first if it's not an url
127 # check first if it's not an url
128 if os.path.isdir(url) or url.startswith('file:'):
128 if os.path.isdir(url) or url.startswith('file:'):
129 return True
129 return True
130
130
131 if '+' in url.split('://', 1)[0]:
131 if '+' in url.split('://', 1)[0]:
132 url = url.split('+', 1)[1]
132 url = url.split('+', 1)[1]
133
133
134 # Request the _remote to verify the url
134 # Request the _remote to verify the url
135 return connection.Git.check_url(url, config.serialize())
135 return connection.Git.check_url(url, config.serialize())
136
136
137 @staticmethod
137 @staticmethod
138 def is_valid_repository(path):
138 def is_valid_repository(path):
139 if os.path.isdir(os.path.join(path, '.git')):
139 if os.path.isdir(os.path.join(path, '.git')):
140 return True
140 return True
141 # check case of bare repository
141 # check case of bare repository
142 try:
142 try:
143 GitRepository(path)
143 GitRepository(path)
144 return True
144 return True
145 except VCSError:
145 except VCSError:
146 pass
146 pass
147 return False
147 return False
148
148
149 def _init_repo(self, create, src_url=None, do_workspace_checkout=False,
149 def _init_repo(self, create, src_url=None, do_workspace_checkout=False,
150 bare=False):
150 bare=False):
151 if create and os.path.exists(self.path):
151 if create and os.path.exists(self.path):
152 raise RepositoryError(
152 raise RepositoryError(
153 f"Cannot create repository at {self.path}, location already exist")
153 f"Cannot create repository at {self.path}, location already exist")
154
154
155 if bare and do_workspace_checkout:
155 if bare and do_workspace_checkout:
156 raise RepositoryError("Cannot update a bare repository")
156 raise RepositoryError("Cannot update a bare repository")
157 try:
157 try:
158
158
159 if src_url:
159 if src_url:
160 # check URL before any actions
160 # check URL before any actions
161 GitRepository.check_url(src_url, self.config)
161 GitRepository.check_url(src_url, self.config)
162
162
163 if create:
163 if create:
164 if bare:
164 if bare:
165 self._remote.init_bare()
165 self._remote.init_bare()
166 else:
166 else:
167 self._remote.init()
167 self._remote.init()
168
168
169 if src_url and bare:
169 if src_url and bare:
170 # bare repository only allows a fetch and checkout is not allowed
170 # bare repository only allows a fetch and checkout is not allowed
171 self.fetch(src_url, commit_ids=None)
171 self.fetch(src_url, commit_ids=None)
172 elif src_url:
172 elif src_url:
173 self.pull(src_url, commit_ids=None,
173 self.pull(src_url, commit_ids=None,
174 update_after=do_workspace_checkout)
174 update_after=do_workspace_checkout)
175
175
176 else:
176 else:
177 if not self._remote.assert_correct_path():
177 if not self._remote.assert_correct_path():
178 raise RepositoryError(
178 raise RepositoryError(
179 f'Path "{self.path}" does not contain a Git repository')
179 f'Path "{self.path}" does not contain a Git repository')
180
180
181 # TODO: johbo: check if we have to translate the OSError here
181 # TODO: johbo: check if we have to translate the OSError here
182 except OSError as err:
182 except OSError as err:
183 raise RepositoryError(err)
183 raise RepositoryError(err)
184
184
185 def _get_all_commit_ids(self):
185 def _get_all_commit_ids(self):
186 return self._remote.get_all_commit_ids()
186 return self._remote.get_all_commit_ids()
187
187
188 def _get_commit_ids(self, filters=None):
188 def _get_commit_ids(self, filters=None):
189 # we must check if this repo is not empty, since later command
189 # we must check if this repo is not empty, since later command
190 # fails if it is. And it's cheaper to ask than throw the subprocess
190 # fails if it is. And it's cheaper to ask than throw the subprocess
191 # errors
191 # errors
192
192
193 head = self._remote.head(show_exc=False)
193 head = self._remote.head(show_exc=False)
194
194
195 if not head:
195 if not head:
196 return []
196 return []
197
197
198 rev_filter = ['--branches', '--tags']
198 rev_filter = ['--branches', '--tags']
199 extra_filter = []
199 extra_filter = []
200
200
201 if filters:
201 if filters:
202 if filters.get('since'):
202 if filters.get('since'):
203 extra_filter.append('--since=%s' % (filters['since']))
203 extra_filter.append('--since=%s' % (filters['since']))
204 if filters.get('until'):
204 if filters.get('until'):
205 extra_filter.append('--until=%s' % (filters['until']))
205 extra_filter.append('--until=%s' % (filters['until']))
206 if filters.get('branch_name'):
206 if filters.get('branch_name'):
207 rev_filter = []
207 rev_filter = []
208 extra_filter.append(filters['branch_name'])
208 extra_filter.append(filters['branch_name'])
209 rev_filter.extend(extra_filter)
209 rev_filter.extend(extra_filter)
210
210
211 # if filters.get('start') or filters.get('end'):
211 # if filters.get('start') or filters.get('end'):
212 # # skip is offset, max-count is limit
212 # # skip is offset, max-count is limit
213 # if filters.get('start'):
213 # if filters.get('start'):
214 # extra_filter += ' --skip=%s' % filters['start']
214 # extra_filter += ' --skip=%s' % filters['start']
215 # if filters.get('end'):
215 # if filters.get('end'):
216 # extra_filter += ' --max-count=%s' % (filters['end'] - (filters['start'] or 0))
216 # extra_filter += ' --max-count=%s' % (filters['end'] - (filters['start'] or 0))
217
217
218 cmd = ['rev-list', '--reverse', '--date-order'] + rev_filter
218 cmd = ['rev-list', '--reverse', '--date-order'] + rev_filter
219 try:
219 try:
220 output, __ = self.run_git_command(cmd)
220 output, __ = self.run_git_command(cmd)
221 except RepositoryError:
221 except RepositoryError:
222 # Can be raised for empty repositories
222 # Can be raised for empty repositories
223 return []
223 return []
224 return output.splitlines()
224 return output.splitlines()
225
225
226 def _lookup_commit(self, commit_id_or_idx, translate_tag=True, maybe_unreachable=False, reference_obj=None):
226 def _lookup_commit(self, commit_id_or_idx, translate_tag=True, maybe_unreachable=False, reference_obj=None):
227
227
228 def is_null(value):
228 def is_null(value):
229 return len(value) == commit_id_or_idx.count('0')
229 return len(value) == commit_id_or_idx.count('0')
230
230
231 if commit_id_or_idx in (None, '', 'tip', 'HEAD', 'head', -1):
231 if commit_id_or_idx in (None, '', 'tip', 'HEAD', 'head', -1):
232 return self.commit_ids[-1]
232 return self.commit_ids[-1]
233
233
234 commit_missing_err = "Commit {} does not exist for `{}`".format(
234 commit_missing_err = "Commit {} does not exist for `{}`".format(
235 *map(safe_str, [commit_id_or_idx, self.name]))
235 *map(safe_str, [commit_id_or_idx, self.name]))
236
236
237 is_bstr = isinstance(commit_id_or_idx, str)
237 is_bstr = isinstance(commit_id_or_idx, str)
238 is_branch = reference_obj and reference_obj.branch
238 is_branch = reference_obj and reference_obj.branch
239
239
240 lookup_ok = False
240 lookup_ok = False
241 if is_bstr:
241 if is_bstr:
242 # Need to call remote to translate id for tagging scenarios,
242 # Need to call remote to translate id for tagging scenarios,
243 # or branch that are numeric
243 # or branch that are numeric
244 try:
244 try:
245 remote_data = self._remote.get_object(commit_id_or_idx,
245 remote_data = self._remote.get_object(commit_id_or_idx,
246 maybe_unreachable=maybe_unreachable)
246 maybe_unreachable=maybe_unreachable)
247 commit_id_or_idx = remote_data["commit_id"]
247 commit_id_or_idx = remote_data["commit_id"]
248 lookup_ok = True
248 lookup_ok = True
249 except (CommitDoesNotExistError,):
249 except (CommitDoesNotExistError,):
250 lookup_ok = False
250 lookup_ok = False
251
251
252 if lookup_ok is False:
252 if lookup_ok is False:
253 is_numeric_idx = \
253 is_numeric_idx = \
254 (is_bstr and commit_id_or_idx.isdigit() and len(commit_id_or_idx) < 12) \
254 (is_bstr and commit_id_or_idx.isdigit() and len(commit_id_or_idx) < 12) \
255 or isinstance(commit_id_or_idx, int)
255 or isinstance(commit_id_or_idx, int)
256 if not is_branch and (is_numeric_idx or is_null(commit_id_or_idx)):
256 if not is_branch and (is_numeric_idx or is_null(commit_id_or_idx)):
257 try:
257 try:
258 commit_id_or_idx = self.commit_ids[int(commit_id_or_idx)]
258 commit_id_or_idx = self.commit_ids[int(commit_id_or_idx)]
259 lookup_ok = True
259 lookup_ok = True
260 except Exception:
260 except Exception:
261 raise CommitDoesNotExistError(commit_missing_err)
261 raise CommitDoesNotExistError(commit_missing_err)
262
262
263 # we failed regular lookup, and by integer number lookup
263 # we failed regular lookup, and by integer number lookup
264 if lookup_ok is False:
264 if lookup_ok is False:
265 raise CommitDoesNotExistError(commit_missing_err)
265 raise CommitDoesNotExistError(commit_missing_err)
266
266
267 # Ensure we return full id
267 # Ensure we return full id
268 if not SHA_PATTERN.match(str(commit_id_or_idx)):
268 if not SHA_PATTERN.match(str(commit_id_or_idx)):
269 raise CommitDoesNotExistError(
269 raise CommitDoesNotExistError(
270 "Given commit id %s not recognized" % commit_id_or_idx)
270 "Given commit id %s not recognized" % commit_id_or_idx)
271 return commit_id_or_idx
271 return commit_id_or_idx
272
272
273 def get_hook_location(self):
273 def get_hook_location(self):
274 """
274 """
275 returns absolute path to location where hooks are stored
275 returns absolute path to location where hooks are stored
276 """
276 """
277 loc = os.path.join(self.path, 'hooks')
277 loc = os.path.join(self.path, 'hooks')
278 if not self.bare:
278 if not self.bare:
279 loc = os.path.join(self.path, '.git', 'hooks')
279 loc = os.path.join(self.path, '.git', 'hooks')
280 return loc
280 return loc
281
281
282 @LazyProperty
282 @LazyProperty
283 def last_change(self):
283 def last_change(self):
284 """
284 """
285 Returns last change made on this repository as
285 Returns last change made on this repository as
286 `datetime.datetime` object.
286 `datetime.datetime` object.
287 """
287 """
288 try:
288 try:
289 return self.get_commit().date
289 return self.get_commit().date
290 except RepositoryError:
290 except RepositoryError:
291 tzoffset = makedate()[1]
291 tzoffset = makedate()[1]
292 return utcdate_fromtimestamp(self._get_fs_mtime(), tzoffset)
292 return utcdate_fromtimestamp(self._get_fs_mtime(), tzoffset)
293
293
294 def _get_fs_mtime(self):
294 def _get_fs_mtime(self):
295 idx_loc = '' if self.bare else '.git'
295 idx_loc = '' if self.bare else '.git'
296 # fallback to filesystem
296 # fallback to filesystem
297 in_path = os.path.join(self.path, idx_loc, "index")
297 in_path = os.path.join(self.path, idx_loc, "index")
298 he_path = os.path.join(self.path, idx_loc, "HEAD")
298 he_path = os.path.join(self.path, idx_loc, "HEAD")
299 if os.path.exists(in_path):
299 if os.path.exists(in_path):
300 return os.stat(in_path).st_mtime
300 return os.stat(in_path).st_mtime
301 else:
301 else:
302 return os.stat(he_path).st_mtime
302 return os.stat(he_path).st_mtime
303
303
304 @LazyProperty
304 @LazyProperty
305 def description(self):
305 def description(self):
306 description = self._remote.get_description()
306 description = self._remote.get_description()
307 return safe_str(description or self.DEFAULT_DESCRIPTION)
307 return safe_str(description or self.DEFAULT_DESCRIPTION)
308
308
309 def _get_refs_entries(self, prefix='', reverse=False, strip_prefix=True):
309 def _get_refs_entries(self, prefix='', reverse=False, strip_prefix=True):
310 if self.is_empty():
310 if self.is_empty():
311 return OrderedDict()
311 return OrderedDict()
312
312
313 result = []
313 result = []
314 for ref, sha in self._refs.items():
314 for ref, sha in self._refs.items():
315 if ref.startswith(prefix):
315 if ref.startswith(prefix):
316 ref_name = ref
316 ref_name = ref
317 if strip_prefix:
317 if strip_prefix:
318 ref_name = ref[len(prefix):]
318 ref_name = ref[len(prefix):]
319 result.append((safe_str(ref_name), sha))
319 result.append((safe_str(ref_name), sha))
320
320
321 def get_name(entry):
321 def get_name(entry):
322 return entry[0]
322 return entry[0]
323
323
324 return OrderedDict(sorted(result, key=get_name, reverse=reverse))
324 return OrderedDict(sorted(result, key=get_name, reverse=reverse))
325
325
326 def _get_branches(self):
326 def _get_branches(self):
327 return self._get_refs_entries(prefix='refs/heads/', strip_prefix=True)
327 return self._get_refs_entries(prefix='refs/heads/', strip_prefix=True)
328
328
329 @CachedProperty
329 @CachedProperty
330 def branches(self):
330 def branches(self):
331 return self._get_branches()
331 return self._get_branches()
332
332
333 @CachedProperty
333 @CachedProperty
334 def branches_closed(self):
334 def branches_closed(self):
335 return {}
335 return {}
336
336
337 @CachedProperty
337 @CachedProperty
338 def bookmarks(self):
338 def bookmarks(self):
339 return {}
339 return {}
340
340
341 @CachedProperty
341 @CachedProperty
342 def branches_all(self):
342 def branches_all(self):
343 all_branches = {}
343 all_branches = {}
344 all_branches.update(self.branches)
344 all_branches.update(self.branches)
345 all_branches.update(self.branches_closed)
345 all_branches.update(self.branches_closed)
346 return all_branches
346 return all_branches
347
347
348 @CachedProperty
348 @CachedProperty
349 def tags(self):
349 def tags(self):
350 return self._get_tags()
350 return self._get_tags()
351
351
352 def _get_tags(self):
352 def _get_tags(self):
353 return self._get_refs_entries(prefix='refs/tags/', strip_prefix=True, reverse=True)
353 return self._get_refs_entries(prefix='refs/tags/', strip_prefix=True, reverse=True)
354
354
355 def tag(self, name, user, commit_id=None, message=None, date=None,
355 def tag(self, name, user, commit_id=None, message=None, date=None,
356 **kwargs):
356 **kwargs):
357 # TODO: fix this method to apply annotated tags correct with message
357 # TODO: fix this method to apply annotated tags correct with message
358 """
358 """
359 Creates and returns a tag for the given ``commit_id``.
359 Creates and returns a tag for the given ``commit_id``.
360
360
361 :param name: name for new tag
361 :param name: name for new tag
362 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
362 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
363 :param commit_id: commit id for which new tag would be created
363 :param commit_id: commit id for which new tag would be created
364 :param message: message of the tag's commit
364 :param message: message of the tag's commit
365 :param date: date of tag's commit
365 :param date: date of tag's commit
366
366
367 :raises TagAlreadyExistError: if tag with same name already exists
367 :raises TagAlreadyExistError: if tag with same name already exists
368 """
368 """
369 if name in self.tags:
369 if name in self.tags:
370 raise TagAlreadyExistError("Tag %s already exists" % name)
370 raise TagAlreadyExistError("Tag %s already exists" % name)
371 commit = self.get_commit(commit_id=commit_id)
371 commit = self.get_commit(commit_id=commit_id)
372 message = message or f"Added tag {name} for commit {commit.raw_id}"
372 message = message or f"Added tag {name} for commit {commit.raw_id}"
373
373
374 self._remote.set_refs('refs/tags/%s' % name, commit.raw_id)
374 self._remote.set_refs('refs/tags/%s' % name, commit.raw_id)
375
375
376 self._invalidate_prop_cache('tags')
376 self._invalidate_prop_cache('tags')
377 self._invalidate_prop_cache('_refs')
377 self._invalidate_prop_cache('_refs')
378
378
379 return commit
379 return commit
380
380
381 def remove_tag(self, name, user, message=None, date=None):
381 def remove_tag(self, name, user, message=None, date=None):
382 """
382 """
383 Removes tag with the given ``name``.
383 Removes tag with the given ``name``.
384
384
385 :param name: name of the tag to be removed
385 :param name: name of the tag to be removed
386 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
386 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
387 :param message: message of the tag's removal commit
387 :param message: message of the tag's removal commit
388 :param date: date of tag's removal commit
388 :param date: date of tag's removal commit
389
389
390 :raises TagDoesNotExistError: if tag with given name does not exists
390 :raises TagDoesNotExistError: if tag with given name does not exists
391 """
391 """
392 if name not in self.tags:
392 if name not in self.tags:
393 raise TagDoesNotExistError("Tag %s does not exist" % name)
393 raise TagDoesNotExistError("Tag %s does not exist" % name)
394
394
395 self._remote.tag_remove(name)
395 self._remote.tag_remove(name)
396 self._invalidate_prop_cache('tags')
396 self._invalidate_prop_cache('tags')
397 self._invalidate_prop_cache('_refs')
397 self._invalidate_prop_cache('_refs')
398
398
399 def _get_refs(self):
399 def _get_refs(self):
400 return self._remote.get_refs()
400 return self._remote.get_refs()
401
401
402 @CachedProperty
402 @CachedProperty
403 def _refs(self):
403 def _refs(self):
404 return self._get_refs()
404 return self._get_refs()
405
405
406 @property
406 @property
407 def _ref_tree(self):
407 def _ref_tree(self):
408 node = tree = {}
408 node = tree = {}
409 for ref, sha in self._refs.items():
409 for ref, sha in self._refs.items():
410 path = ref.split('/')
410 path = ref.split('/')
411 for bit in path[:-1]:
411 for bit in path[:-1]:
412 node = node.setdefault(bit, {})
412 node = node.setdefault(bit, {})
413 node[path[-1]] = sha
413 node[path[-1]] = sha
414 node = tree
414 node = tree
415 return tree
415 return tree
416
416
417 def get_remote_ref(self, ref_name):
417 def get_remote_ref(self, ref_name):
418 ref_key = f'refs/remotes/origin/{safe_str(ref_name)}'
418 ref_key = f'refs/remotes/origin/{safe_str(ref_name)}'
419 try:
419 try:
420 return self._refs[ref_key]
420 return self._refs[ref_key]
421 except Exception:
421 except Exception:
422 return
422 return
423
423
424 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None,
424 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None,
425 translate_tag=True, maybe_unreachable=False, reference_obj=None):
425 translate_tag=True, maybe_unreachable=False, reference_obj=None):
426 """
426 """
427 Returns `GitCommit` object representing commit from git repository
427 Returns `GitCommit` object representing commit from git repository
428 at the given `commit_id` or head (most recent commit) if None given.
428 at the given `commit_id` or head (most recent commit) if None given.
429 """
429 """
430
430
431 if self.is_empty():
431 if self.is_empty():
432 raise EmptyRepositoryError("There are no commits yet")
432 raise EmptyRepositoryError("There are no commits yet")
433
433
434 if commit_id is not None:
434 if commit_id is not None:
435 self._validate_commit_id(commit_id)
435 self._validate_commit_id(commit_id)
436 try:
436 try:
437 # we have cached idx, use it without contacting the remote
437 # we have cached idx, use it without contacting the remote
438 idx = self._commit_ids[commit_id]
438 idx = self._commit_ids[commit_id]
439 return GitCommit(self, commit_id, idx, pre_load=pre_load)
439 return GitCommit(self, commit_id, idx, pre_load=pre_load)
440 except KeyError:
440 except KeyError:
441 pass
441 pass
442
442
443 elif commit_idx is not None:
443 elif commit_idx is not None:
444 self._validate_commit_idx(commit_idx)
444 self._validate_commit_idx(commit_idx)
445 try:
445 try:
446 _commit_id = self.commit_ids[commit_idx]
446 _commit_id = self.commit_ids[commit_idx]
447 if commit_idx < 0:
447 if commit_idx < 0:
448 commit_idx = self.commit_ids.index(_commit_id)
448 commit_idx = self.commit_ids.index(_commit_id)
449 return GitCommit(self, _commit_id, commit_idx, pre_load=pre_load)
449 return GitCommit(self, _commit_id, commit_idx, pre_load=pre_load)
450 except IndexError:
450 except IndexError:
451 commit_id = commit_idx
451 commit_id = commit_idx
452 else:
452 else:
453 commit_id = "tip"
453 commit_id = "tip"
454
454
455 if translate_tag:
455 if translate_tag:
456 commit_id = self._lookup_commit(
456 commit_id = self._lookup_commit(
457 commit_id, maybe_unreachable=maybe_unreachable,
457 commit_id, maybe_unreachable=maybe_unreachable,
458 reference_obj=reference_obj)
458 reference_obj=reference_obj)
459
459
460 try:
460 try:
461 idx = self._commit_ids[commit_id]
461 idx = self._commit_ids[commit_id]
462 except KeyError:
462 except KeyError:
463 idx = -1
463 idx = -1
464
464
465 return GitCommit(self, commit_id, idx, pre_load=pre_load)
465 return GitCommit(self, commit_id, idx, pre_load=pre_load)
466
466
467 def get_commits(
467 def get_commits(
468 self, start_id=None, end_id=None, start_date=None, end_date=None,
468 self, start_id=None, end_id=None, start_date=None, end_date=None,
469 branch_name=None, show_hidden=False, pre_load=None, translate_tags=True):
469 branch_name=None, show_hidden=False, pre_load=None, translate_tags=True):
470 """
470 """
471 Returns generator of `GitCommit` objects from start to end (both
471 Returns generator of `GitCommit` objects from start to end (both
472 are inclusive), in ascending date order.
472 are inclusive), in ascending date order.
473
473
474 :param start_id: None, str(commit_id)
474 :param start_id: None, str(commit_id)
475 :param end_id: None, str(commit_id)
475 :param end_id: None, str(commit_id)
476 :param start_date: if specified, commits with commit date less than
476 :param start_date: if specified, commits with commit date less than
477 ``start_date`` would be filtered out from returned set
477 ``start_date`` would be filtered out from returned set
478 :param end_date: if specified, commits with commit date greater than
478 :param end_date: if specified, commits with commit date greater than
479 ``end_date`` would be filtered out from returned set
479 ``end_date`` would be filtered out from returned set
480 :param branch_name: if specified, commits not reachable from given
480 :param branch_name: if specified, commits not reachable from given
481 branch would be filtered out from returned set
481 branch would be filtered out from returned set
482 :param show_hidden: Show hidden commits such as obsolete or hidden from
482 :param show_hidden: Show hidden commits such as obsolete or hidden from
483 Mercurial evolve
483 Mercurial evolve
484 :raise BranchDoesNotExistError: If given `branch_name` does not
484 :raise BranchDoesNotExistError: If given `branch_name` does not
485 exist.
485 exist.
486 :raise CommitDoesNotExistError: If commits for given `start` or
486 :raise CommitDoesNotExistError: If commits for given `start` or
487 `end` could not be found.
487 `end` could not be found.
488
488
489 """
489 """
490 if self.is_empty():
490 if self.is_empty():
491 raise EmptyRepositoryError("There are no commits yet")
491 raise EmptyRepositoryError("There are no commits yet")
492
492
493 self._validate_branch_name(branch_name)
493 self._validate_branch_name(branch_name)
494
494
495 if start_id is not None:
495 if start_id is not None:
496 self._validate_commit_id(start_id)
496 self._validate_commit_id(start_id)
497 if end_id is not None:
497 if end_id is not None:
498 self._validate_commit_id(end_id)
498 self._validate_commit_id(end_id)
499
499
500 start_raw_id = self._lookup_commit(start_id)
500 start_raw_id = self._lookup_commit(start_id)
501 start_pos = self._commit_ids[start_raw_id] if start_id else None
501 start_pos = self._commit_ids[start_raw_id] if start_id else None
502 end_raw_id = self._lookup_commit(end_id)
502 end_raw_id = self._lookup_commit(end_id)
503 end_pos = max(0, self._commit_ids[end_raw_id]) if end_id else None
503 end_pos = max(0, self._commit_ids[end_raw_id]) if end_id else None
504
504
505 if None not in [start_id, end_id] and start_pos > end_pos:
505 if None not in [start_id, end_id] and start_pos > end_pos:
506 raise RepositoryError(
506 raise RepositoryError(
507 "Start commit '%s' cannot be after end commit '%s'" %
507 "Start commit '%s' cannot be after end commit '%s'" %
508 (start_id, end_id))
508 (start_id, end_id))
509
509
510 if end_pos is not None:
510 if end_pos is not None:
511 end_pos += 1
511 end_pos += 1
512
512
513 filter_ = []
513 filter_ = []
514 if branch_name:
514 if branch_name:
515 filter_.append({'branch_name': branch_name})
515 filter_.append({'branch_name': branch_name})
516 if start_date and not end_date:
516 if start_date and not end_date:
517 filter_.append({'since': start_date})
517 filter_.append({'since': start_date})
518 if end_date and not start_date:
518 if end_date and not start_date:
519 filter_.append({'until': end_date})
519 filter_.append({'until': end_date})
520 if start_date and end_date:
520 if start_date and end_date:
521 filter_.append({'since': start_date})
521 filter_.append({'since': start_date})
522 filter_.append({'until': end_date})
522 filter_.append({'until': end_date})
523
523
524 # if start_pos or end_pos:
524 # if start_pos or end_pos:
525 # filter_.append({'start': start_pos})
525 # filter_.append({'start': start_pos})
526 # filter_.append({'end': end_pos})
526 # filter_.append({'end': end_pos})
527
527
528 if filter_:
528 if filter_:
529 revfilters = {
529 revfilters = {
530 'branch_name': branch_name,
530 'branch_name': branch_name,
531 'since': start_date.strftime('%m/%d/%y %H:%M:%S') if start_date else None,
531 'since': start_date.strftime('%m/%d/%y %H:%M:%S') if start_date else None,
532 'until': end_date.strftime('%m/%d/%y %H:%M:%S') if end_date else None,
532 'until': end_date.strftime('%m/%d/%y %H:%M:%S') if end_date else None,
533 'start': start_pos,
533 'start': start_pos,
534 'end': end_pos,
534 'end': end_pos,
535 }
535 }
536 commit_ids = self._get_commit_ids(filters=revfilters)
536 commit_ids = self._get_commit_ids(filters=revfilters)
537
537
538 else:
538 else:
539 commit_ids = self.commit_ids
539 commit_ids = self.commit_ids
540
540
541 if start_pos or end_pos:
541 if start_pos or end_pos:
542 commit_ids = commit_ids[start_pos: end_pos]
542 commit_ids = commit_ids[start_pos: end_pos]
543
543
544 return CollectionGenerator(self, commit_ids, pre_load=pre_load,
544 return CollectionGenerator(self, commit_ids, pre_load=pre_load,
545 translate_tag=translate_tags)
545 translate_tag=translate_tags)
546
546
547 def get_diff(
547 def get_diff(
548 self, commit1, commit2, path='', ignore_whitespace=False,
548 self, commit1, commit2, path='', ignore_whitespace=False,
549 context=3, path1=None):
549 context=3, path1=None):
550 """
550 """
551 Returns (git like) *diff*, as plain text. Shows changes introduced by
551 Returns (git like) *diff*, as plain text. Shows changes introduced by
552 ``commit2`` since ``commit1``.
552 ``commit2`` since ``commit1``.
553
553
554 :param commit1: Entry point from which diff is shown. Can be
554 :param commit1: Entry point from which diff is shown. Can be
555 ``self.EMPTY_COMMIT`` - in this case, patch showing all
555 ``self.EMPTY_COMMIT`` - in this case, patch showing all
556 the changes since empty state of the repository until ``commit2``
556 the changes since empty state of the repository until ``commit2``
557 :param commit2: Until which commits changes should be shown.
557 :param commit2: Until which commits changes should be shown.
558 :param path:
558 :param path:
559 :param ignore_whitespace: If set to ``True``, would not show whitespace
559 :param ignore_whitespace: If set to ``True``, would not show whitespace
560 changes. Defaults to ``False``.
560 changes. Defaults to ``False``.
561 :param context: How many lines before/after changed lines should be
561 :param context: How many lines before/after changed lines should be
562 shown. Defaults to ``3``.
562 shown. Defaults to ``3``.
563 :param path1:
563 :param path1:
564 """
564 """
565 self._validate_diff_commits(commit1, commit2)
565 self._validate_diff_commits(commit1, commit2)
566 if path1 is not None and path1 != path:
566 if path1 is not None and path1 != path:
567 raise ValueError("Diff of two different paths not supported.")
567 raise ValueError("Diff of two different paths not supported.")
568
568
569 if path:
569 if path:
570 file_filter = path
570 file_filter = path
571 else:
571 else:
572 file_filter = None
572 file_filter = None
573
573
574 diff = self._remote.diff(
574 diff = self._remote.diff(
575 commit1.raw_id, commit2.raw_id, file_filter=file_filter,
575 commit1.raw_id, commit2.raw_id, file_filter=file_filter,
576 opt_ignorews=ignore_whitespace,
576 opt_ignorews=ignore_whitespace,
577 context=context)
577 context=context)
578
578
579 return GitDiff(diff)
579 return GitDiff(diff)
580
580
581 def strip(self, commit_id, branch_name):
581 def strip(self, commit_id, branch_name):
582 commit = self.get_commit(commit_id=commit_id)
582 commit = self.get_commit(commit_id=commit_id)
583 if commit.merge:
583 if commit.merge:
584 raise Exception('Cannot reset to merge commit')
584 raise Exception('Cannot reset to merge commit')
585
585
586 if not branch_name:
587 raise ValueError(f'git strip requires a valid branch name, got {branch_name} instead')
588
586 # parent is going to be the new head now
589 # parent is going to be the new head now
587 commit = commit.parents[0]
590 commit = commit.parents[0]
588 self._remote.set_refs('refs/heads/%s' % branch_name, commit.raw_id)
591 self._remote.update_refs(f'refs/heads/{branch_name}', commit.raw_id)
589
592
590 # clear cached properties
593 # clear cached properties
591 self._invalidate_prop_cache('commit_ids')
594 self._invalidate_prop_cache('commit_ids')
592 self._invalidate_prop_cache('_refs')
595 self._invalidate_prop_cache('_refs')
593 self._invalidate_prop_cache('branches')
596 self._invalidate_prop_cache('branches')
594
597
595 return len(self.commit_ids)
598 return len(self.commit_ids)
596
599
597 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
600 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
598 log.debug('Calculating common ancestor between %sc1:%s and %sc2:%s',
601 log.debug('Calculating common ancestor between %sc1:%s and %sc2:%s',
599 self, commit_id1, repo2, commit_id2)
602 self, commit_id1, repo2, commit_id2)
600
603
601 if commit_id1 == commit_id2:
604 if commit_id1 == commit_id2:
602 return commit_id1
605 return commit_id1
603
606
604 if self != repo2:
607 if self != repo2:
605 commits = self._remote.get_missing_revs(
608 commits = self._remote.get_missing_revs(
606 commit_id1, commit_id2, repo2.path)
609 commit_id1, commit_id2, repo2.path)
607 if commits:
610 if commits:
608 commit = repo2.get_commit(commits[-1])
611 commit = repo2.get_commit(commits[-1])
609 if commit.parents:
612 if commit.parents:
610 ancestor_id = commit.parents[0].raw_id
613 ancestor_id = commit.parents[0].raw_id
611 else:
614 else:
612 ancestor_id = None
615 ancestor_id = None
613 else:
616 else:
614 # no commits from other repo, ancestor_id is the commit_id2
617 # no commits from other repo, ancestor_id is the commit_id2
615 ancestor_id = commit_id2
618 ancestor_id = commit_id2
616 else:
619 else:
617 output, __ = self.run_git_command(
620 output, __ = self.run_git_command(
618 ['merge-base', commit_id1, commit_id2])
621 ['merge-base', commit_id1, commit_id2])
619 ancestor_id = self.COMMIT_ID_PAT.findall(output)[0]
622 ancestor_id = self.COMMIT_ID_PAT.findall(output)[0]
620
623
621 log.debug('Found common ancestor with sha: %s', ancestor_id)
624 log.debug('Found common ancestor with sha: %s', ancestor_id)
622
625
623 return ancestor_id
626 return ancestor_id
624
627
625 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
628 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
626 repo1 = self
629 repo1 = self
627 ancestor_id = None
630 ancestor_id = None
628
631
629 if commit_id1 == commit_id2:
632 if commit_id1 == commit_id2:
630 commits = []
633 commits = []
631 elif repo1 != repo2:
634 elif repo1 != repo2:
632 missing_ids = self._remote.get_missing_revs(commit_id1, commit_id2,
635 missing_ids = self._remote.get_missing_revs(commit_id1, commit_id2,
633 repo2.path)
636 repo2.path)
634 commits = [
637 commits = [
635 repo2.get_commit(commit_id=commit_id, pre_load=pre_load)
638 repo2.get_commit(commit_id=commit_id, pre_load=pre_load)
636 for commit_id in reversed(missing_ids)]
639 for commit_id in reversed(missing_ids)]
637 else:
640 else:
638 output, __ = repo1.run_git_command(
641 output, __ = repo1.run_git_command(
639 ['log', '--reverse', '--pretty=format: %H', '-s',
642 ['log', '--reverse', '--pretty=format: %H', '-s',
640 f'{commit_id1}..{commit_id2}'])
643 f'{commit_id1}..{commit_id2}'])
641 commits = [
644 commits = [
642 repo1.get_commit(commit_id=commit_id, pre_load=pre_load)
645 repo1.get_commit(commit_id=commit_id, pre_load=pre_load)
643 for commit_id in self.COMMIT_ID_PAT.findall(output)]
646 for commit_id in self.COMMIT_ID_PAT.findall(output)]
644
647
645 return commits
648 return commits
646
649
647 @LazyProperty
650 @LazyProperty
648 def in_memory_commit(self):
651 def in_memory_commit(self):
649 """
652 """
650 Returns ``GitInMemoryCommit`` object for this repository.
653 Returns ``GitInMemoryCommit`` object for this repository.
651 """
654 """
652 return GitInMemoryCommit(self)
655 return GitInMemoryCommit(self)
653
656
654 def pull(self, url, commit_ids=None, update_after=False):
657 def pull(self, url, commit_ids=None, update_after=False):
655 """
658 """
656 Pull changes from external location. Pull is different in GIT
659 Pull changes from external location. Pull is different in GIT
657 that fetch since it's doing a checkout
660 that fetch since it's doing a checkout
658
661
659 :param commit_ids: Optional. Can be set to a list of commit ids
662 :param commit_ids: Optional. Can be set to a list of commit ids
660 which shall be pulled from the other repository.
663 which shall be pulled from the other repository.
661 """
664 """
662 refs = None
665 refs = None
663 if commit_ids is not None:
666 if commit_ids is not None:
664 remote_refs = self._remote.get_remote_refs(url)
667 remote_refs = self._remote.get_remote_refs(url)
665 refs = [ref for ref in remote_refs if remote_refs[ref] in commit_ids]
668 refs = [ref for ref in remote_refs if remote_refs[ref] in commit_ids]
666 self._remote.pull(url, refs=refs, update_after=update_after)
669 self._remote.pull(url, refs=refs, update_after=update_after)
667 self._remote.invalidate_vcs_cache()
670 self._remote.invalidate_vcs_cache()
668
671
669 def fetch(self, url, commit_ids=None):
672 def fetch(self, url, commit_ids=None):
670 """
673 """
671 Fetch all git objects from external location.
674 Fetch all git objects from external location.
672 """
675 """
673 self._remote.sync_fetch(url, refs=commit_ids)
676 self._remote.sync_fetch(url, refs=commit_ids)
674 self._remote.invalidate_vcs_cache()
677 self._remote.invalidate_vcs_cache()
675
678
676 def push(self, url):
679 def push(self, url):
677 refs = None
680 refs = None
678 self._remote.sync_push(url, refs=refs)
681 self._remote.sync_push(url, refs=refs)
679
682
680 def set_refs(self, ref_name, commit_id):
683 def set_refs(self, ref_name, commit_id):
681 self._remote.set_refs(ref_name, commit_id)
684 self._remote.set_refs(ref_name, commit_id)
682 self._invalidate_prop_cache('_refs')
685 self._invalidate_prop_cache('_refs')
683
686
684 def remove_ref(self, ref_name):
687 def remove_ref(self, ref_name):
685 self._remote.remove_ref(ref_name)
688 self._remote.remove_ref(ref_name)
686 self._invalidate_prop_cache('_refs')
689 self._invalidate_prop_cache('_refs')
687
690
688 def run_gc(self, prune=True):
691 def run_gc(self, prune=True):
689 cmd = ['gc', '--aggressive']
692 cmd = ['gc', '--aggressive']
690 if prune:
693 if prune:
691 cmd += ['--prune=now']
694 cmd += ['--prune=now']
692 _stdout, stderr = self.run_git_command(cmd, fail_on_stderr=False)
695 _stdout, stderr = self.run_git_command(cmd, fail_on_stderr=False)
693 return stderr
696 return stderr
694
697
695 def _update_server_info(self):
698 def _update_server_info(self):
696 """
699 """
697 runs gits update-server-info command in this repo instance
700 runs gits update-server-info command in this repo instance
698 """
701 """
699 self._remote.update_server_info()
702 self._remote.update_server_info()
700
703
701 def _current_branch(self):
704 def _current_branch(self):
702 """
705 """
703 Return the name of the current branch.
706 Return the name of the current branch.
704
707
705 It only works for non bare repositories (i.e. repositories with a
708 It only works for non bare repositories (i.e. repositories with a
706 working copy)
709 working copy)
707 """
710 """
708 if self.bare:
711 if self.bare:
709 raise RepositoryError('Bare git repos do not have active branches')
712 raise RepositoryError('Bare git repos do not have active branches')
710
713
711 if self.is_empty():
714 if self.is_empty():
712 return None
715 return None
713
716
714 stdout, _ = self.run_git_command(['rev-parse', '--abbrev-ref', 'HEAD'])
717 stdout, _ = self.run_git_command(['rev-parse', '--abbrev-ref', 'HEAD'])
715 return stdout.strip()
718 return stdout.strip()
716
719
717 def _checkout(self, branch_name, create=False, force=False):
720 def _checkout(self, branch_name, create=False, force=False):
718 """
721 """
719 Checkout a branch in the working directory.
722 Checkout a branch in the working directory.
720
723
721 It tries to create the branch if create is True, failing if the branch
724 It tries to create the branch if create is True, failing if the branch
722 already exists.
725 already exists.
723
726
724 It only works for non bare repositories (i.e. repositories with a
727 It only works for non bare repositories (i.e. repositories with a
725 working copy)
728 working copy)
726 """
729 """
727 if self.bare:
730 if self.bare:
728 raise RepositoryError('Cannot checkout branches in a bare git repo')
731 raise RepositoryError('Cannot checkout branches in a bare git repo')
729
732
730 cmd = ['checkout']
733 cmd = ['checkout']
731 if force:
734 if force:
732 cmd.append('-f')
735 cmd.append('-f')
733 if create:
736 if create:
734 cmd.append('-b')
737 cmd.append('-b')
735 cmd.append(branch_name)
738 cmd.append(branch_name)
736 self.run_git_command(cmd, fail_on_stderr=False)
739 self.run_git_command(cmd, fail_on_stderr=False)
737
740
738 def _create_branch(self, branch_name, commit_id):
741 def _create_branch(self, branch_name, commit_id):
739 """
742 """
740 creates a branch in a GIT repo
743 creates a branch in a GIT repo
741 """
744 """
742 self._remote.create_branch(branch_name, commit_id)
745 self._remote.create_branch(branch_name, commit_id)
743
746
744 def _identify(self):
747 def _identify(self):
745 """
748 """
746 Return the current state of the working directory.
749 Return the current state of the working directory.
747 """
750 """
748 if self.bare:
751 if self.bare:
749 raise RepositoryError('Bare git repos do not have active branches')
752 raise RepositoryError('Bare git repos do not have active branches')
750
753
751 if self.is_empty():
754 if self.is_empty():
752 return None
755 return None
753
756
754 stdout, _ = self.run_git_command(['rev-parse', 'HEAD'])
757 stdout, _ = self.run_git_command(['rev-parse', 'HEAD'])
755 return stdout.strip()
758 return stdout.strip()
756
759
757 def _local_clone(self, clone_path, branch_name, source_branch=None):
760 def _local_clone(self, clone_path, branch_name, source_branch=None):
758 """
761 """
759 Create a local clone of the current repo.
762 Create a local clone of the current repo.
760 """
763 """
761 # N.B.(skreft): the --branch option is required as otherwise the shallow
764 # N.B.(skreft): the --branch option is required as otherwise the shallow
762 # clone will only fetch the active branch.
765 # clone will only fetch the active branch.
763 cmd = ['clone', '--branch', branch_name,
766 cmd = ['clone', '--branch', branch_name,
764 self.path, os.path.abspath(clone_path)]
767 self.path, os.path.abspath(clone_path)]
765
768
766 self.run_git_command(cmd, fail_on_stderr=False)
769 self.run_git_command(cmd, fail_on_stderr=False)
767
770
768 # if we get the different source branch, make sure we also fetch it for
771 # if we get the different source branch, make sure we also fetch it for
769 # merge conditions
772 # merge conditions
770 if source_branch and source_branch != branch_name:
773 if source_branch and source_branch != branch_name:
771 # check if the ref exists.
774 # check if the ref exists.
772 shadow_repo = GitRepository(os.path.abspath(clone_path))
775 shadow_repo = GitRepository(os.path.abspath(clone_path))
773 if shadow_repo.get_remote_ref(source_branch):
776 if shadow_repo.get_remote_ref(source_branch):
774 cmd = ['fetch', self.path, source_branch]
777 cmd = ['fetch', self.path, source_branch]
775 self.run_git_command(cmd, fail_on_stderr=False)
778 self.run_git_command(cmd, fail_on_stderr=False)
776
779
777 def _local_fetch(self, repository_path, branch_name, use_origin=False):
780 def _local_fetch(self, repository_path, branch_name, use_origin=False):
778 """
781 """
779 Fetch a branch from a local repository.
782 Fetch a branch from a local repository.
780 """
783 """
781 repository_path = os.path.abspath(repository_path)
784 repository_path = os.path.abspath(repository_path)
782 if repository_path == self.path:
785 if repository_path == self.path:
783 raise ValueError('Cannot fetch from the same repository')
786 raise ValueError('Cannot fetch from the same repository')
784
787
785 if use_origin:
788 if use_origin:
786 branch_name = '+{branch}:refs/heads/{branch}'.format(
789 branch_name = '+{branch}:refs/heads/{branch}'.format(
787 branch=branch_name)
790 branch=branch_name)
788
791
789 cmd = ['fetch', '--no-tags', '--update-head-ok',
792 cmd = ['fetch', '--no-tags', '--update-head-ok',
790 repository_path, branch_name]
793 repository_path, branch_name]
791 self.run_git_command(cmd, fail_on_stderr=False)
794 self.run_git_command(cmd, fail_on_stderr=False)
792
795
793 def _local_reset(self, branch_name):
796 def _local_reset(self, branch_name):
794 branch_name = f'{branch_name}'
797 branch_name = f'{branch_name}'
795 cmd = ['reset', '--hard', branch_name, '--']
798 cmd = ['reset', '--hard', branch_name, '--']
796 self.run_git_command(cmd, fail_on_stderr=False)
799 self.run_git_command(cmd, fail_on_stderr=False)
797
800
798 def _last_fetch_heads(self):
801 def _last_fetch_heads(self):
799 """
802 """
800 Return the last fetched heads that need merging.
803 Return the last fetched heads that need merging.
801
804
802 The algorithm is defined at
805 The algorithm is defined at
803 https://github.com/git/git/blob/v2.1.3/git-pull.sh#L283
806 https://github.com/git/git/blob/v2.1.3/git-pull.sh#L283
804 """
807 """
805 if not self.bare:
808 if not self.bare:
806 fetch_heads_path = os.path.join(self.path, '.git', 'FETCH_HEAD')
809 fetch_heads_path = os.path.join(self.path, '.git', 'FETCH_HEAD')
807 else:
810 else:
808 fetch_heads_path = os.path.join(self.path, 'FETCH_HEAD')
811 fetch_heads_path = os.path.join(self.path, 'FETCH_HEAD')
809
812
810 heads = []
813 heads = []
811 with open(fetch_heads_path) as f:
814 with open(fetch_heads_path) as f:
812 for line in f:
815 for line in f:
813 if ' not-for-merge ' in line:
816 if ' not-for-merge ' in line:
814 continue
817 continue
815 line = re.sub('\t.*', '', line, flags=re.DOTALL)
818 line = re.sub('\t.*', '', line, flags=re.DOTALL)
816 heads.append(line)
819 heads.append(line)
817
820
818 return heads
821 return heads
819
822
820 def get_shadow_instance(self, shadow_repository_path, enable_hooks=False, cache=False):
823 def get_shadow_instance(self, shadow_repository_path, enable_hooks=False, cache=False):
821 return GitRepository(shadow_repository_path, with_wire={"cache": cache})
824 return GitRepository(shadow_repository_path, with_wire={"cache": cache})
822
825
823 def _local_pull(self, repository_path, branch_name, ff_only=True):
826 def _local_pull(self, repository_path, branch_name, ff_only=True):
824 """
827 """
825 Pull a branch from a local repository.
828 Pull a branch from a local repository.
826 """
829 """
827 if self.bare:
830 if self.bare:
828 raise RepositoryError('Cannot pull into a bare git repository')
831 raise RepositoryError('Cannot pull into a bare git repository')
829 # N.B.(skreft): The --ff-only option is to make sure this is a
832 # N.B.(skreft): The --ff-only option is to make sure this is a
830 # fast-forward (i.e., we are only pulling new changes and there are no
833 # fast-forward (i.e., we are only pulling new changes and there are no
831 # conflicts with our current branch)
834 # conflicts with our current branch)
832 # Additionally, that option needs to go before --no-tags, otherwise git
835 # Additionally, that option needs to go before --no-tags, otherwise git
833 # pull complains about it being an unknown flag.
836 # pull complains about it being an unknown flag.
834 cmd = ['pull']
837 cmd = ['pull']
835 if ff_only:
838 if ff_only:
836 cmd.append('--ff-only')
839 cmd.append('--ff-only')
837 cmd.extend(['--no-tags', repository_path, branch_name])
840 cmd.extend(['--no-tags', repository_path, branch_name])
838 self.run_git_command(cmd, fail_on_stderr=False)
841 self.run_git_command(cmd, fail_on_stderr=False)
839
842
840 def _local_merge(self, merge_message, user_name, user_email, heads):
843 def _local_merge(self, merge_message, user_name, user_email, heads):
841 """
844 """
842 Merge the given head into the checked out branch.
845 Merge the given head into the checked out branch.
843
846
844 It will force a merge commit.
847 It will force a merge commit.
845
848
846 Currently it raises an error if the repo is empty, as it is not possible
849 Currently it raises an error if the repo is empty, as it is not possible
847 to create a merge commit in an empty repo.
850 to create a merge commit in an empty repo.
848
851
849 :param merge_message: The message to use for the merge commit.
852 :param merge_message: The message to use for the merge commit.
850 :param heads: the heads to merge.
853 :param heads: the heads to merge.
851 """
854 """
852 if self.bare:
855 if self.bare:
853 raise RepositoryError('Cannot merge into a bare git repository')
856 raise RepositoryError('Cannot merge into a bare git repository')
854
857
855 if not heads:
858 if not heads:
856 return
859 return
857
860
858 if self.is_empty():
861 if self.is_empty():
859 # TODO(skreft): do something more robust in this case.
862 # TODO(skreft): do something more robust in this case.
860 raise RepositoryError('Do not know how to merge into empty repositories yet')
863 raise RepositoryError('Do not know how to merge into empty repositories yet')
861 unresolved = None
864 unresolved = None
862
865
863 # N.B.(skreft): the --no-ff option is used to enforce the creation of a
866 # N.B.(skreft): the --no-ff option is used to enforce the creation of a
864 # commit message. We also specify the user who is doing the merge.
867 # commit message. We also specify the user who is doing the merge.
865 cmd = ['-c', f'user.name="{user_name}"',
868 cmd = ['-c', f'user.name="{user_name}"',
866 '-c', f'user.email={user_email}',
869 '-c', f'user.email={user_email}',
867 'merge', '--no-ff', '-m', safe_str(merge_message)]
870 'merge', '--no-ff', '-m', safe_str(merge_message)]
868
871
869 merge_cmd = cmd + heads
872 merge_cmd = cmd + heads
870
873
871 try:
874 try:
872 self.run_git_command(merge_cmd, fail_on_stderr=False)
875 self.run_git_command(merge_cmd, fail_on_stderr=False)
873 except RepositoryError:
876 except RepositoryError:
874 files = self.run_git_command(['diff', '--name-only', '--diff-filter', 'U'],
877 files = self.run_git_command(['diff', '--name-only', '--diff-filter', 'U'],
875 fail_on_stderr=False)[0].splitlines()
878 fail_on_stderr=False)[0].splitlines()
876 # NOTE(marcink): we add U notation for consistent with HG backend output
879 # NOTE(marcink): we add U notation for consistent with HG backend output
877 unresolved = [f'U {f}' for f in files]
880 unresolved = [f'U {f}' for f in files]
878
881
879 # Cleanup any merge leftovers
882 # Cleanup any merge leftovers
880 self._remote.invalidate_vcs_cache()
883 self._remote.invalidate_vcs_cache()
881 self.run_git_command(['merge', '--abort'], fail_on_stderr=False)
884 self.run_git_command(['merge', '--abort'], fail_on_stderr=False)
882
885
883 if unresolved:
886 if unresolved:
884 raise UnresolvedFilesInRepo(unresolved)
887 raise UnresolvedFilesInRepo(unresolved)
885 else:
888 else:
886 raise
889 raise
887
890
888 def _local_push(
891 def _local_push(
889 self, source_branch, repository_path, target_branch,
892 self, source_branch, repository_path, target_branch,
890 enable_hooks=False, rc_scm_data=None):
893 enable_hooks=False, rc_scm_data=None):
891 """
894 """
892 Push the source_branch to the given repository and target_branch.
895 Push the source_branch to the given repository and target_branch.
893
896
894 Currently it if the target_branch is not master and the target repo is
897 Currently it if the target_branch is not master and the target repo is
895 empty, the push will work, but then GitRepository won't be able to find
898 empty, the push will work, but then GitRepository won't be able to find
896 the pushed branch or the commits. As the HEAD will be corrupted (i.e.,
899 the pushed branch or the commits. As the HEAD will be corrupted (i.e.,
897 pointing to master, which does not exist).
900 pointing to master, which does not exist).
898
901
899 It does not run the hooks in the target repo.
902 It does not run the hooks in the target repo.
900 """
903 """
901 # TODO(skreft): deal with the case in which the target repo is empty,
904 # TODO(skreft): deal with the case in which the target repo is empty,
902 # and the target_branch is not master.
905 # and the target_branch is not master.
903 target_repo = GitRepository(repository_path)
906 target_repo = GitRepository(repository_path)
904 if (not target_repo.bare and
907 if (not target_repo.bare and
905 target_repo._current_branch() == target_branch):
908 target_repo._current_branch() == target_branch):
906 # Git prevents pushing to the checked out branch, so simulate it by
909 # Git prevents pushing to the checked out branch, so simulate it by
907 # pulling into the target repository.
910 # pulling into the target repository.
908 target_repo._local_pull(self.path, source_branch)
911 target_repo._local_pull(self.path, source_branch)
909 else:
912 else:
910 cmd = ['push', os.path.abspath(repository_path),
913 cmd = ['push', os.path.abspath(repository_path),
911 f'{source_branch}:{target_branch}']
914 f'{source_branch}:{target_branch}']
912 gitenv = {}
915 gitenv = {}
913 if rc_scm_data:
916 if rc_scm_data:
914 gitenv.update({'RC_SCM_DATA': rc_scm_data})
917 gitenv.update({'RC_SCM_DATA': rc_scm_data})
915
918
916 if not enable_hooks:
919 if not enable_hooks:
917 gitenv['RC_SKIP_HOOKS'] = '1'
920 gitenv['RC_SKIP_HOOKS'] = '1'
918 self.run_git_command(cmd, fail_on_stderr=False, extra_env=gitenv)
921 self.run_git_command(cmd, fail_on_stderr=False, extra_env=gitenv)
919
922
920 def _get_new_pr_branch(self, source_branch, target_branch):
923 def _get_new_pr_branch(self, source_branch, target_branch):
921 prefix = f'pr_{source_branch}-{target_branch}_'
924 prefix = f'pr_{source_branch}-{target_branch}_'
922 pr_branches = []
925 pr_branches = []
923 for branch in self.branches:
926 for branch in self.branches:
924 if branch.startswith(prefix):
927 if branch.startswith(prefix):
925 pr_branches.append(int(branch[len(prefix):]))
928 pr_branches.append(int(branch[len(prefix):]))
926
929
927 if not pr_branches:
930 if not pr_branches:
928 branch_id = 0
931 branch_id = 0
929 else:
932 else:
930 branch_id = max(pr_branches) + 1
933 branch_id = max(pr_branches) + 1
931
934
932 return '%s%d' % (prefix, branch_id)
935 return '%s%d' % (prefix, branch_id)
933
936
934 def _maybe_prepare_merge_workspace(
937 def _maybe_prepare_merge_workspace(
935 self, repo_id, workspace_id, target_ref, source_ref):
938 self, repo_id, workspace_id, target_ref, source_ref):
936 shadow_repository_path = self._get_shadow_repository_path(
939 shadow_repository_path = self._get_shadow_repository_path(
937 self.path, repo_id, workspace_id)
940 self.path, repo_id, workspace_id)
938 if not os.path.exists(shadow_repository_path):
941 if not os.path.exists(shadow_repository_path):
939 self._local_clone(
942 self._local_clone(
940 shadow_repository_path, target_ref.name, source_ref.name)
943 shadow_repository_path, target_ref.name, source_ref.name)
941 log.debug('Prepared %s shadow repository in %s',
944 log.debug('Prepared %s shadow repository in %s',
942 self.alias, shadow_repository_path)
945 self.alias, shadow_repository_path)
943
946
944 return shadow_repository_path
947 return shadow_repository_path
945
948
946 def _merge_repo(self, repo_id, workspace_id, target_ref,
949 def _merge_repo(self, repo_id, workspace_id, target_ref,
947 source_repo, source_ref, merge_message,
950 source_repo, source_ref, merge_message,
948 merger_name, merger_email, dry_run=False,
951 merger_name, merger_email, dry_run=False,
949 use_rebase=False, close_branch=False):
952 use_rebase=False, close_branch=False):
950
953
951 log.debug('Executing merge_repo with %s strategy, dry_run mode:%s',
954 log.debug('Executing merge_repo with %s strategy, dry_run mode:%s',
952 'rebase' if use_rebase else 'merge', dry_run)
955 'rebase' if use_rebase else 'merge', dry_run)
953
956
954 if target_ref.commit_id != self.branches[target_ref.name]:
957 if target_ref.commit_id != self.branches[target_ref.name]:
955 log.warning('Target ref %s commit mismatch %s vs %s', target_ref,
958 log.warning('Target ref %s commit mismatch %s vs %s', target_ref,
956 target_ref.commit_id, self.branches[target_ref.name])
959 target_ref.commit_id, self.branches[target_ref.name])
957 return MergeResponse(
960 return MergeResponse(
958 False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD,
961 False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD,
959 metadata={'target_ref': target_ref})
962 metadata={'target_ref': target_ref})
960
963
961 shadow_repository_path = self._maybe_prepare_merge_workspace(
964 shadow_repository_path = self._maybe_prepare_merge_workspace(
962 repo_id, workspace_id, target_ref, source_ref)
965 repo_id, workspace_id, target_ref, source_ref)
963 shadow_repo = self.get_shadow_instance(shadow_repository_path)
966 shadow_repo = self.get_shadow_instance(shadow_repository_path)
964
967
965 # checkout source, if it's different. Otherwise we could not
968 # checkout source, if it's different. Otherwise we could not
966 # fetch proper commits for merge testing
969 # fetch proper commits for merge testing
967 if source_ref.name != target_ref.name:
970 if source_ref.name != target_ref.name:
968 if shadow_repo.get_remote_ref(source_ref.name):
971 if shadow_repo.get_remote_ref(source_ref.name):
969 shadow_repo._checkout(source_ref.name, force=True)
972 shadow_repo._checkout(source_ref.name, force=True)
970
973
971 # checkout target, and fetch changes
974 # checkout target, and fetch changes
972 shadow_repo._checkout(target_ref.name, force=True)
975 shadow_repo._checkout(target_ref.name, force=True)
973
976
974 # fetch/reset pull the target, in case it is changed
977 # fetch/reset pull the target, in case it is changed
975 # this handles even force changes
978 # this handles even force changes
976 shadow_repo._local_fetch(self.path, target_ref.name, use_origin=True)
979 shadow_repo._local_fetch(self.path, target_ref.name, use_origin=True)
977 shadow_repo._local_reset(target_ref.name)
980 shadow_repo._local_reset(target_ref.name)
978
981
979 # Need to reload repo to invalidate the cache, or otherwise we cannot
982 # Need to reload repo to invalidate the cache, or otherwise we cannot
980 # retrieve the last target commit.
983 # retrieve the last target commit.
981 shadow_repo = self.get_shadow_instance(shadow_repository_path)
984 shadow_repo = self.get_shadow_instance(shadow_repository_path)
982 if target_ref.commit_id != shadow_repo.branches[target_ref.name]:
985 if target_ref.commit_id != shadow_repo.branches[target_ref.name]:
983 log.warning('Shadow Target ref %s commit mismatch %s vs %s',
986 log.warning('Shadow Target ref %s commit mismatch %s vs %s',
984 target_ref, target_ref.commit_id,
987 target_ref, target_ref.commit_id,
985 shadow_repo.branches[target_ref.name])
988 shadow_repo.branches[target_ref.name])
986 return MergeResponse(
989 return MergeResponse(
987 False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD,
990 False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD,
988 metadata={'target_ref': target_ref})
991 metadata={'target_ref': target_ref})
989
992
990 # calculate new branch
993 # calculate new branch
991 pr_branch = shadow_repo._get_new_pr_branch(
994 pr_branch = shadow_repo._get_new_pr_branch(
992 source_ref.name, target_ref.name)
995 source_ref.name, target_ref.name)
993 log.debug('using pull-request merge branch: `%s`', pr_branch)
996 log.debug('using pull-request merge branch: `%s`', pr_branch)
994 # checkout to temp branch, and fetch changes
997 # checkout to temp branch, and fetch changes
995 shadow_repo._checkout(pr_branch, create=True)
998 shadow_repo._checkout(pr_branch, create=True)
996 try:
999 try:
997 shadow_repo._local_fetch(source_repo.path, source_ref.name)
1000 shadow_repo._local_fetch(source_repo.path, source_ref.name)
998 except RepositoryError:
1001 except RepositoryError:
999 log.exception('Failure when doing local fetch on '
1002 log.exception('Failure when doing local fetch on '
1000 'shadow repo: %s', shadow_repo)
1003 'shadow repo: %s', shadow_repo)
1001 return MergeResponse(
1004 return MergeResponse(
1002 False, False, None, MergeFailureReason.MISSING_SOURCE_REF,
1005 False, False, None, MergeFailureReason.MISSING_SOURCE_REF,
1003 metadata={'source_ref': source_ref})
1006 metadata={'source_ref': source_ref})
1004
1007
1005 merge_ref = None
1008 merge_ref = None
1006 merge_failure_reason = MergeFailureReason.NONE
1009 merge_failure_reason = MergeFailureReason.NONE
1007 metadata = {}
1010 metadata = {}
1008 try:
1011 try:
1009 shadow_repo._local_merge(merge_message, merger_name, merger_email,
1012 shadow_repo._local_merge(merge_message, merger_name, merger_email,
1010 [source_ref.commit_id])
1013 [source_ref.commit_id])
1011 merge_possible = True
1014 merge_possible = True
1012
1015
1013 # Need to invalidate the cache, or otherwise we
1016 # Need to invalidate the cache, or otherwise we
1014 # cannot retrieve the merge commit.
1017 # cannot retrieve the merge commit.
1015 shadow_repo = shadow_repo.get_shadow_instance(shadow_repository_path)
1018 shadow_repo = shadow_repo.get_shadow_instance(shadow_repository_path)
1016 merge_commit_id = shadow_repo.branches[pr_branch]
1019 merge_commit_id = shadow_repo.branches[pr_branch]
1017
1020
1018 # Set a reference pointing to the merge commit. This reference may
1021 # Set a reference pointing to the merge commit. This reference may
1019 # be used to easily identify the last successful merge commit in
1022 # be used to easily identify the last successful merge commit in
1020 # the shadow repository.
1023 # the shadow repository.
1021 shadow_repo.set_refs('refs/heads/pr-merge', merge_commit_id)
1024 shadow_repo.set_refs('refs/heads/pr-merge', merge_commit_id)
1022 merge_ref = Reference('branch', 'pr-merge', merge_commit_id)
1025 merge_ref = Reference('branch', 'pr-merge', merge_commit_id)
1023 except RepositoryError as e:
1026 except RepositoryError as e:
1024 log.exception('Failure when doing local merge on git shadow repo')
1027 log.exception('Failure when doing local merge on git shadow repo')
1025 if isinstance(e, UnresolvedFilesInRepo):
1028 if isinstance(e, UnresolvedFilesInRepo):
1026 metadata['unresolved_files'] = '\n* conflict: ' + ('\n * conflict: '.join(e.args[0]))
1029 metadata['unresolved_files'] = '\n* conflict: ' + ('\n * conflict: '.join(e.args[0]))
1027
1030
1028 merge_possible = False
1031 merge_possible = False
1029 merge_failure_reason = MergeFailureReason.MERGE_FAILED
1032 merge_failure_reason = MergeFailureReason.MERGE_FAILED
1030
1033
1031 if merge_possible and not dry_run:
1034 if merge_possible and not dry_run:
1032 try:
1035 try:
1033 shadow_repo._local_push(
1036 shadow_repo._local_push(
1034 pr_branch, self.path, target_ref.name, enable_hooks=True,
1037 pr_branch, self.path, target_ref.name, enable_hooks=True,
1035 rc_scm_data=self.config.get('rhodecode', 'RC_SCM_DATA'))
1038 rc_scm_data=self.config.get('rhodecode', 'RC_SCM_DATA'))
1036 merge_succeeded = True
1039 merge_succeeded = True
1037 except RepositoryError:
1040 except RepositoryError:
1038 log.exception(
1041 log.exception(
1039 'Failure when doing local push from the shadow '
1042 'Failure when doing local push from the shadow '
1040 'repository to the target repository at %s.', self.path)
1043 'repository to the target repository at %s.', self.path)
1041 merge_succeeded = False
1044 merge_succeeded = False
1042 merge_failure_reason = MergeFailureReason.PUSH_FAILED
1045 merge_failure_reason = MergeFailureReason.PUSH_FAILED
1043 metadata['target'] = 'git shadow repo'
1046 metadata['target'] = 'git shadow repo'
1044 metadata['merge_commit'] = pr_branch
1047 metadata['merge_commit'] = pr_branch
1045 else:
1048 else:
1046 merge_succeeded = False
1049 merge_succeeded = False
1047
1050
1048 return MergeResponse(
1051 return MergeResponse(
1049 merge_possible, merge_succeeded, merge_ref, merge_failure_reason,
1052 merge_possible, merge_succeeded, merge_ref, merge_failure_reason,
1050 metadata=metadata)
1053 metadata=metadata)
@@ -1,197 +1,220 b''
1 <div class="panel panel-default">
1 <div class="panel panel-default">
2 <div class="panel-heading">
2 <div class="panel-heading">
3 <h3 class="panel-title">${_('Strip commits from repository')}</h3>
3 <h3 class="panel-title">${_('Strip commits from repository')}</h3>
4 </div>
4 </div>
5 <div class="panel-body">
5 <div class="panel-body">
6 %if c.rhodecode_db_repo.repo_type != 'svn':
6 %if c.rhodecode_db_repo.repo_type != 'svn':
7 <h4>${_('Please provide up to %d commits commits to strip') % c.strip_limit}</h4>
7 <h4>${_('Please provide up to %d commits commits to strip') % c.strip_limit}</h4>
8 <p>
8 <p>
9 ${_('In the first step commits will be verified for existance in the repository')}. </br>
9 ${_('In the first step commits will be verified for existance in the repository')}. </br>
10 ${_('In the second step, correct commits will be available for stripping')}.
10 ${_('In the second step, correct commits will be available for stripping')}.
11 </p>
11 </p>
12 ${h.secure_form(h.route_path('strip_check', repo_name=c.rhodecode_db_repo.repo_name), request=request)}
12 ${h.secure_form(h.route_path('strip_check', repo_name=c.rhodecode_db_repo.repo_name), request=request)}
13 <div id="change_body" class="field">
13 <div id="change_body" class="field">
14 <div id="box-1" class="inputx locked_input">
14 <div id="box-1" class="inputx locked_input">
15 <input class="text" id="changeset_id-1" name="changeset_id-1" size="59"
15 <input class="text" id="changeset_id-1" name="changeset_id-1" size="59"
16 placeholder="${_('Enter full 40 character commit sha')}" type="text" value="">
16 placeholder="${_('Enter full 40 character commit sha')}" type="text" value="">
17
18 %if c.rhodecode_db_repo.repo_type == 'git':
19 <input class="text" id="changeset_id_branch-1" name="changeset_id_branch-1" size="30"
20 placeholder="${_('Enter name of branch')}" type="text" value="">
21 %endif
22
17 <div id="plus_icon-1" class="btn btn-default plus_input_button" onclick="addNew(1);return false">
23 <div id="plus_icon-1" class="btn btn-default plus_input_button" onclick="addNew(1);return false">
18 <i class="icon-plus">${_('Add another commit')}</i>
24 <i class="icon-plus">${_('Add another commit')}</i>
19 </div>
25 </div>
20 </div>
26 </div>
21 </div>
27 </div>
22
28
23 <div id="results" style="display:none; padding: 10px 0px;"></div>
29 <div id="results" style="display:none; padding: 10px 0px;"></div>
24
30
25 <div class="buttons">
31 <div class="buttons">
26 <button id="strip_action" class="btn btn-small btn-primary" onclick="checkCommits();return false">
32 <button id="strip_action" class="btn btn-small btn-primary" onclick="checkCommits();return false">
27 ${_('Check commits')}
33 ${_('Check commits')}
28 </button>
34 </button>
29 </div>
35 </div>
30
36
31 ${h.end_form()}
37 ${h.end_form()}
32 %else:
38 %else:
33 <h4>${_('Sorry this functionality is not available for SVN repository')}</h4>
39 <h4>${_('Sorry this functionality is not available for SVN repository')}</h4>
34 %endif
40 %endif
35 </div>
41 </div>
36 </div>
42 </div>
37
43
38
44
39 <script>
45 <script>
40 var plus_leaf = 1;
46 var plus_leaf = 1;
41
47
42 addNew = function(number){
48 addNew = function(number){
43 if (number >= ${c.strip_limit}){
49 if (number >= ${c.strip_limit}){
44 return;
50 return;
45 }
51 }
46 var minus = '<i class="icon-minus">${_('Remove')}</i>';
52 var minus = '<i class="icon-minus">${_('Remove')}</i>';
47 $('#plus_icon-'+number).detach();
53 $('#plus_icon-'+number).detach();
48 number++;
54 number++;
49
55
50 var input = '<div id="box-'+number+'" class="inputx locked_input">'+
56 %if c.rhodecode_db_repo.repo_type == 'git':
51 '<input class="text" id="changeset_id-'+number+'" name="changeset_id-'+number+'" size="59" type="text" value=""' +
57 var input = '<div id="box-' + number + '" class="inputx locked_input">' +
52 'placeholder="${_('Enter full 40 character commit sha')}">'+
58 '<input class="text" id="changeset_id-' + number + '" name="changeset_id-' + number + '" size="59" type="text" value=""' +
53 '<div id="plus_icon-'+number+'" class="btn btn-default plus_input_button" onclick="addNew('+number+');return false">'+
59 'placeholder="${_('Enter full 40 character commit sha')}">' +
54 '<i class="icon-plus">${_('Add another commit')}</i>'+
60 '<input class="text" id="changeset_id_branch-' + number + '" name="changeset_id_branch-' + number + '" size="30" type="text" value=""' +
55 '</div>'+
61 'placeholder="${_('Enter name of branch')}">' +
56 '<div id="minus_icon-'+number+'" class="btn btn-default minus_input_button" onclick="delOld('+(number)+');return false">'+
62 '<div id="plus_icon-' + number + '" class="btn btn-default plus_input_button" onclick="addNew(' + number + ');return false">' +
63 '<i class="icon-plus">${_('Add another commit')}</i>' +
64 '</div>' +
65 '<div id="minus_icon-' + number + '" class="btn btn-default minus_input_button" onclick="delOld(' + (number) + ');return false">' +
57 minus +
66 minus +
58 '</div>' +
67 '</div>' +
59 '</div>';
68 '</div>';
69 %else:
70 var input = '<div id="box-'+number+'" class="inputx locked_input">'+
71 '<input class="text" id="changeset_id-'+number+'" name="changeset_id-'+number+'" size="59" type="text" value=""' +
72 'placeholder="${_('Enter full 40 character commit sha')}">'+
73 '<div id="plus_icon-'+number+'" class="btn btn-default plus_input_button" onclick="addNew('+number+');return false">'+
74 '<i class="icon-plus">${_('Add another commit')}</i>'+
75 '</div>'+
76 '<div id="minus_icon-'+number+'" class="btn btn-default minus_input_button" onclick="delOld('+(number)+');return false">'+
77 minus +
78 '</div>' +
79 '</div>';
80 %endif
81
60 $('#change_body').append(input);
82 $('#change_body').append(input);
61 plus_leaf++;
83 plus_leaf++;
62 };
84 };
63
85
64 reIndex = function(number){
86 reIndex = function(number){
65 for(var i=number;i<=plus_leaf;i++){
87 for(var i=number;i<=plus_leaf;i++){
66 var check = $('#box-'+i);
88 var check = $('#box-'+i);
67 if (check.length == 0){
89 if (check.length == 0){
68 var change = $('#box-'+(i+1));
90 var change = $('#box-'+(i+1));
69 change.attr('id','box-'+i);
91 change.attr('id','box-'+i);
70 var plus = $('#plus_icon-'+(i+1));
92 var plus = $('#plus_icon-'+(i+1));
71
93
72 if (plus.length != 0){
94 if (plus.length != 0){
73 plus.attr('id','plus_icon-'+i);
95 plus.attr('id','plus_icon-'+i);
74 plus.attr('onclick','addNew('+i+');return false');
96 plus.attr('onclick','addNew('+i+');return false');
75 plus_leaf--;
97 plus_leaf--;
76 }
98 }
77 var minus = $('#minus_icon-'+(i+1));
99 var minus = $('#minus_icon-'+(i+1));
78
100
79 minus.attr('id','minus_icon-'+i);
101 minus.attr('id','minus_icon-'+i);
80
102
81 minus.attr('onclick','delOld('+i+');re' +
103 minus.attr('onclick','delOld('+i+');re' +
82 'turn false');
104 'turn false');
83 var input = $('input#changeset_id-'+(i+1));
105 var input = $('input#changeset_id-'+(i+1));
84 input.attr('name','changeset_id-'+i);
106 input.attr('name','changeset_id-'+i);
85 input.attr('id','changeset_id-'+i);
107 input.attr('id','changeset_id-'+i);
86 }
108 }
87 }
109 }
88 };
110 };
89
111
90 delOld = function(number){
112 delOld = function(number){
91 $('#box-'+number).remove();
113 $('#box-'+number).remove();
92 number = number - 1;
114 number = number - 1;
93 var box = $('#box-'+number);
115 var box = $('#box-'+number);
94 var plus = '<div id="plus_icon-'+number+'" class="btn btn-default plus_input_button" onclick="addNew('+number +');return false">'+
116 var plus = '<div id="plus_icon-'+number+'" class="btn btn-default plus_input_button" onclick="addNew('+number +');return false">'+
95 '<i id="i_plus_icon-'+number+'" class="icon-plus">${_('Add another commit')}</i></div>';
117 '<i id="i_plus_icon-'+number+'" class="icon-plus">${_('Add another commit')}</i></div>';
96 var minus = $('#minus_icon-'+number);
118 var minus = $('#minus_icon-'+number);
97 if(number +1 == plus_leaf){
119 if(number +1 == plus_leaf){
98 minus.detach();
120 minus.detach();
99 box.append(plus);
121 box.append(plus);
100 box.append(minus);
122 box.append(minus);
101 plus_leaf --;
123 plus_leaf --;
102 }
124 }
103 reIndex(number+1);
125 reIndex(number+1);
104
126
105 };
127 };
106
128
107 var resultData = {
129 var resultData = {
108 'csrf_token': CSRF_TOKEN
130 'csrf_token': CSRF_TOKEN
109 };
131 };
110
132
111 checkCommits = function() {
133 checkCommits = function() {
112 var postData = $('form').serialize();
134 var postData = $('form').serialize();
113 $('#results').show();
135 $('#results').show();
114 $('#results').html('<h4>${_('Checking commits')}...</h4>');
136 $('#results').html('<h4>${_('Checking commits')}...</h4>');
115 var url = "${h.route_path('strip_check', repo_name=c.rhodecode_db_repo.repo_name)}";
137 var url = "${h.route_path('strip_check', repo_name=c.rhodecode_db_repo.repo_name)}";
116 var btn = $('#strip_action');
138 var btn = $('#strip_action');
117 btn.attr('disabled', 'disabled');
139 btn.attr('disabled', 'disabled');
118 btn.addClass('disabled');
140 btn.addClass('disabled');
119
141
120 var success = function (data) {
142 var success = function (data) {
121 resultData = {
143 resultData = {
122 'csrf_token': CSRF_TOKEN
144 'csrf_token': CSRF_TOKEN
123 };
145 };
124 var i = 0;
146 var i = 0;
125 var result = '<ol>';
147 var result = '<ol>';
126 $.each(data, function(index, value){
148 $.each(data, function(index, value){
127 i= index;
149 i= index;
128 var box = $('#box-'+index);
150 var box = $('#box-'+index);
129 if (value.rev){
151 if (value.rev){
130 resultData[index] = JSON.stringify(value);
152 resultData[index] = JSON.stringify(value);
131
153
132 var verifiedHtml = (
154 var verifiedHtml = (
133 '<li style="line-height:1.2em">' +
155 '<li style="line-height:1.2em">' +
134 '<code>{0}</code>' +
156 '<code>{0}</code>' +
135 '{1}' +
157 '{1}' +
136 '<div style="white-space:pre">' +
158 '<div style="white-space:pre">' +
137 'author: {2}\n' +
159 'author: {2}\n' +
138 'description: {3}' +
160 'description: {3}\n' +
161 'branch: {4}' +
139 '</div>' +
162 '</div>' +
140 '</li>').format(
163 '</li>').format(
141 value.rev,
164 value.rev,
142 "${_(' commit verified positive')}",
165 "${_(' commit verified positive')}",
143 value.author, value.comment
166 value.author, value.comment, value.branch
144 );
167 );
145 result += verifiedHtml;
168 result += verifiedHtml;
146 }
169 }
147 else {
170 else {
148 var verifiedHtml = (
171 var verifiedHtml = (
149 '<li style="line-height:1.2em">' +
172 '<li style="line-height:1.2em">' +
150 '<code><strike>{0}</strike></code>' +
173 '<code><strike>{0}</strike></code>' +
151 '{1}' +
174 '{1}' +
152 '</li>').format(
175 '</li>').format(
153 value.commit,
176 value.commit,
154 "${_(' commit verified negative')}"
177 "${_(' commit verified negative')}"
155 );
178 );
156 result += verifiedHtml;
179 result += verifiedHtml;
157 }
180 }
158 box.remove();
181 box.remove();
159 });
182 });
160 result += '</ol>';
183 result += '</ol>';
161 var box = $('#box-'+(parseInt(i)+1));
184 var box = $('#box-'+(parseInt(i)+1));
162 box.remove();
185 box.remove();
163 $('#results').html(result);
186 $('#results').html(result);
164 };
187 };
165
188
166 btn.html('Strip');
189 btn.html('Strip');
167 btn.removeAttr('disabled');
190 btn.removeAttr('disabled');
168 btn.removeClass('disabled');
191 btn.removeClass('disabled');
169 btn.attr('onclick','strip();return false;');
192 btn.attr('onclick','strip();return false;');
170 ajaxPOST(url, postData, success, null);
193 ajaxPOST(url, postData, success, null);
171 };
194 };
172
195
173 strip = function() {
196 strip = function() {
174 var url = "${h.route_path('strip_execute', repo_name=c.rhodecode_db_repo.repo_name)}";
197 var url = "${h.route_path('strip_execute', repo_name=c.rhodecode_db_repo.repo_name)}";
175 var success = function(data) {
198 var success = function(data) {
176 var result = '<h4>Strip executed</h4><ol>';
199 var result = '<h4>Strip executed</h4><ol>';
177 $.each(data, function(index, value){
200 $.each(data, function(index, value){
178 if(data[index]) {
201 if(data[index]) {
179 result += '<li><code>' +index+ '</code> ${_(' commit striped successfully')}' + '</li>';
202 result += '<li><code>' +index+ '</code> ${_(' commit striped successfully')}' + '</li>';
180 }
203 }
181 else {
204 else {
182 result += '<li><code>' +index+ '</code> ${_(' commit strip failed')}' + '</li>';
205 result += '<li><code>' +index+ '</code> ${_(' commit strip failed')}' + '</li>';
183 }
206 }
184 });
207 });
185 if ($.isEmptyObject(data)) {
208 if ($.isEmptyObject(data)) {
186 result += '<li>Nothing done...</li>'
209 result += '<li>Nothing done...</li>'
187 }
210 }
188 result += '</ol>';
211 result += '</ol>';
189 $('#results').html(result);
212 $('#results').html(result);
190
213
191 };
214 };
192 ajaxPOST(url, resultData, success, null);
215 ajaxPOST(url, resultData, success, null);
193 var btn = $('#strip_action');
216 var btn = $('#strip_action');
194 btn.remove();
217 btn.remove();
195
218
196 };
219 };
197 </script>
220 </script>
General Comments 0
You need to be logged in to leave comments. Login now