##// END OF EJS Templates
feat(branch removal trough UI): Added delete_branch method for git. Fixes: RCCE-75
ilin.s -
r1246:f0acf3f6 default
parent child Browse files
Show More
@@ -1,1519 +1,1526 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2023 RhodeCode GmbH
2 # Copyright (C) 2014-2023 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
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 General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 import collections
18 import collections
19 import logging
19 import logging
20 import os
20 import os
21 import re
21 import re
22 import stat
22 import stat
23 import traceback
23 import traceback
24 import urllib.request
24 import urllib.request
25 import urllib.parse
25 import urllib.parse
26 import urllib.error
26 import urllib.error
27 from functools import wraps
27 from functools import wraps
28
28
29 import more_itertools
29 import more_itertools
30 import pygit2
30 import pygit2
31 from pygit2 import Repository as LibGit2Repo
31 from pygit2 import Repository as LibGit2Repo
32 from pygit2 import index as LibGit2Index
32 from pygit2 import index as LibGit2Index
33 from dulwich import index, objects
33 from dulwich import index, objects
34 from dulwich.client import HttpGitClient, LocalGitClient, FetchPackResult
34 from dulwich.client import HttpGitClient, LocalGitClient, FetchPackResult
35 from dulwich.errors import (
35 from dulwich.errors import (
36 NotGitRepository, ChecksumMismatch, WrongObjectException,
36 NotGitRepository, ChecksumMismatch, WrongObjectException,
37 MissingCommitError, ObjectMissing, HangupException,
37 MissingCommitError, ObjectMissing, HangupException,
38 UnexpectedCommandError)
38 UnexpectedCommandError)
39 from dulwich.repo import Repo as DulwichRepo
39 from dulwich.repo import Repo as DulwichRepo
40
40
41 import rhodecode
41 import rhodecode
42 from vcsserver import exceptions, settings, subprocessio
42 from vcsserver import exceptions, settings, subprocessio
43 from vcsserver.str_utils import safe_str, safe_int, safe_bytes, ascii_bytes, convert_to_str, splitnewlines
43 from vcsserver.str_utils import safe_str, safe_int, safe_bytes, ascii_bytes, convert_to_str, splitnewlines
44 from vcsserver.base import RepoFactory, obfuscate_qs, ArchiveNode, store_archive_in_cache, BytesEnvelope, BinaryEnvelope
44 from vcsserver.base import RepoFactory, obfuscate_qs, ArchiveNode, store_archive_in_cache, BytesEnvelope, BinaryEnvelope
45 from vcsserver.hgcompat import (
45 from vcsserver.hgcompat import (
46 hg_url as url_parser, httpbasicauthhandler, httpdigestauthhandler)
46 hg_url as url_parser, httpbasicauthhandler, httpdigestauthhandler)
47 from vcsserver.git_lfs.lib import LFSOidStore
47 from vcsserver.git_lfs.lib import LFSOidStore
48 from vcsserver.vcs_base import RemoteBase
48 from vcsserver.vcs_base import RemoteBase
49
49
50 DIR_STAT = stat.S_IFDIR
50 DIR_STAT = stat.S_IFDIR
51 FILE_MODE = stat.S_IFMT
51 FILE_MODE = stat.S_IFMT
52 GIT_LINK = objects.S_IFGITLINK
52 GIT_LINK = objects.S_IFGITLINK
53 PEELED_REF_MARKER = b'^{}'
53 PEELED_REF_MARKER = b'^{}'
54 HEAD_MARKER = b'HEAD'
54 HEAD_MARKER = b'HEAD'
55
55
56 log = logging.getLogger(__name__)
56 log = logging.getLogger(__name__)
57
57
58
58
59 def reraise_safe_exceptions(func):
59 def reraise_safe_exceptions(func):
60 """Converts Dulwich exceptions to something neutral."""
60 """Converts Dulwich exceptions to something neutral."""
61
61
62 @wraps(func)
62 @wraps(func)
63 def wrapper(*args, **kwargs):
63 def wrapper(*args, **kwargs):
64 try:
64 try:
65 return func(*args, **kwargs)
65 return func(*args, **kwargs)
66 except (ChecksumMismatch, WrongObjectException, MissingCommitError, ObjectMissing,) as e:
66 except (ChecksumMismatch, WrongObjectException, MissingCommitError, ObjectMissing,) as e:
67 exc = exceptions.LookupException(org_exc=e)
67 exc = exceptions.LookupException(org_exc=e)
68 raise exc(safe_str(e))
68 raise exc(safe_str(e))
69 except (HangupException, UnexpectedCommandError) as e:
69 except (HangupException, UnexpectedCommandError) as e:
70 exc = exceptions.VcsException(org_exc=e)
70 exc = exceptions.VcsException(org_exc=e)
71 raise exc(safe_str(e))
71 raise exc(safe_str(e))
72 except Exception:
72 except Exception:
73 # NOTE(marcink): because of how dulwich handles some exceptions
73 # NOTE(marcink): because of how dulwich handles some exceptions
74 # (KeyError on empty repos), we cannot track this and catch all
74 # (KeyError on empty repos), we cannot track this and catch all
75 # exceptions, it's an exceptions from other handlers
75 # exceptions, it's an exceptions from other handlers
76 #if not hasattr(e, '_vcs_kind'):
76 #if not hasattr(e, '_vcs_kind'):
77 #log.exception("Unhandled exception in git remote call")
77 #log.exception("Unhandled exception in git remote call")
78 #raise_from_original(exceptions.UnhandledException)
78 #raise_from_original(exceptions.UnhandledException)
79 raise
79 raise
80 return wrapper
80 return wrapper
81
81
82
82
83 class Repo(DulwichRepo):
83 class Repo(DulwichRepo):
84 """
84 """
85 A wrapper for dulwich Repo class.
85 A wrapper for dulwich Repo class.
86
86
87 Since dulwich is sometimes keeping .idx file descriptors open, it leads to
87 Since dulwich is sometimes keeping .idx file descriptors open, it leads to
88 "Too many open files" error. We need to close all opened file descriptors
88 "Too many open files" error. We need to close all opened file descriptors
89 once the repo object is destroyed.
89 once the repo object is destroyed.
90 """
90 """
91 def __del__(self):
91 def __del__(self):
92 if hasattr(self, 'object_store'):
92 if hasattr(self, 'object_store'):
93 self.close()
93 self.close()
94
94
95
95
96 class Repository(LibGit2Repo):
96 class Repository(LibGit2Repo):
97
97
98 def __enter__(self):
98 def __enter__(self):
99 return self
99 return self
100
100
101 def __exit__(self, exc_type, exc_val, exc_tb):
101 def __exit__(self, exc_type, exc_val, exc_tb):
102 self.free()
102 self.free()
103
103
104
104
105 class GitFactory(RepoFactory):
105 class GitFactory(RepoFactory):
106 repo_type = 'git'
106 repo_type = 'git'
107
107
108 def _create_repo(self, wire, create, use_libgit2=False):
108 def _create_repo(self, wire, create, use_libgit2=False):
109 if use_libgit2:
109 if use_libgit2:
110 repo = Repository(safe_bytes(wire['path']))
110 repo = Repository(safe_bytes(wire['path']))
111 else:
111 else:
112 # dulwich mode
112 # dulwich mode
113 repo_path = safe_str(wire['path'], to_encoding=settings.WIRE_ENCODING)
113 repo_path = safe_str(wire['path'], to_encoding=settings.WIRE_ENCODING)
114 repo = Repo(repo_path)
114 repo = Repo(repo_path)
115
115
116 log.debug('repository created: got GIT object: %s', repo)
116 log.debug('repository created: got GIT object: %s', repo)
117 return repo
117 return repo
118
118
119 def repo(self, wire, create=False, use_libgit2=False):
119 def repo(self, wire, create=False, use_libgit2=False):
120 """
120 """
121 Get a repository instance for the given path.
121 Get a repository instance for the given path.
122 """
122 """
123 return self._create_repo(wire, create, use_libgit2)
123 return self._create_repo(wire, create, use_libgit2)
124
124
125 def repo_libgit2(self, wire):
125 def repo_libgit2(self, wire):
126 return self.repo(wire, use_libgit2=True)
126 return self.repo(wire, use_libgit2=True)
127
127
128
128
129 def create_signature_from_string(author_str, **kwargs):
129 def create_signature_from_string(author_str, **kwargs):
130 """
130 """
131 Creates a pygit2.Signature object from a string of the format 'Name <email>'.
131 Creates a pygit2.Signature object from a string of the format 'Name <email>'.
132
132
133 :param author_str: String of the format 'Name <email>'
133 :param author_str: String of the format 'Name <email>'
134 :return: pygit2.Signature object
134 :return: pygit2.Signature object
135 """
135 """
136 match = re.match(r'^(.+) <(.+)>$', author_str)
136 match = re.match(r'^(.+) <(.+)>$', author_str)
137 if match is None:
137 if match is None:
138 raise ValueError(f"Invalid format: {author_str}")
138 raise ValueError(f"Invalid format: {author_str}")
139
139
140 name, email = match.groups()
140 name, email = match.groups()
141 return pygit2.Signature(name, email, **kwargs)
141 return pygit2.Signature(name, email, **kwargs)
142
142
143
143
144 def get_obfuscated_url(url_obj):
144 def get_obfuscated_url(url_obj):
145 url_obj.passwd = b'*****' if url_obj.passwd else url_obj.passwd
145 url_obj.passwd = b'*****' if url_obj.passwd else url_obj.passwd
146 url_obj.query = obfuscate_qs(url_obj.query)
146 url_obj.query = obfuscate_qs(url_obj.query)
147 obfuscated_uri = str(url_obj)
147 obfuscated_uri = str(url_obj)
148 return obfuscated_uri
148 return obfuscated_uri
149
149
150
150
151 class GitRemote(RemoteBase):
151 class GitRemote(RemoteBase):
152
152
153 def __init__(self, factory):
153 def __init__(self, factory):
154 self._factory = factory
154 self._factory = factory
155 self._bulk_methods = {
155 self._bulk_methods = {
156 "date": self.date,
156 "date": self.date,
157 "author": self.author,
157 "author": self.author,
158 "branch": self.branch,
158 "branch": self.branch,
159 "message": self.message,
159 "message": self.message,
160 "parents": self.parents,
160 "parents": self.parents,
161 "_commit": self.revision,
161 "_commit": self.revision,
162 }
162 }
163 self._bulk_file_methods = {
163 self._bulk_file_methods = {
164 "size": self.get_node_size,
164 "size": self.get_node_size,
165 "data": self.get_node_data,
165 "data": self.get_node_data,
166 "flags": self.get_node_flags,
166 "flags": self.get_node_flags,
167 "is_binary": self.get_node_is_binary,
167 "is_binary": self.get_node_is_binary,
168 "md5": self.md5_hash
168 "md5": self.md5_hash
169 }
169 }
170
170
171 def _wire_to_config(self, wire):
171 def _wire_to_config(self, wire):
172 if 'config' in wire:
172 if 'config' in wire:
173 return {x[0] + '_' + x[1]: x[2] for x in wire['config']}
173 return {x[0] + '_' + x[1]: x[2] for x in wire['config']}
174 return {}
174 return {}
175
175
176 def _remote_conf(self, config):
176 def _remote_conf(self, config):
177 params = [
177 params = [
178 '-c', 'core.askpass=""',
178 '-c', 'core.askpass=""',
179 ]
179 ]
180 config_attrs = {
180 config_attrs = {
181 'vcs_ssl_dir': 'http.sslCAinfo={}',
181 'vcs_ssl_dir': 'http.sslCAinfo={}',
182 'vcs_git_lfs_store_location': 'lfs.storage={}'
182 'vcs_git_lfs_store_location': 'lfs.storage={}'
183 }
183 }
184 for key, param in config_attrs.items():
184 for key, param in config_attrs.items():
185 if value := config.get(key):
185 if value := config.get(key):
186 params.extend(['-c', param.format(value)])
186 params.extend(['-c', param.format(value)])
187 return params
187 return params
188
188
189 @reraise_safe_exceptions
189 @reraise_safe_exceptions
190 def discover_git_version(self):
190 def discover_git_version(self):
191 stdout, _ = self.run_git_command(
191 stdout, _ = self.run_git_command(
192 {}, ['--version'], _bare=True, _safe=True)
192 {}, ['--version'], _bare=True, _safe=True)
193 prefix = b'git version'
193 prefix = b'git version'
194 if stdout.startswith(prefix):
194 if stdout.startswith(prefix):
195 stdout = stdout[len(prefix):]
195 stdout = stdout[len(prefix):]
196 return safe_str(stdout.strip())
196 return safe_str(stdout.strip())
197
197
198 @reraise_safe_exceptions
198 @reraise_safe_exceptions
199 def is_empty(self, wire):
199 def is_empty(self, wire):
200 repo_init = self._factory.repo_libgit2(wire)
200 repo_init = self._factory.repo_libgit2(wire)
201 with repo_init as repo:
201 with repo_init as repo:
202 try:
202 try:
203 has_head = repo.head.name
203 has_head = repo.head.name
204 if has_head:
204 if has_head:
205 return False
205 return False
206
206
207 # NOTE(marcink): check again using more expensive method
207 # NOTE(marcink): check again using more expensive method
208 return repo.is_empty
208 return repo.is_empty
209 except Exception:
209 except Exception:
210 pass
210 pass
211
211
212 return True
212 return True
213
213
214 @reraise_safe_exceptions
214 @reraise_safe_exceptions
215 def assert_correct_path(self, wire):
215 def assert_correct_path(self, wire):
216 cache_on, context_uid, repo_id = self._cache_on(wire)
216 cache_on, context_uid, repo_id = self._cache_on(wire)
217 region = self._region(wire)
217 region = self._region(wire)
218
218
219 @region.conditional_cache_on_arguments(condition=cache_on)
219 @region.conditional_cache_on_arguments(condition=cache_on)
220 def _assert_correct_path(_context_uid, _repo_id, fast_check):
220 def _assert_correct_path(_context_uid, _repo_id, fast_check):
221 if fast_check:
221 if fast_check:
222 path = safe_str(wire['path'])
222 path = safe_str(wire['path'])
223 if pygit2.discover_repository(path):
223 if pygit2.discover_repository(path):
224 return True
224 return True
225 return False
225 return False
226 else:
226 else:
227 try:
227 try:
228 repo_init = self._factory.repo_libgit2(wire)
228 repo_init = self._factory.repo_libgit2(wire)
229 with repo_init:
229 with repo_init:
230 pass
230 pass
231 except pygit2.GitError:
231 except pygit2.GitError:
232 path = wire.get('path')
232 path = wire.get('path')
233 tb = traceback.format_exc()
233 tb = traceback.format_exc()
234 log.debug("Invalid Git path `%s`, tb: %s", path, tb)
234 log.debug("Invalid Git path `%s`, tb: %s", path, tb)
235 return False
235 return False
236 return True
236 return True
237
237
238 return _assert_correct_path(context_uid, repo_id, True)
238 return _assert_correct_path(context_uid, repo_id, True)
239
239
240 @reraise_safe_exceptions
240 @reraise_safe_exceptions
241 def bare(self, wire):
241 def bare(self, wire):
242 repo_init = self._factory.repo_libgit2(wire)
242 repo_init = self._factory.repo_libgit2(wire)
243 with repo_init as repo:
243 with repo_init as repo:
244 return repo.is_bare
244 return repo.is_bare
245
245
246 @reraise_safe_exceptions
246 @reraise_safe_exceptions
247 def get_node_data(self, wire, commit_id, path):
247 def get_node_data(self, wire, commit_id, path):
248 repo_init = self._factory.repo_libgit2(wire)
248 repo_init = self._factory.repo_libgit2(wire)
249 with repo_init as repo:
249 with repo_init as repo:
250 commit = repo[commit_id]
250 commit = repo[commit_id]
251 blob_obj = commit.tree[path]
251 blob_obj = commit.tree[path]
252
252
253 if blob_obj.type != pygit2.GIT_OBJ_BLOB:
253 if blob_obj.type != pygit2.GIT_OBJ_BLOB:
254 raise exceptions.LookupException()(
254 raise exceptions.LookupException()(
255 f'Tree for commit_id:{commit_id} is not a blob: {blob_obj.type_str}')
255 f'Tree for commit_id:{commit_id} is not a blob: {blob_obj.type_str}')
256
256
257 return BytesEnvelope(blob_obj.data)
257 return BytesEnvelope(blob_obj.data)
258
258
259 @reraise_safe_exceptions
259 @reraise_safe_exceptions
260 def get_node_size(self, wire, commit_id, path):
260 def get_node_size(self, wire, commit_id, path):
261 repo_init = self._factory.repo_libgit2(wire)
261 repo_init = self._factory.repo_libgit2(wire)
262 with repo_init as repo:
262 with repo_init as repo:
263 commit = repo[commit_id]
263 commit = repo[commit_id]
264 blob_obj = commit.tree[path]
264 blob_obj = commit.tree[path]
265
265
266 if blob_obj.type != pygit2.GIT_OBJ_BLOB:
266 if blob_obj.type != pygit2.GIT_OBJ_BLOB:
267 raise exceptions.LookupException()(
267 raise exceptions.LookupException()(
268 f'Tree for commit_id:{commit_id} is not a blob: {blob_obj.type_str}')
268 f'Tree for commit_id:{commit_id} is not a blob: {blob_obj.type_str}')
269
269
270 return blob_obj.size
270 return blob_obj.size
271
271
272 @reraise_safe_exceptions
272 @reraise_safe_exceptions
273 def get_node_flags(self, wire, commit_id, path):
273 def get_node_flags(self, wire, commit_id, path):
274 repo_init = self._factory.repo_libgit2(wire)
274 repo_init = self._factory.repo_libgit2(wire)
275 with repo_init as repo:
275 with repo_init as repo:
276 commit = repo[commit_id]
276 commit = repo[commit_id]
277 blob_obj = commit.tree[path]
277 blob_obj = commit.tree[path]
278
278
279 if blob_obj.type != pygit2.GIT_OBJ_BLOB:
279 if blob_obj.type != pygit2.GIT_OBJ_BLOB:
280 raise exceptions.LookupException()(
280 raise exceptions.LookupException()(
281 f'Tree for commit_id:{commit_id} is not a blob: {blob_obj.type_str}')
281 f'Tree for commit_id:{commit_id} is not a blob: {blob_obj.type_str}')
282
282
283 return blob_obj.filemode
283 return blob_obj.filemode
284
284
285 @reraise_safe_exceptions
285 @reraise_safe_exceptions
286 def get_node_is_binary(self, wire, commit_id, path):
286 def get_node_is_binary(self, wire, commit_id, path):
287 repo_init = self._factory.repo_libgit2(wire)
287 repo_init = self._factory.repo_libgit2(wire)
288 with repo_init as repo:
288 with repo_init as repo:
289 commit = repo[commit_id]
289 commit = repo[commit_id]
290 blob_obj = commit.tree[path]
290 blob_obj = commit.tree[path]
291
291
292 if blob_obj.type != pygit2.GIT_OBJ_BLOB:
292 if blob_obj.type != pygit2.GIT_OBJ_BLOB:
293 raise exceptions.LookupException()(
293 raise exceptions.LookupException()(
294 f'Tree for commit_id:{commit_id} is not a blob: {blob_obj.type_str}')
294 f'Tree for commit_id:{commit_id} is not a blob: {blob_obj.type_str}')
295
295
296 return blob_obj.is_binary
296 return blob_obj.is_binary
297
297
298 @reraise_safe_exceptions
298 @reraise_safe_exceptions
299 def blob_as_pretty_string(self, wire, sha):
299 def blob_as_pretty_string(self, wire, sha):
300 repo_init = self._factory.repo_libgit2(wire)
300 repo_init = self._factory.repo_libgit2(wire)
301 with repo_init as repo:
301 with repo_init as repo:
302 blob_obj = repo[sha]
302 blob_obj = repo[sha]
303 return BytesEnvelope(blob_obj.data)
303 return BytesEnvelope(blob_obj.data)
304
304
305 @reraise_safe_exceptions
305 @reraise_safe_exceptions
306 def blob_raw_length(self, wire, sha):
306 def blob_raw_length(self, wire, sha):
307 cache_on, context_uid, repo_id = self._cache_on(wire)
307 cache_on, context_uid, repo_id = self._cache_on(wire)
308 region = self._region(wire)
308 region = self._region(wire)
309
309
310 @region.conditional_cache_on_arguments(condition=cache_on)
310 @region.conditional_cache_on_arguments(condition=cache_on)
311 def _blob_raw_length(_repo_id, _sha):
311 def _blob_raw_length(_repo_id, _sha):
312
312
313 repo_init = self._factory.repo_libgit2(wire)
313 repo_init = self._factory.repo_libgit2(wire)
314 with repo_init as repo:
314 with repo_init as repo:
315 blob = repo[sha]
315 blob = repo[sha]
316 return blob.size
316 return blob.size
317
317
318 return _blob_raw_length(repo_id, sha)
318 return _blob_raw_length(repo_id, sha)
319
319
320 def _parse_lfs_pointer(self, raw_content):
320 def _parse_lfs_pointer(self, raw_content):
321 spec_string = b'version https://git-lfs.github.com/spec'
321 spec_string = b'version https://git-lfs.github.com/spec'
322 if raw_content and raw_content.startswith(spec_string):
322 if raw_content and raw_content.startswith(spec_string):
323
323
324 pattern = re.compile(rb"""
324 pattern = re.compile(rb"""
325 (?:\n)?
325 (?:\n)?
326 ^version[ ]https://git-lfs\.github\.com/spec/(?P<spec_ver>v\d+)\n
326 ^version[ ]https://git-lfs\.github\.com/spec/(?P<spec_ver>v\d+)\n
327 ^oid[ ] sha256:(?P<oid_hash>[0-9a-f]{64})\n
327 ^oid[ ] sha256:(?P<oid_hash>[0-9a-f]{64})\n
328 ^size[ ](?P<oid_size>[0-9]+)\n
328 ^size[ ](?P<oid_size>[0-9]+)\n
329 (?:\n)?
329 (?:\n)?
330 """, re.VERBOSE | re.MULTILINE)
330 """, re.VERBOSE | re.MULTILINE)
331 match = pattern.match(raw_content)
331 match = pattern.match(raw_content)
332 if match:
332 if match:
333 return match.groupdict()
333 return match.groupdict()
334
334
335 return {}
335 return {}
336
336
337 @reraise_safe_exceptions
337 @reraise_safe_exceptions
338 def is_large_file(self, wire, commit_id):
338 def is_large_file(self, wire, commit_id):
339 cache_on, context_uid, repo_id = self._cache_on(wire)
339 cache_on, context_uid, repo_id = self._cache_on(wire)
340 region = self._region(wire)
340 region = self._region(wire)
341
341
342 @region.conditional_cache_on_arguments(condition=cache_on)
342 @region.conditional_cache_on_arguments(condition=cache_on)
343 def _is_large_file(_repo_id, _sha):
343 def _is_large_file(_repo_id, _sha):
344 repo_init = self._factory.repo_libgit2(wire)
344 repo_init = self._factory.repo_libgit2(wire)
345 with repo_init as repo:
345 with repo_init as repo:
346 blob = repo[commit_id]
346 blob = repo[commit_id]
347 if blob.is_binary:
347 if blob.is_binary:
348 return {}
348 return {}
349
349
350 return self._parse_lfs_pointer(blob.data)
350 return self._parse_lfs_pointer(blob.data)
351
351
352 return _is_large_file(repo_id, commit_id)
352 return _is_large_file(repo_id, commit_id)
353
353
354 @reraise_safe_exceptions
354 @reraise_safe_exceptions
355 def is_binary(self, wire, tree_id):
355 def is_binary(self, wire, tree_id):
356 cache_on, context_uid, repo_id = self._cache_on(wire)
356 cache_on, context_uid, repo_id = self._cache_on(wire)
357 region = self._region(wire)
357 region = self._region(wire)
358
358
359 @region.conditional_cache_on_arguments(condition=cache_on)
359 @region.conditional_cache_on_arguments(condition=cache_on)
360 def _is_binary(_repo_id, _tree_id):
360 def _is_binary(_repo_id, _tree_id):
361 repo_init = self._factory.repo_libgit2(wire)
361 repo_init = self._factory.repo_libgit2(wire)
362 with repo_init as repo:
362 with repo_init as repo:
363 blob_obj = repo[tree_id]
363 blob_obj = repo[tree_id]
364 return blob_obj.is_binary
364 return blob_obj.is_binary
365
365
366 return _is_binary(repo_id, tree_id)
366 return _is_binary(repo_id, tree_id)
367
367
368 @reraise_safe_exceptions
368 @reraise_safe_exceptions
369 def md5_hash(self, wire, commit_id, path):
369 def md5_hash(self, wire, commit_id, path):
370 cache_on, context_uid, repo_id = self._cache_on(wire)
370 cache_on, context_uid, repo_id = self._cache_on(wire)
371 region = self._region(wire)
371 region = self._region(wire)
372
372
373 @region.conditional_cache_on_arguments(condition=cache_on)
373 @region.conditional_cache_on_arguments(condition=cache_on)
374 def _md5_hash(_repo_id, _commit_id, _path):
374 def _md5_hash(_repo_id, _commit_id, _path):
375 repo_init = self._factory.repo_libgit2(wire)
375 repo_init = self._factory.repo_libgit2(wire)
376 with repo_init as repo:
376 with repo_init as repo:
377 commit = repo[_commit_id]
377 commit = repo[_commit_id]
378 blob_obj = commit.tree[_path]
378 blob_obj = commit.tree[_path]
379
379
380 if blob_obj.type != pygit2.GIT_OBJ_BLOB:
380 if blob_obj.type != pygit2.GIT_OBJ_BLOB:
381 raise exceptions.LookupException()(
381 raise exceptions.LookupException()(
382 f'Tree for commit_id:{_commit_id} is not a blob: {blob_obj.type_str}')
382 f'Tree for commit_id:{_commit_id} is not a blob: {blob_obj.type_str}')
383
383
384 return ''
384 return ''
385
385
386 return _md5_hash(repo_id, commit_id, path)
386 return _md5_hash(repo_id, commit_id, path)
387
387
388 @reraise_safe_exceptions
388 @reraise_safe_exceptions
389 def in_largefiles_store(self, wire, oid):
389 def in_largefiles_store(self, wire, oid):
390 conf = self._wire_to_config(wire)
390 conf = self._wire_to_config(wire)
391 repo_init = self._factory.repo_libgit2(wire)
391 repo_init = self._factory.repo_libgit2(wire)
392 with repo_init as repo:
392 with repo_init as repo:
393 repo_name = repo.path
393 repo_name = repo.path
394
394
395 store_location = conf.get('vcs_git_lfs_store_location')
395 store_location = conf.get('vcs_git_lfs_store_location')
396 if store_location:
396 if store_location:
397
397
398 store = LFSOidStore(
398 store = LFSOidStore(
399 oid=oid, repo=repo_name, store_location=store_location)
399 oid=oid, repo=repo_name, store_location=store_location)
400 return store.has_oid()
400 return store.has_oid()
401
401
402 return False
402 return False
403
403
404 @reraise_safe_exceptions
404 @reraise_safe_exceptions
405 def store_path(self, wire, oid):
405 def store_path(self, wire, oid):
406 conf = self._wire_to_config(wire)
406 conf = self._wire_to_config(wire)
407 repo_init = self._factory.repo_libgit2(wire)
407 repo_init = self._factory.repo_libgit2(wire)
408 with repo_init as repo:
408 with repo_init as repo:
409 repo_name = repo.path
409 repo_name = repo.path
410
410
411 store_location = conf.get('vcs_git_lfs_store_location')
411 store_location = conf.get('vcs_git_lfs_store_location')
412 if store_location:
412 if store_location:
413 store = LFSOidStore(
413 store = LFSOidStore(
414 oid=oid, repo=repo_name, store_location=store_location)
414 oid=oid, repo=repo_name, store_location=store_location)
415 return store.oid_path
415 return store.oid_path
416 raise ValueError(f'Unable to fetch oid with path {oid}')
416 raise ValueError(f'Unable to fetch oid with path {oid}')
417
417
418 @reraise_safe_exceptions
418 @reraise_safe_exceptions
419 def bulk_request(self, wire, rev, pre_load):
419 def bulk_request(self, wire, rev, pre_load):
420 cache_on, context_uid, repo_id = self._cache_on(wire)
420 cache_on, context_uid, repo_id = self._cache_on(wire)
421 region = self._region(wire)
421 region = self._region(wire)
422
422
423 @region.conditional_cache_on_arguments(condition=cache_on)
423 @region.conditional_cache_on_arguments(condition=cache_on)
424 def _bulk_request(_repo_id, _rev, _pre_load):
424 def _bulk_request(_repo_id, _rev, _pre_load):
425 result = {}
425 result = {}
426 for attr in pre_load:
426 for attr in pre_load:
427 try:
427 try:
428 method = self._bulk_methods[attr]
428 method = self._bulk_methods[attr]
429 wire.update({'cache': False}) # disable cache for bulk calls so we don't double cache
429 wire.update({'cache': False}) # disable cache for bulk calls so we don't double cache
430 args = [wire, rev]
430 args = [wire, rev]
431 result[attr] = method(*args)
431 result[attr] = method(*args)
432 except KeyError as e:
432 except KeyError as e:
433 raise exceptions.VcsException(e)(f"Unknown bulk attribute: {attr}")
433 raise exceptions.VcsException(e)(f"Unknown bulk attribute: {attr}")
434 return result
434 return result
435
435
436 return _bulk_request(repo_id, rev, sorted(pre_load))
436 return _bulk_request(repo_id, rev, sorted(pre_load))
437
437
438 @reraise_safe_exceptions
438 @reraise_safe_exceptions
439 def bulk_file_request(self, wire, commit_id, path, pre_load):
439 def bulk_file_request(self, wire, commit_id, path, pre_load):
440 cache_on, context_uid, repo_id = self._cache_on(wire)
440 cache_on, context_uid, repo_id = self._cache_on(wire)
441 region = self._region(wire)
441 region = self._region(wire)
442
442
443 @region.conditional_cache_on_arguments(condition=cache_on)
443 @region.conditional_cache_on_arguments(condition=cache_on)
444 def _bulk_file_request(_repo_id, _commit_id, _path, _pre_load):
444 def _bulk_file_request(_repo_id, _commit_id, _path, _pre_load):
445 result = {}
445 result = {}
446 for attr in pre_load:
446 for attr in pre_load:
447 try:
447 try:
448 method = self._bulk_file_methods[attr]
448 method = self._bulk_file_methods[attr]
449 wire.update({'cache': False}) # disable cache for bulk calls so we don't double cache
449 wire.update({'cache': False}) # disable cache for bulk calls so we don't double cache
450 result[attr] = method(wire, _commit_id, _path)
450 result[attr] = method(wire, _commit_id, _path)
451 except KeyError as e:
451 except KeyError as e:
452 raise exceptions.VcsException(e)(f'Unknown bulk attribute: "{attr}"')
452 raise exceptions.VcsException(e)(f'Unknown bulk attribute: "{attr}"')
453 return result
453 return result
454
454
455 return BinaryEnvelope(_bulk_file_request(repo_id, commit_id, path, sorted(pre_load)))
455 return BinaryEnvelope(_bulk_file_request(repo_id, commit_id, path, sorted(pre_load)))
456
456
457 def _build_opener(self, url: str):
457 def _build_opener(self, url: str):
458 handlers = []
458 handlers = []
459 url_obj = url_parser(safe_bytes(url))
459 url_obj = url_parser(safe_bytes(url))
460 authinfo = url_obj.authinfo()[1]
460 authinfo = url_obj.authinfo()[1]
461
461
462 if authinfo:
462 if authinfo:
463 # create a password manager
463 # create a password manager
464 passmgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()
464 passmgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()
465 passmgr.add_password(*convert_to_str(authinfo))
465 passmgr.add_password(*convert_to_str(authinfo))
466
466
467 handlers.extend((httpbasicauthhandler(passmgr),
467 handlers.extend((httpbasicauthhandler(passmgr),
468 httpdigestauthhandler(passmgr)))
468 httpdigestauthhandler(passmgr)))
469
469
470 return urllib.request.build_opener(*handlers)
470 return urllib.request.build_opener(*handlers)
471
471
472 @reraise_safe_exceptions
472 @reraise_safe_exceptions
473 def check_url(self, url, config):
473 def check_url(self, url, config):
474 url_obj = url_parser(safe_bytes(url))
474 url_obj = url_parser(safe_bytes(url))
475
475
476 test_uri = safe_str(url_obj.authinfo()[0])
476 test_uri = safe_str(url_obj.authinfo()[0])
477 obfuscated_uri = get_obfuscated_url(url_obj)
477 obfuscated_uri = get_obfuscated_url(url_obj)
478
478
479 log.info("Checking URL for remote cloning/import: %s", obfuscated_uri)
479 log.info("Checking URL for remote cloning/import: %s", obfuscated_uri)
480
480
481 if not test_uri.endswith('info/refs'):
481 if not test_uri.endswith('info/refs'):
482 test_uri = test_uri.rstrip('/') + '/info/refs'
482 test_uri = test_uri.rstrip('/') + '/info/refs'
483
483
484 o = self._build_opener(url=url)
484 o = self._build_opener(url=url)
485 o.addheaders = [('User-Agent', 'git/1.7.8.0')] # fake some git
485 o.addheaders = [('User-Agent', 'git/1.7.8.0')] # fake some git
486
486
487 q = {"service": 'git-upload-pack'}
487 q = {"service": 'git-upload-pack'}
488 qs = f'?{urllib.parse.urlencode(q)}'
488 qs = f'?{urllib.parse.urlencode(q)}'
489 cu = f"{test_uri}{qs}"
489 cu = f"{test_uri}{qs}"
490
490
491 try:
491 try:
492 req = urllib.request.Request(cu, None, {})
492 req = urllib.request.Request(cu, None, {})
493 log.debug("Trying to open URL %s", obfuscated_uri)
493 log.debug("Trying to open URL %s", obfuscated_uri)
494 resp = o.open(req)
494 resp = o.open(req)
495 if resp.code != 200:
495 if resp.code != 200:
496 raise exceptions.URLError()('Return Code is not 200')
496 raise exceptions.URLError()('Return Code is not 200')
497 except Exception as e:
497 except Exception as e:
498 log.warning("URL cannot be opened: %s", obfuscated_uri, exc_info=True)
498 log.warning("URL cannot be opened: %s", obfuscated_uri, exc_info=True)
499 # means it cannot be cloned
499 # means it cannot be cloned
500 raise exceptions.URLError(e)(f"[{obfuscated_uri}] org_exc: {e}")
500 raise exceptions.URLError(e)(f"[{obfuscated_uri}] org_exc: {e}")
501
501
502 # now detect if it's proper git repo
502 # now detect if it's proper git repo
503 gitdata: bytes = resp.read()
503 gitdata: bytes = resp.read()
504
504
505 if b'service=git-upload-pack' in gitdata:
505 if b'service=git-upload-pack' in gitdata:
506 pass
506 pass
507 elif re.findall(br'[0-9a-fA-F]{40}\s+refs', gitdata):
507 elif re.findall(br'[0-9a-fA-F]{40}\s+refs', gitdata):
508 # old style git can return some other format!
508 # old style git can return some other format!
509 pass
509 pass
510 else:
510 else:
511 e = None
511 e = None
512 raise exceptions.URLError(e)(
512 raise exceptions.URLError(e)(
513 f"url [{obfuscated_uri}] does not look like an hg repo org_exc: {e}")
513 f"url [{obfuscated_uri}] does not look like an hg repo org_exc: {e}")
514
514
515 return True
515 return True
516
516
517 @reraise_safe_exceptions
517 @reraise_safe_exceptions
518 def clone(self, wire, url, deferred, valid_refs, update_after_clone):
518 def clone(self, wire, url, deferred, valid_refs, update_after_clone):
519 # TODO(marcink): deprecate this method. Last i checked we don't use it anymore
519 # TODO(marcink): deprecate this method. Last i checked we don't use it anymore
520 remote_refs = self.pull(wire, url, apply_refs=False)
520 remote_refs = self.pull(wire, url, apply_refs=False)
521 repo = self._factory.repo(wire)
521 repo = self._factory.repo(wire)
522 if isinstance(valid_refs, list):
522 if isinstance(valid_refs, list):
523 valid_refs = tuple(valid_refs)
523 valid_refs = tuple(valid_refs)
524
524
525 for k in remote_refs:
525 for k in remote_refs:
526 # only parse heads/tags and skip so called deferred tags
526 # only parse heads/tags and skip so called deferred tags
527 if k.startswith(valid_refs) and not k.endswith(deferred):
527 if k.startswith(valid_refs) and not k.endswith(deferred):
528 repo[k] = remote_refs[k]
528 repo[k] = remote_refs[k]
529
529
530 if update_after_clone:
530 if update_after_clone:
531 # we want to checkout HEAD
531 # we want to checkout HEAD
532 repo["HEAD"] = remote_refs["HEAD"]
532 repo["HEAD"] = remote_refs["HEAD"]
533 index.build_index_from_tree(repo.path, repo.index_path(),
533 index.build_index_from_tree(repo.path, repo.index_path(),
534 repo.object_store, repo["HEAD"].tree)
534 repo.object_store, repo["HEAD"].tree)
535
535
536 @reraise_safe_exceptions
536 @reraise_safe_exceptions
537 def branch(self, wire, commit_id):
537 def branch(self, wire, commit_id):
538 cache_on, context_uid, repo_id = self._cache_on(wire)
538 cache_on, context_uid, repo_id = self._cache_on(wire)
539 region = self._region(wire)
539 region = self._region(wire)
540
540
541 @region.conditional_cache_on_arguments(condition=cache_on)
541 @region.conditional_cache_on_arguments(condition=cache_on)
542 def _branch(_context_uid, _repo_id, _commit_id):
542 def _branch(_context_uid, _repo_id, _commit_id):
543 regex = re.compile('^refs/heads')
543 regex = re.compile('^refs/heads')
544
544
545 def filter_with(ref):
545 def filter_with(ref):
546 return regex.match(ref[0]) and ref[1] == _commit_id
546 return regex.match(ref[0]) and ref[1] == _commit_id
547
547
548 branches = list(filter(filter_with, list(self.get_refs(wire).items())))
548 branches = list(filter(filter_with, list(self.get_refs(wire).items())))
549 return [x[0].split('refs/heads/')[-1] for x in branches]
549 return [x[0].split('refs/heads/')[-1] for x in branches]
550
550
551 return _branch(context_uid, repo_id, commit_id)
551 return _branch(context_uid, repo_id, commit_id)
552
552
553 @reraise_safe_exceptions
553 @reraise_safe_exceptions
554 def delete_branch(self, wire, branch_name):
555 repo_init = self._factory.repo_libgit2(wire)
556 with repo_init as repo:
557 if branch := repo.lookup_branch(branch_name):
558 branch.delete()
559
560 @reraise_safe_exceptions
554 def commit_branches(self, wire, commit_id):
561 def commit_branches(self, wire, commit_id):
555 cache_on, context_uid, repo_id = self._cache_on(wire)
562 cache_on, context_uid, repo_id = self._cache_on(wire)
556 region = self._region(wire)
563 region = self._region(wire)
557
564
558 @region.conditional_cache_on_arguments(condition=cache_on)
565 @region.conditional_cache_on_arguments(condition=cache_on)
559 def _commit_branches(_context_uid, _repo_id, _commit_id):
566 def _commit_branches(_context_uid, _repo_id, _commit_id):
560 repo_init = self._factory.repo_libgit2(wire)
567 repo_init = self._factory.repo_libgit2(wire)
561 with repo_init as repo:
568 with repo_init as repo:
562 branches = [x for x in repo.branches.with_commit(_commit_id)]
569 branches = [x for x in repo.branches.with_commit(_commit_id)]
563 return branches
570 return branches
564
571
565 return _commit_branches(context_uid, repo_id, commit_id)
572 return _commit_branches(context_uid, repo_id, commit_id)
566
573
567 @reraise_safe_exceptions
574 @reraise_safe_exceptions
568 def add_object(self, wire, content):
575 def add_object(self, wire, content):
569 repo_init = self._factory.repo_libgit2(wire)
576 repo_init = self._factory.repo_libgit2(wire)
570 with repo_init as repo:
577 with repo_init as repo:
571 blob = objects.Blob()
578 blob = objects.Blob()
572 blob.set_raw_string(content)
579 blob.set_raw_string(content)
573 repo.object_store.add_object(blob)
580 repo.object_store.add_object(blob)
574 return blob.id
581 return blob.id
575
582
576 @reraise_safe_exceptions
583 @reraise_safe_exceptions
577 def create_commit(self, wire, author, committer, message, branch, new_tree_id,
584 def create_commit(self, wire, author, committer, message, branch, new_tree_id,
578 date_args: list[int, int] = None,
585 date_args: list[int, int] = None,
579 parents: list | None = None):
586 parents: list | None = None):
580
587
581 repo_init = self._factory.repo_libgit2(wire)
588 repo_init = self._factory.repo_libgit2(wire)
582 with repo_init as repo:
589 with repo_init as repo:
583
590
584 if date_args:
591 if date_args:
585 current_time, offset = date_args
592 current_time, offset = date_args
586
593
587 kw = {
594 kw = {
588 'time': current_time,
595 'time': current_time,
589 'offset': offset
596 'offset': offset
590 }
597 }
591 author = create_signature_from_string(author, **kw)
598 author = create_signature_from_string(author, **kw)
592 committer = create_signature_from_string(committer, **kw)
599 committer = create_signature_from_string(committer, **kw)
593
600
594 tree = new_tree_id
601 tree = new_tree_id
595 if isinstance(tree, (bytes, str)):
602 if isinstance(tree, (bytes, str)):
596 # validate this tree is in the repo...
603 # validate this tree is in the repo...
597 tree = repo[safe_str(tree)].id
604 tree = repo[safe_str(tree)].id
598
605
599 if parents:
606 if parents:
600 # run via sha's and validate them in repo
607 # run via sha's and validate them in repo
601 parents = [repo[c].id for c in parents]
608 parents = [repo[c].id for c in parents]
602 else:
609 else:
603 parents = []
610 parents = []
604 # ensure we COMMIT on top of given branch head
611 # ensure we COMMIT on top of given branch head
605 # check if this repo has ANY branches, otherwise it's a new branch case we need to make
612 # check if this repo has ANY branches, otherwise it's a new branch case we need to make
606 if branch in repo.branches.local:
613 if branch in repo.branches.local:
607 parents += [repo.branches[branch].target]
614 parents += [repo.branches[branch].target]
608 elif [x for x in repo.branches.local]:
615 elif [x for x in repo.branches.local]:
609 parents += [repo.head.target]
616 parents += [repo.head.target]
610 #else:
617 #else:
611 # in case we want to commit on new branch we create it on top of HEAD
618 # in case we want to commit on new branch we create it on top of HEAD
612 #repo.branches.local.create(branch, repo.revparse_single('HEAD'))
619 #repo.branches.local.create(branch, repo.revparse_single('HEAD'))
613
620
614 # # Create a new commit
621 # # Create a new commit
615 commit_oid = repo.create_commit(
622 commit_oid = repo.create_commit(
616 f'refs/heads/{branch}', # the name of the reference to update
623 f'refs/heads/{branch}', # the name of the reference to update
617 author, # the author of the commit
624 author, # the author of the commit
618 committer, # the committer of the commit
625 committer, # the committer of the commit
619 message, # the commit message
626 message, # the commit message
620 tree, # the tree produced by the index
627 tree, # the tree produced by the index
621 parents # list of parents for the new commit, usually just one,
628 parents # list of parents for the new commit, usually just one,
622 )
629 )
623
630
624 new_commit_id = safe_str(commit_oid)
631 new_commit_id = safe_str(commit_oid)
625
632
626 return new_commit_id
633 return new_commit_id
627
634
628 @reraise_safe_exceptions
635 @reraise_safe_exceptions
629 def commit(self, wire, commit_data, branch, commit_tree, updated, removed):
636 def commit(self, wire, commit_data, branch, commit_tree, updated, removed):
630
637
631 def mode2pygit(mode):
638 def mode2pygit(mode):
632 """
639 """
633 git only supports two filemode 644 and 755
640 git only supports two filemode 644 and 755
634
641
635 0o100755 -> 33261
642 0o100755 -> 33261
636 0o100644 -> 33188
643 0o100644 -> 33188
637 """
644 """
638 return {
645 return {
639 0o100644: pygit2.GIT_FILEMODE_BLOB,
646 0o100644: pygit2.GIT_FILEMODE_BLOB,
640 0o100755: pygit2.GIT_FILEMODE_BLOB_EXECUTABLE,
647 0o100755: pygit2.GIT_FILEMODE_BLOB_EXECUTABLE,
641 0o120000: pygit2.GIT_FILEMODE_LINK
648 0o120000: pygit2.GIT_FILEMODE_LINK
642 }.get(mode) or pygit2.GIT_FILEMODE_BLOB
649 }.get(mode) or pygit2.GIT_FILEMODE_BLOB
643
650
644 repo_init = self._factory.repo_libgit2(wire)
651 repo_init = self._factory.repo_libgit2(wire)
645 with repo_init as repo:
652 with repo_init as repo:
646 repo_index = repo.index
653 repo_index = repo.index
647
654
648 commit_parents = None
655 commit_parents = None
649 if commit_tree and commit_data['parents']:
656 if commit_tree and commit_data['parents']:
650 commit_parents = commit_data['parents']
657 commit_parents = commit_data['parents']
651 parent_commit = repo[commit_parents[0]]
658 parent_commit = repo[commit_parents[0]]
652 repo_index.read_tree(parent_commit.tree)
659 repo_index.read_tree(parent_commit.tree)
653
660
654 for pathspec in updated:
661 for pathspec in updated:
655 blob_id = repo.create_blob(pathspec['content'])
662 blob_id = repo.create_blob(pathspec['content'])
656 ie = pygit2.IndexEntry(pathspec['path'], blob_id, mode2pygit(pathspec['mode']))
663 ie = pygit2.IndexEntry(pathspec['path'], blob_id, mode2pygit(pathspec['mode']))
657 repo_index.add(ie)
664 repo_index.add(ie)
658
665
659 for pathspec in removed:
666 for pathspec in removed:
660 repo_index.remove(pathspec)
667 repo_index.remove(pathspec)
661
668
662 # Write changes to the index
669 # Write changes to the index
663 repo_index.write()
670 repo_index.write()
664
671
665 # Create a tree from the updated index
672 # Create a tree from the updated index
666 written_commit_tree = repo_index.write_tree()
673 written_commit_tree = repo_index.write_tree()
667
674
668 new_tree_id = written_commit_tree
675 new_tree_id = written_commit_tree
669
676
670 author = commit_data['author']
677 author = commit_data['author']
671 committer = commit_data['committer']
678 committer = commit_data['committer']
672 message = commit_data['message']
679 message = commit_data['message']
673
680
674 date_args = [int(commit_data['commit_time']), int(commit_data['commit_timezone'])]
681 date_args = [int(commit_data['commit_time']), int(commit_data['commit_timezone'])]
675
682
676 new_commit_id = self.create_commit(wire, author, committer, message, branch,
683 new_commit_id = self.create_commit(wire, author, committer, message, branch,
677 new_tree_id, date_args=date_args, parents=commit_parents)
684 new_tree_id, date_args=date_args, parents=commit_parents)
678
685
679 # libgit2, ensure the branch is there and exists
686 # libgit2, ensure the branch is there and exists
680 self.create_branch(wire, branch, new_commit_id)
687 self.create_branch(wire, branch, new_commit_id)
681
688
682 # libgit2, set new ref to this created commit
689 # libgit2, set new ref to this created commit
683 self.set_refs(wire, f'refs/heads/{branch}', new_commit_id)
690 self.set_refs(wire, f'refs/heads/{branch}', new_commit_id)
684
691
685 return new_commit_id
692 return new_commit_id
686
693
687 @reraise_safe_exceptions
694 @reraise_safe_exceptions
688 def pull(self, wire, url, apply_refs=True, refs=None, update_after=False):
695 def pull(self, wire, url, apply_refs=True, refs=None, update_after=False):
689 if url != 'default' and '://' not in url:
696 if url != 'default' and '://' not in url:
690 client = LocalGitClient(url)
697 client = LocalGitClient(url)
691 else:
698 else:
692 url_obj = url_parser(safe_bytes(url))
699 url_obj = url_parser(safe_bytes(url))
693 o = self._build_opener(url)
700 o = self._build_opener(url)
694 url = url_obj.authinfo()[0]
701 url = url_obj.authinfo()[0]
695 client = HttpGitClient(base_url=url, opener=o)
702 client = HttpGitClient(base_url=url, opener=o)
696 repo = self._factory.repo(wire)
703 repo = self._factory.repo(wire)
697
704
698 determine_wants = repo.object_store.determine_wants_all
705 determine_wants = repo.object_store.determine_wants_all
699
706
700 if refs:
707 if refs:
701 refs: list[bytes] = [ascii_bytes(x) for x in refs]
708 refs: list[bytes] = [ascii_bytes(x) for x in refs]
702
709
703 def determine_wants_requested(_remote_refs):
710 def determine_wants_requested(_remote_refs):
704 determined = []
711 determined = []
705 for ref_name, ref_hash in _remote_refs.items():
712 for ref_name, ref_hash in _remote_refs.items():
706 bytes_ref_name = safe_bytes(ref_name)
713 bytes_ref_name = safe_bytes(ref_name)
707
714
708 if bytes_ref_name in refs:
715 if bytes_ref_name in refs:
709 bytes_ref_hash = safe_bytes(ref_hash)
716 bytes_ref_hash = safe_bytes(ref_hash)
710 determined.append(bytes_ref_hash)
717 determined.append(bytes_ref_hash)
711 return determined
718 return determined
712
719
713 # swap with our custom requested wants
720 # swap with our custom requested wants
714 determine_wants = determine_wants_requested
721 determine_wants = determine_wants_requested
715
722
716 try:
723 try:
717 remote_refs = client.fetch(
724 remote_refs = client.fetch(
718 path=url, target=repo, determine_wants=determine_wants)
725 path=url, target=repo, determine_wants=determine_wants)
719
726
720 except NotGitRepository as e:
727 except NotGitRepository as e:
721 log.warning(
728 log.warning(
722 'Trying to fetch from "%s" failed, not a Git repository.', url)
729 'Trying to fetch from "%s" failed, not a Git repository.', url)
723 # Exception can contain unicode which we convert
730 # Exception can contain unicode which we convert
724 raise exceptions.AbortException(e)(repr(e))
731 raise exceptions.AbortException(e)(repr(e))
725
732
726 # mikhail: client.fetch() returns all the remote refs, but fetches only
733 # mikhail: client.fetch() returns all the remote refs, but fetches only
727 # refs filtered by `determine_wants` function. We need to filter result
734 # refs filtered by `determine_wants` function. We need to filter result
728 # as well
735 # as well
729 if refs:
736 if refs:
730 remote_refs = {k: remote_refs[k] for k in remote_refs if k in refs}
737 remote_refs = {k: remote_refs[k] for k in remote_refs if k in refs}
731
738
732 if apply_refs:
739 if apply_refs:
733 # TODO: johbo: Needs proper test coverage with a git repository
740 # TODO: johbo: Needs proper test coverage with a git repository
734 # that contains a tag object, so that we would end up with
741 # that contains a tag object, so that we would end up with
735 # a peeled ref at this point.
742 # a peeled ref at this point.
736 for k in remote_refs:
743 for k in remote_refs:
737 if k.endswith(PEELED_REF_MARKER):
744 if k.endswith(PEELED_REF_MARKER):
738 log.debug("Skipping peeled reference %s", k)
745 log.debug("Skipping peeled reference %s", k)
739 continue
746 continue
740 repo[k] = remote_refs[k]
747 repo[k] = remote_refs[k]
741
748
742 if refs and not update_after:
749 if refs and not update_after:
743 # update to ref
750 # update to ref
744 # mikhail: explicitly set the head to the last ref.
751 # mikhail: explicitly set the head to the last ref.
745 update_to_ref = refs[-1]
752 update_to_ref = refs[-1]
746 if isinstance(update_after, str):
753 if isinstance(update_after, str):
747 update_to_ref = update_after
754 update_to_ref = update_after
748
755
749 repo[HEAD_MARKER] = remote_refs[update_to_ref]
756 repo[HEAD_MARKER] = remote_refs[update_to_ref]
750
757
751 if update_after:
758 if update_after:
752 # we want to check out HEAD
759 # we want to check out HEAD
753 repo[HEAD_MARKER] = remote_refs[HEAD_MARKER]
760 repo[HEAD_MARKER] = remote_refs[HEAD_MARKER]
754 index.build_index_from_tree(repo.path, repo.index_path(),
761 index.build_index_from_tree(repo.path, repo.index_path(),
755 repo.object_store, repo[HEAD_MARKER].tree)
762 repo.object_store, repo[HEAD_MARKER].tree)
756
763
757 if isinstance(remote_refs, FetchPackResult):
764 if isinstance(remote_refs, FetchPackResult):
758 return remote_refs.refs
765 return remote_refs.refs
759 return remote_refs
766 return remote_refs
760
767
761 @reraise_safe_exceptions
768 @reraise_safe_exceptions
762 def sync_fetch(self, wire, url, refs=None, all_refs=False, **kwargs):
769 def sync_fetch(self, wire, url, refs=None, all_refs=False, **kwargs):
763 self._factory.repo(wire)
770 self._factory.repo(wire)
764 if refs and not isinstance(refs, (list, tuple)):
771 if refs and not isinstance(refs, (list, tuple)):
765 refs = [refs]
772 refs = [refs]
766
773
767 config = self._wire_to_config(wire)
774 config = self._wire_to_config(wire)
768 # get all remote refs we'll use to fetch later
775 # get all remote refs we'll use to fetch later
769 cmd = ['ls-remote']
776 cmd = ['ls-remote']
770 if not all_refs:
777 if not all_refs:
771 cmd += ['--heads', '--tags']
778 cmd += ['--heads', '--tags']
772 cmd += [url]
779 cmd += [url]
773 output, __ = self.run_git_command(
780 output, __ = self.run_git_command(
774 wire, cmd, fail_on_stderr=False,
781 wire, cmd, fail_on_stderr=False,
775 _copts=self._remote_conf(config),
782 _copts=self._remote_conf(config),
776 extra_env={'GIT_TERMINAL_PROMPT': '0'})
783 extra_env={'GIT_TERMINAL_PROMPT': '0'})
777
784
778 remote_refs = collections.OrderedDict()
785 remote_refs = collections.OrderedDict()
779 fetch_refs = []
786 fetch_refs = []
780
787
781 for ref_line in output.splitlines():
788 for ref_line in output.splitlines():
782 sha, ref = ref_line.split(b'\t')
789 sha, ref = ref_line.split(b'\t')
783 sha = sha.strip()
790 sha = sha.strip()
784 if ref in remote_refs:
791 if ref in remote_refs:
785 # duplicate, skip
792 # duplicate, skip
786 continue
793 continue
787 if ref.endswith(PEELED_REF_MARKER):
794 if ref.endswith(PEELED_REF_MARKER):
788 log.debug("Skipping peeled reference %s", ref)
795 log.debug("Skipping peeled reference %s", ref)
789 continue
796 continue
790 # don't sync HEAD
797 # don't sync HEAD
791 if ref in [HEAD_MARKER]:
798 if ref in [HEAD_MARKER]:
792 continue
799 continue
793
800
794 remote_refs[ref] = sha
801 remote_refs[ref] = sha
795
802
796 if refs and sha in refs:
803 if refs and sha in refs:
797 # we filter fetch using our specified refs
804 # we filter fetch using our specified refs
798 fetch_refs.append(f'{safe_str(ref)}:{safe_str(ref)}')
805 fetch_refs.append(f'{safe_str(ref)}:{safe_str(ref)}')
799 elif not refs:
806 elif not refs:
800 fetch_refs.append(f'{safe_str(ref)}:{safe_str(ref)}')
807 fetch_refs.append(f'{safe_str(ref)}:{safe_str(ref)}')
801 log.debug('Finished obtaining fetch refs, total: %s', len(fetch_refs))
808 log.debug('Finished obtaining fetch refs, total: %s', len(fetch_refs))
802
809
803 if fetch_refs:
810 if fetch_refs:
804 for chunk in more_itertools.chunked(fetch_refs, 128):
811 for chunk in more_itertools.chunked(fetch_refs, 128):
805 fetch_refs_chunks = list(chunk)
812 fetch_refs_chunks = list(chunk)
806 log.debug('Fetching %s refs from import url', len(fetch_refs_chunks))
813 log.debug('Fetching %s refs from import url', len(fetch_refs_chunks))
807 self.run_git_command(
814 self.run_git_command(
808 wire, ['fetch', url, '--force', '--prune', '--'] + fetch_refs_chunks,
815 wire, ['fetch', url, '--force', '--prune', '--'] + fetch_refs_chunks,
809 fail_on_stderr=False,
816 fail_on_stderr=False,
810 _copts=self._remote_conf(config),
817 _copts=self._remote_conf(config),
811 extra_env={'GIT_TERMINAL_PROMPT': '0'})
818 extra_env={'GIT_TERMINAL_PROMPT': '0'})
812 if kwargs.get('sync_large_objects'):
819 if kwargs.get('sync_large_objects'):
813 self.run_git_command(
820 self.run_git_command(
814 wire, ['lfs', 'fetch', url, '--all'],
821 wire, ['lfs', 'fetch', url, '--all'],
815 fail_on_stderr=False,
822 fail_on_stderr=False,
816 _copts=self._remote_conf(config),
823 _copts=self._remote_conf(config),
817 )
824 )
818
825
819 return remote_refs
826 return remote_refs
820
827
821 @reraise_safe_exceptions
828 @reraise_safe_exceptions
822 def sync_push(self, wire, url, refs=None, **kwargs):
829 def sync_push(self, wire, url, refs=None, **kwargs):
823 if not self.check_url(url, wire):
830 if not self.check_url(url, wire):
824 return
831 return
825 config = self._wire_to_config(wire)
832 config = self._wire_to_config(wire)
826 self._factory.repo(wire)
833 self._factory.repo(wire)
827 self.run_git_command(
834 self.run_git_command(
828 wire, ['push', url, '--mirror'], fail_on_stderr=False,
835 wire, ['push', url, '--mirror'], fail_on_stderr=False,
829 _copts=self._remote_conf(config),
836 _copts=self._remote_conf(config),
830 extra_env={'GIT_TERMINAL_PROMPT': '0'})
837 extra_env={'GIT_TERMINAL_PROMPT': '0'})
831 if kwargs.get('sync_large_objects'):
838 if kwargs.get('sync_large_objects'):
832 self.run_git_command(
839 self.run_git_command(
833 wire, ['lfs', 'push', url, '--all'],
840 wire, ['lfs', 'push', url, '--all'],
834 fail_on_stderr=False,
841 fail_on_stderr=False,
835 _copts=self._remote_conf(config),
842 _copts=self._remote_conf(config),
836 )
843 )
837
844
838 @reraise_safe_exceptions
845 @reraise_safe_exceptions
839 def get_remote_refs(self, wire, url):
846 def get_remote_refs(self, wire, url):
840 repo = Repo(url)
847 repo = Repo(url)
841 return repo.get_refs()
848 return repo.get_refs()
842
849
843 @reraise_safe_exceptions
850 @reraise_safe_exceptions
844 def get_description(self, wire):
851 def get_description(self, wire):
845 repo = self._factory.repo(wire)
852 repo = self._factory.repo(wire)
846 return repo.get_description()
853 return repo.get_description()
847
854
848 @reraise_safe_exceptions
855 @reraise_safe_exceptions
849 def get_missing_revs(self, wire, rev1, rev2, other_repo_path):
856 def get_missing_revs(self, wire, rev1, rev2, other_repo_path):
850 origin_repo_path = wire['path']
857 origin_repo_path = wire['path']
851 repo = self._factory.repo(wire)
858 repo = self._factory.repo(wire)
852 # fetch from other_repo_path to our origin repo
859 # fetch from other_repo_path to our origin repo
853 LocalGitClient(thin_packs=False).fetch(other_repo_path, repo)
860 LocalGitClient(thin_packs=False).fetch(other_repo_path, repo)
854
861
855 wire_remote = wire.copy()
862 wire_remote = wire.copy()
856 wire_remote['path'] = other_repo_path
863 wire_remote['path'] = other_repo_path
857 repo_remote = self._factory.repo(wire_remote)
864 repo_remote = self._factory.repo(wire_remote)
858
865
859 # fetch from origin_repo_path to our remote repo
866 # fetch from origin_repo_path to our remote repo
860 LocalGitClient(thin_packs=False).fetch(origin_repo_path, repo_remote)
867 LocalGitClient(thin_packs=False).fetch(origin_repo_path, repo_remote)
861
868
862 revs = [
869 revs = [
863 x.commit.id
870 x.commit.id
864 for x in repo_remote.get_walker(include=[safe_bytes(rev2)], exclude=[safe_bytes(rev1)])]
871 for x in repo_remote.get_walker(include=[safe_bytes(rev2)], exclude=[safe_bytes(rev1)])]
865 return revs
872 return revs
866
873
867 @reraise_safe_exceptions
874 @reraise_safe_exceptions
868 def get_object(self, wire, sha, maybe_unreachable=False):
875 def get_object(self, wire, sha, maybe_unreachable=False):
869 cache_on, context_uid, repo_id = self._cache_on(wire)
876 cache_on, context_uid, repo_id = self._cache_on(wire)
870 region = self._region(wire)
877 region = self._region(wire)
871
878
872 @region.conditional_cache_on_arguments(condition=cache_on)
879 @region.conditional_cache_on_arguments(condition=cache_on)
873 def _get_object(_context_uid, _repo_id, _sha):
880 def _get_object(_context_uid, _repo_id, _sha):
874 repo_init = self._factory.repo_libgit2(wire)
881 repo_init = self._factory.repo_libgit2(wire)
875 with repo_init as repo:
882 with repo_init as repo:
876
883
877 missing_commit_err = 'Commit {} does not exist for `{}`'.format(sha, wire['path'])
884 missing_commit_err = 'Commit {} does not exist for `{}`'.format(sha, wire['path'])
878 try:
885 try:
879 commit = repo.revparse_single(sha)
886 commit = repo.revparse_single(sha)
880 except KeyError:
887 except KeyError:
881 # NOTE(marcink): KeyError doesn't give us any meaningful information
888 # NOTE(marcink): KeyError doesn't give us any meaningful information
882 # here, we instead give something more explicit
889 # here, we instead give something more explicit
883 e = exceptions.RefNotFoundException('SHA: %s not found', sha)
890 e = exceptions.RefNotFoundException('SHA: %s not found', sha)
884 raise exceptions.LookupException(e)(missing_commit_err)
891 raise exceptions.LookupException(e)(missing_commit_err)
885 except ValueError as e:
892 except ValueError as e:
886 raise exceptions.LookupException(e)(missing_commit_err)
893 raise exceptions.LookupException(e)(missing_commit_err)
887
894
888 is_tag = False
895 is_tag = False
889 if isinstance(commit, pygit2.Tag):
896 if isinstance(commit, pygit2.Tag):
890 commit = repo.get(commit.target)
897 commit = repo.get(commit.target)
891 is_tag = True
898 is_tag = True
892
899
893 check_dangling = True
900 check_dangling = True
894 if is_tag:
901 if is_tag:
895 check_dangling = False
902 check_dangling = False
896
903
897 if check_dangling and maybe_unreachable:
904 if check_dangling and maybe_unreachable:
898 check_dangling = False
905 check_dangling = False
899
906
900 # we used a reference and it parsed means we're not having a dangling commit
907 # we used a reference and it parsed means we're not having a dangling commit
901 if sha != commit.hex:
908 if sha != commit.hex:
902 check_dangling = False
909 check_dangling = False
903
910
904 if check_dangling:
911 if check_dangling:
905 # check for dangling commit
912 # check for dangling commit
906 for branch in repo.branches.with_commit(commit.hex):
913 for branch in repo.branches.with_commit(commit.hex):
907 if branch:
914 if branch:
908 break
915 break
909 else:
916 else:
910 # NOTE(marcink): Empty error doesn't give us any meaningful information
917 # NOTE(marcink): Empty error doesn't give us any meaningful information
911 # here, we instead give something more explicit
918 # here, we instead give something more explicit
912 e = exceptions.RefNotFoundException('SHA: %s not found in branches', sha)
919 e = exceptions.RefNotFoundException('SHA: %s not found in branches', sha)
913 raise exceptions.LookupException(e)(missing_commit_err)
920 raise exceptions.LookupException(e)(missing_commit_err)
914
921
915 commit_id = commit.hex
922 commit_id = commit.hex
916 type_str = commit.type_str
923 type_str = commit.type_str
917
924
918 return {
925 return {
919 'id': commit_id,
926 'id': commit_id,
920 'type': type_str,
927 'type': type_str,
921 'commit_id': commit_id,
928 'commit_id': commit_id,
922 'idx': 0
929 'idx': 0
923 }
930 }
924
931
925 return _get_object(context_uid, repo_id, sha)
932 return _get_object(context_uid, repo_id, sha)
926
933
927 @reraise_safe_exceptions
934 @reraise_safe_exceptions
928 def get_refs(self, wire):
935 def get_refs(self, wire):
929 cache_on, context_uid, repo_id = self._cache_on(wire)
936 cache_on, context_uid, repo_id = self._cache_on(wire)
930 region = self._region(wire)
937 region = self._region(wire)
931
938
932 @region.conditional_cache_on_arguments(condition=cache_on)
939 @region.conditional_cache_on_arguments(condition=cache_on)
933 def _get_refs(_context_uid, _repo_id):
940 def _get_refs(_context_uid, _repo_id):
934
941
935 repo_init = self._factory.repo_libgit2(wire)
942 repo_init = self._factory.repo_libgit2(wire)
936 with repo_init as repo:
943 with repo_init as repo:
937 regex = re.compile('^refs/(heads|tags)/')
944 regex = re.compile('^refs/(heads|tags)/')
938 return {x.name: x.target.hex for x in
945 return {x.name: x.target.hex for x in
939 [ref for ref in repo.listall_reference_objects() if regex.match(ref.name)]}
946 [ref for ref in repo.listall_reference_objects() if regex.match(ref.name)]}
940
947
941 return _get_refs(context_uid, repo_id)
948 return _get_refs(context_uid, repo_id)
942
949
943 @reraise_safe_exceptions
950 @reraise_safe_exceptions
944 def get_branch_pointers(self, wire):
951 def get_branch_pointers(self, wire):
945 cache_on, context_uid, repo_id = self._cache_on(wire)
952 cache_on, context_uid, repo_id = self._cache_on(wire)
946 region = self._region(wire)
953 region = self._region(wire)
947
954
948 @region.conditional_cache_on_arguments(condition=cache_on)
955 @region.conditional_cache_on_arguments(condition=cache_on)
949 def _get_branch_pointers(_context_uid, _repo_id):
956 def _get_branch_pointers(_context_uid, _repo_id):
950
957
951 repo_init = self._factory.repo_libgit2(wire)
958 repo_init = self._factory.repo_libgit2(wire)
952 regex = re.compile('^refs/heads')
959 regex = re.compile('^refs/heads')
953 with repo_init as repo:
960 with repo_init as repo:
954 branches = [ref for ref in repo.listall_reference_objects() if regex.match(ref.name)]
961 branches = [ref for ref in repo.listall_reference_objects() if regex.match(ref.name)]
955 return {x.target.hex: x.shorthand for x in branches}
962 return {x.target.hex: x.shorthand for x in branches}
956
963
957 return _get_branch_pointers(context_uid, repo_id)
964 return _get_branch_pointers(context_uid, repo_id)
958
965
959 @reraise_safe_exceptions
966 @reraise_safe_exceptions
960 def head(self, wire, show_exc=True):
967 def head(self, wire, show_exc=True):
961 cache_on, context_uid, repo_id = self._cache_on(wire)
968 cache_on, context_uid, repo_id = self._cache_on(wire)
962 region = self._region(wire)
969 region = self._region(wire)
963
970
964 @region.conditional_cache_on_arguments(condition=cache_on)
971 @region.conditional_cache_on_arguments(condition=cache_on)
965 def _head(_context_uid, _repo_id, _show_exc):
972 def _head(_context_uid, _repo_id, _show_exc):
966 repo_init = self._factory.repo_libgit2(wire)
973 repo_init = self._factory.repo_libgit2(wire)
967 with repo_init as repo:
974 with repo_init as repo:
968 try:
975 try:
969 return repo.head.peel().hex
976 return repo.head.peel().hex
970 except Exception:
977 except Exception:
971 if show_exc:
978 if show_exc:
972 raise
979 raise
973 return _head(context_uid, repo_id, show_exc)
980 return _head(context_uid, repo_id, show_exc)
974
981
975 @reraise_safe_exceptions
982 @reraise_safe_exceptions
976 def init(self, wire):
983 def init(self, wire):
977 repo_path = safe_str(wire['path'])
984 repo_path = safe_str(wire['path'])
978 os.makedirs(repo_path, mode=0o755)
985 os.makedirs(repo_path, mode=0o755)
979 pygit2.init_repository(repo_path, bare=False)
986 pygit2.init_repository(repo_path, bare=False)
980
987
981 @reraise_safe_exceptions
988 @reraise_safe_exceptions
982 def init_bare(self, wire):
989 def init_bare(self, wire):
983 repo_path = safe_str(wire['path'])
990 repo_path = safe_str(wire['path'])
984 os.makedirs(repo_path, mode=0o755)
991 os.makedirs(repo_path, mode=0o755)
985 pygit2.init_repository(repo_path, bare=True)
992 pygit2.init_repository(repo_path, bare=True)
986
993
987 @reraise_safe_exceptions
994 @reraise_safe_exceptions
988 def revision(self, wire, rev):
995 def revision(self, wire, rev):
989
996
990 cache_on, context_uid, repo_id = self._cache_on(wire)
997 cache_on, context_uid, repo_id = self._cache_on(wire)
991 region = self._region(wire)
998 region = self._region(wire)
992
999
993 @region.conditional_cache_on_arguments(condition=cache_on)
1000 @region.conditional_cache_on_arguments(condition=cache_on)
994 def _revision(_context_uid, _repo_id, _rev):
1001 def _revision(_context_uid, _repo_id, _rev):
995 repo_init = self._factory.repo_libgit2(wire)
1002 repo_init = self._factory.repo_libgit2(wire)
996 with repo_init as repo:
1003 with repo_init as repo:
997 commit = repo[rev]
1004 commit = repo[rev]
998 obj_data = {
1005 obj_data = {
999 'id': commit.id.hex,
1006 'id': commit.id.hex,
1000 }
1007 }
1001 # tree objects itself don't have tree_id attribute
1008 # tree objects itself don't have tree_id attribute
1002 if hasattr(commit, 'tree_id'):
1009 if hasattr(commit, 'tree_id'):
1003 obj_data['tree'] = commit.tree_id.hex
1010 obj_data['tree'] = commit.tree_id.hex
1004
1011
1005 return obj_data
1012 return obj_data
1006 return _revision(context_uid, repo_id, rev)
1013 return _revision(context_uid, repo_id, rev)
1007
1014
1008 @reraise_safe_exceptions
1015 @reraise_safe_exceptions
1009 def date(self, wire, commit_id):
1016 def date(self, wire, commit_id):
1010 cache_on, context_uid, repo_id = self._cache_on(wire)
1017 cache_on, context_uid, repo_id = self._cache_on(wire)
1011 region = self._region(wire)
1018 region = self._region(wire)
1012
1019
1013 @region.conditional_cache_on_arguments(condition=cache_on)
1020 @region.conditional_cache_on_arguments(condition=cache_on)
1014 def _date(_repo_id, _commit_id):
1021 def _date(_repo_id, _commit_id):
1015 repo_init = self._factory.repo_libgit2(wire)
1022 repo_init = self._factory.repo_libgit2(wire)
1016 with repo_init as repo:
1023 with repo_init as repo:
1017 commit = repo[commit_id]
1024 commit = repo[commit_id]
1018
1025
1019 if hasattr(commit, 'commit_time'):
1026 if hasattr(commit, 'commit_time'):
1020 commit_time, commit_time_offset = commit.commit_time, commit.commit_time_offset
1027 commit_time, commit_time_offset = commit.commit_time, commit.commit_time_offset
1021 else:
1028 else:
1022 commit = commit.get_object()
1029 commit = commit.get_object()
1023 commit_time, commit_time_offset = commit.commit_time, commit.commit_time_offset
1030 commit_time, commit_time_offset = commit.commit_time, commit.commit_time_offset
1024
1031
1025 # TODO(marcink): check dulwich difference of offset vs timezone
1032 # TODO(marcink): check dulwich difference of offset vs timezone
1026 return [commit_time, commit_time_offset]
1033 return [commit_time, commit_time_offset]
1027 return _date(repo_id, commit_id)
1034 return _date(repo_id, commit_id)
1028
1035
1029 @reraise_safe_exceptions
1036 @reraise_safe_exceptions
1030 def author(self, wire, commit_id):
1037 def author(self, wire, commit_id):
1031 cache_on, context_uid, repo_id = self._cache_on(wire)
1038 cache_on, context_uid, repo_id = self._cache_on(wire)
1032 region = self._region(wire)
1039 region = self._region(wire)
1033
1040
1034 @region.conditional_cache_on_arguments(condition=cache_on)
1041 @region.conditional_cache_on_arguments(condition=cache_on)
1035 def _author(_repo_id, _commit_id):
1042 def _author(_repo_id, _commit_id):
1036 repo_init = self._factory.repo_libgit2(wire)
1043 repo_init = self._factory.repo_libgit2(wire)
1037 with repo_init as repo:
1044 with repo_init as repo:
1038 commit = repo[commit_id]
1045 commit = repo[commit_id]
1039
1046
1040 if hasattr(commit, 'author'):
1047 if hasattr(commit, 'author'):
1041 author = commit.author
1048 author = commit.author
1042 else:
1049 else:
1043 author = commit.get_object().author
1050 author = commit.get_object().author
1044
1051
1045 if author.email:
1052 if author.email:
1046 return f"{author.name} <{author.email}>"
1053 return f"{author.name} <{author.email}>"
1047
1054
1048 try:
1055 try:
1049 return f"{author.name}"
1056 return f"{author.name}"
1050 except Exception:
1057 except Exception:
1051 return f"{safe_str(author.raw_name)}"
1058 return f"{safe_str(author.raw_name)}"
1052
1059
1053 return _author(repo_id, commit_id)
1060 return _author(repo_id, commit_id)
1054
1061
1055 @reraise_safe_exceptions
1062 @reraise_safe_exceptions
1056 def message(self, wire, commit_id):
1063 def message(self, wire, commit_id):
1057 cache_on, context_uid, repo_id = self._cache_on(wire)
1064 cache_on, context_uid, repo_id = self._cache_on(wire)
1058 region = self._region(wire)
1065 region = self._region(wire)
1059
1066
1060 @region.conditional_cache_on_arguments(condition=cache_on)
1067 @region.conditional_cache_on_arguments(condition=cache_on)
1061 def _message(_repo_id, _commit_id):
1068 def _message(_repo_id, _commit_id):
1062 repo_init = self._factory.repo_libgit2(wire)
1069 repo_init = self._factory.repo_libgit2(wire)
1063 with repo_init as repo:
1070 with repo_init as repo:
1064 commit = repo[commit_id]
1071 commit = repo[commit_id]
1065 return commit.message
1072 return commit.message
1066 return _message(repo_id, commit_id)
1073 return _message(repo_id, commit_id)
1067
1074
1068 @reraise_safe_exceptions
1075 @reraise_safe_exceptions
1069 def parents(self, wire, commit_id):
1076 def parents(self, wire, commit_id):
1070 cache_on, context_uid, repo_id = self._cache_on(wire)
1077 cache_on, context_uid, repo_id = self._cache_on(wire)
1071 region = self._region(wire)
1078 region = self._region(wire)
1072
1079
1073 @region.conditional_cache_on_arguments(condition=cache_on)
1080 @region.conditional_cache_on_arguments(condition=cache_on)
1074 def _parents(_repo_id, _commit_id):
1081 def _parents(_repo_id, _commit_id):
1075 repo_init = self._factory.repo_libgit2(wire)
1082 repo_init = self._factory.repo_libgit2(wire)
1076 with repo_init as repo:
1083 with repo_init as repo:
1077 commit = repo[commit_id]
1084 commit = repo[commit_id]
1078 if hasattr(commit, 'parent_ids'):
1085 if hasattr(commit, 'parent_ids'):
1079 parent_ids = commit.parent_ids
1086 parent_ids = commit.parent_ids
1080 else:
1087 else:
1081 parent_ids = commit.get_object().parent_ids
1088 parent_ids = commit.get_object().parent_ids
1082
1089
1083 return [x.hex for x in parent_ids]
1090 return [x.hex for x in parent_ids]
1084 return _parents(repo_id, commit_id)
1091 return _parents(repo_id, commit_id)
1085
1092
1086 @reraise_safe_exceptions
1093 @reraise_safe_exceptions
1087 def children(self, wire, commit_id):
1094 def children(self, wire, commit_id):
1088 cache_on, context_uid, repo_id = self._cache_on(wire)
1095 cache_on, context_uid, repo_id = self._cache_on(wire)
1089 region = self._region(wire)
1096 region = self._region(wire)
1090
1097
1091 head = self.head(wire)
1098 head = self.head(wire)
1092
1099
1093 @region.conditional_cache_on_arguments(condition=cache_on)
1100 @region.conditional_cache_on_arguments(condition=cache_on)
1094 def _children(_repo_id, _commit_id):
1101 def _children(_repo_id, _commit_id):
1095
1102
1096 output, __ = self.run_git_command(
1103 output, __ = self.run_git_command(
1097 wire, ['rev-list', '--all', '--children', f'{commit_id}^..{head}'])
1104 wire, ['rev-list', '--all', '--children', f'{commit_id}^..{head}'])
1098
1105
1099 child_ids = []
1106 child_ids = []
1100 pat = re.compile(fr'^{commit_id}')
1107 pat = re.compile(fr'^{commit_id}')
1101 for line in output.splitlines():
1108 for line in output.splitlines():
1102 line = safe_str(line)
1109 line = safe_str(line)
1103 if pat.match(line):
1110 if pat.match(line):
1104 found_ids = line.split(' ')[1:]
1111 found_ids = line.split(' ')[1:]
1105 child_ids.extend(found_ids)
1112 child_ids.extend(found_ids)
1106 break
1113 break
1107
1114
1108 return child_ids
1115 return child_ids
1109 return _children(repo_id, commit_id)
1116 return _children(repo_id, commit_id)
1110
1117
1111 @reraise_safe_exceptions
1118 @reraise_safe_exceptions
1112 def set_refs(self, wire, key, value):
1119 def set_refs(self, wire, key, value):
1113 repo_init = self._factory.repo_libgit2(wire)
1120 repo_init = self._factory.repo_libgit2(wire)
1114 with repo_init as repo:
1121 with repo_init as repo:
1115 repo.references.create(key, value, force=True)
1122 repo.references.create(key, value, force=True)
1116
1123
1117 @reraise_safe_exceptions
1124 @reraise_safe_exceptions
1118 def update_refs(self, wire, key, value):
1125 def update_refs(self, wire, key, value):
1119 repo_init = self._factory.repo_libgit2(wire)
1126 repo_init = self._factory.repo_libgit2(wire)
1120 with repo_init as repo:
1127 with repo_init as repo:
1121 if key not in repo.references:
1128 if key not in repo.references:
1122 raise ValueError(f'Reference {key} not found in the repository')
1129 raise ValueError(f'Reference {key} not found in the repository')
1123 repo.references.create(key, value, force=True)
1130 repo.references.create(key, value, force=True)
1124
1131
1125 @reraise_safe_exceptions
1132 @reraise_safe_exceptions
1126 def create_branch(self, wire, branch_name, commit_id, force=False):
1133 def create_branch(self, wire, branch_name, commit_id, force=False):
1127 repo_init = self._factory.repo_libgit2(wire)
1134 repo_init = self._factory.repo_libgit2(wire)
1128 with repo_init as repo:
1135 with repo_init as repo:
1129 if commit_id:
1136 if commit_id:
1130 commit = repo[commit_id]
1137 commit = repo[commit_id]
1131 else:
1138 else:
1132 # if commit is not given just use the HEAD
1139 # if commit is not given just use the HEAD
1133 commit = repo.head()
1140 commit = repo.head()
1134
1141
1135 if force:
1142 if force:
1136 repo.branches.local.create(branch_name, commit, force=force)
1143 repo.branches.local.create(branch_name, commit, force=force)
1137 elif not repo.branches.get(branch_name):
1144 elif not repo.branches.get(branch_name):
1138 # create only if that branch isn't existing
1145 # create only if that branch isn't existing
1139 repo.branches.local.create(branch_name, commit, force=force)
1146 repo.branches.local.create(branch_name, commit, force=force)
1140
1147
1141 @reraise_safe_exceptions
1148 @reraise_safe_exceptions
1142 def remove_ref(self, wire, key):
1149 def remove_ref(self, wire, key):
1143 repo_init = self._factory.repo_libgit2(wire)
1150 repo_init = self._factory.repo_libgit2(wire)
1144 with repo_init as repo:
1151 with repo_init as repo:
1145 repo.references.delete(key)
1152 repo.references.delete(key)
1146
1153
1147 @reraise_safe_exceptions
1154 @reraise_safe_exceptions
1148 def tag_remove(self, wire, tag_name):
1155 def tag_remove(self, wire, tag_name):
1149 repo_init = self._factory.repo_libgit2(wire)
1156 repo_init = self._factory.repo_libgit2(wire)
1150 with repo_init as repo:
1157 with repo_init as repo:
1151 key = f'refs/tags/{tag_name}'
1158 key = f'refs/tags/{tag_name}'
1152 repo.references.delete(key)
1159 repo.references.delete(key)
1153
1160
1154 @reraise_safe_exceptions
1161 @reraise_safe_exceptions
1155 def tree_changes(self, wire, source_id, target_id):
1162 def tree_changes(self, wire, source_id, target_id):
1156 repo = self._factory.repo(wire)
1163 repo = self._factory.repo(wire)
1157 # source can be empty
1164 # source can be empty
1158 source_id = safe_bytes(source_id if source_id else b'')
1165 source_id = safe_bytes(source_id if source_id else b'')
1159 target_id = safe_bytes(target_id)
1166 target_id = safe_bytes(target_id)
1160
1167
1161 source = repo[source_id].tree if source_id else None
1168 source = repo[source_id].tree if source_id else None
1162 target = repo[target_id].tree
1169 target = repo[target_id].tree
1163 result = repo.object_store.tree_changes(source, target)
1170 result = repo.object_store.tree_changes(source, target)
1164
1171
1165 added = set()
1172 added = set()
1166 modified = set()
1173 modified = set()
1167 deleted = set()
1174 deleted = set()
1168 for (old_path, new_path), (_, _), (_, _) in list(result):
1175 for (old_path, new_path), (_, _), (_, _) in list(result):
1169 if new_path and old_path:
1176 if new_path and old_path:
1170 modified.add(new_path)
1177 modified.add(new_path)
1171 elif new_path and not old_path:
1178 elif new_path and not old_path:
1172 added.add(new_path)
1179 added.add(new_path)
1173 elif not new_path and old_path:
1180 elif not new_path and old_path:
1174 deleted.add(old_path)
1181 deleted.add(old_path)
1175
1182
1176 return list(added), list(modified), list(deleted)
1183 return list(added), list(modified), list(deleted)
1177
1184
1178 @reraise_safe_exceptions
1185 @reraise_safe_exceptions
1179 def tree_and_type_for_path(self, wire, commit_id, path):
1186 def tree_and_type_for_path(self, wire, commit_id, path):
1180
1187
1181 cache_on, context_uid, repo_id = self._cache_on(wire)
1188 cache_on, context_uid, repo_id = self._cache_on(wire)
1182 region = self._region(wire)
1189 region = self._region(wire)
1183
1190
1184 @region.conditional_cache_on_arguments(condition=cache_on)
1191 @region.conditional_cache_on_arguments(condition=cache_on)
1185 def _tree_and_type_for_path(_context_uid, _repo_id, _commit_id, _path):
1192 def _tree_and_type_for_path(_context_uid, _repo_id, _commit_id, _path):
1186 repo_init = self._factory.repo_libgit2(wire)
1193 repo_init = self._factory.repo_libgit2(wire)
1187
1194
1188 with repo_init as repo:
1195 with repo_init as repo:
1189 commit = repo[commit_id]
1196 commit = repo[commit_id]
1190 try:
1197 try:
1191 tree = commit.tree[path]
1198 tree = commit.tree[path]
1192 except KeyError:
1199 except KeyError:
1193 return None, None, None
1200 return None, None, None
1194
1201
1195 return tree.id.hex, tree.type_str, tree.filemode
1202 return tree.id.hex, tree.type_str, tree.filemode
1196 return _tree_and_type_for_path(context_uid, repo_id, commit_id, path)
1203 return _tree_and_type_for_path(context_uid, repo_id, commit_id, path)
1197
1204
1198 @reraise_safe_exceptions
1205 @reraise_safe_exceptions
1199 def tree_items(self, wire, tree_id):
1206 def tree_items(self, wire, tree_id):
1200 cache_on, context_uid, repo_id = self._cache_on(wire)
1207 cache_on, context_uid, repo_id = self._cache_on(wire)
1201 region = self._region(wire)
1208 region = self._region(wire)
1202
1209
1203 @region.conditional_cache_on_arguments(condition=cache_on)
1210 @region.conditional_cache_on_arguments(condition=cache_on)
1204 def _tree_items(_repo_id, _tree_id):
1211 def _tree_items(_repo_id, _tree_id):
1205
1212
1206 repo_init = self._factory.repo_libgit2(wire)
1213 repo_init = self._factory.repo_libgit2(wire)
1207 with repo_init as repo:
1214 with repo_init as repo:
1208 try:
1215 try:
1209 tree = repo[tree_id]
1216 tree = repo[tree_id]
1210 except KeyError:
1217 except KeyError:
1211 raise ObjectMissing(f'No tree with id: {tree_id}')
1218 raise ObjectMissing(f'No tree with id: {tree_id}')
1212
1219
1213 result = []
1220 result = []
1214 for item in tree:
1221 for item in tree:
1215 item_sha = item.hex
1222 item_sha = item.hex
1216 item_mode = item.filemode
1223 item_mode = item.filemode
1217 item_type = item.type_str
1224 item_type = item.type_str
1218
1225
1219 if item_type == 'commit':
1226 if item_type == 'commit':
1220 # NOTE(marcink): submodules we translate to 'link' for backward compat
1227 # NOTE(marcink): submodules we translate to 'link' for backward compat
1221 item_type = 'link'
1228 item_type = 'link'
1222
1229
1223 result.append((item.name, item_mode, item_sha, item_type))
1230 result.append((item.name, item_mode, item_sha, item_type))
1224 return result
1231 return result
1225 return _tree_items(repo_id, tree_id)
1232 return _tree_items(repo_id, tree_id)
1226
1233
1227 @reraise_safe_exceptions
1234 @reraise_safe_exceptions
1228 def diff_2(self, wire, commit_id_1, commit_id_2, file_filter, opt_ignorews, context):
1235 def diff_2(self, wire, commit_id_1, commit_id_2, file_filter, opt_ignorews, context):
1229 """
1236 """
1230 Old version that uses subprocess to call diff
1237 Old version that uses subprocess to call diff
1231 """
1238 """
1232
1239
1233 flags = [
1240 flags = [
1234 f'-U{context}', '--patch',
1241 f'-U{context}', '--patch',
1235 '--binary',
1242 '--binary',
1236 '--find-renames',
1243 '--find-renames',
1237 '--no-indent-heuristic',
1244 '--no-indent-heuristic',
1238 # '--indent-heuristic',
1245 # '--indent-heuristic',
1239 #'--full-index',
1246 #'--full-index',
1240 #'--abbrev=40'
1247 #'--abbrev=40'
1241 ]
1248 ]
1242
1249
1243 if opt_ignorews:
1250 if opt_ignorews:
1244 flags.append('--ignore-all-space')
1251 flags.append('--ignore-all-space')
1245
1252
1246 if commit_id_1 == self.EMPTY_COMMIT:
1253 if commit_id_1 == self.EMPTY_COMMIT:
1247 cmd = ['show'] + flags + [commit_id_2]
1254 cmd = ['show'] + flags + [commit_id_2]
1248 else:
1255 else:
1249 cmd = ['diff'] + flags + [commit_id_1, commit_id_2]
1256 cmd = ['diff'] + flags + [commit_id_1, commit_id_2]
1250
1257
1251 if file_filter:
1258 if file_filter:
1252 cmd.extend(['--', file_filter])
1259 cmd.extend(['--', file_filter])
1253
1260
1254 diff, __ = self.run_git_command(wire, cmd)
1261 diff, __ = self.run_git_command(wire, cmd)
1255 # If we used 'show' command, strip first few lines (until actual diff
1262 # If we used 'show' command, strip first few lines (until actual diff
1256 # starts)
1263 # starts)
1257 if commit_id_1 == self.EMPTY_COMMIT:
1264 if commit_id_1 == self.EMPTY_COMMIT:
1258 lines = diff.splitlines()
1265 lines = diff.splitlines()
1259 x = 0
1266 x = 0
1260 for line in lines:
1267 for line in lines:
1261 if line.startswith(b'diff'):
1268 if line.startswith(b'diff'):
1262 break
1269 break
1263 x += 1
1270 x += 1
1264 # Append new line just like 'diff' command do
1271 # Append new line just like 'diff' command do
1265 diff = '\n'.join(lines[x:]) + '\n'
1272 diff = '\n'.join(lines[x:]) + '\n'
1266 return diff
1273 return diff
1267
1274
1268 @reraise_safe_exceptions
1275 @reraise_safe_exceptions
1269 def diff(self, wire, commit_id_1, commit_id_2, file_filter, opt_ignorews, context):
1276 def diff(self, wire, commit_id_1, commit_id_2, file_filter, opt_ignorews, context):
1270 repo_init = self._factory.repo_libgit2(wire)
1277 repo_init = self._factory.repo_libgit2(wire)
1271
1278
1272 with repo_init as repo:
1279 with repo_init as repo:
1273 swap = True
1280 swap = True
1274 flags = 0
1281 flags = 0
1275 flags |= pygit2.GIT_DIFF_SHOW_BINARY
1282 flags |= pygit2.GIT_DIFF_SHOW_BINARY
1276
1283
1277 if opt_ignorews:
1284 if opt_ignorews:
1278 flags |= pygit2.GIT_DIFF_IGNORE_WHITESPACE
1285 flags |= pygit2.GIT_DIFF_IGNORE_WHITESPACE
1279
1286
1280 if commit_id_1 == self.EMPTY_COMMIT:
1287 if commit_id_1 == self.EMPTY_COMMIT:
1281 comm1 = repo[commit_id_2]
1288 comm1 = repo[commit_id_2]
1282 diff_obj = comm1.tree.diff_to_tree(
1289 diff_obj = comm1.tree.diff_to_tree(
1283 flags=flags, context_lines=context, swap=swap)
1290 flags=flags, context_lines=context, swap=swap)
1284
1291
1285 else:
1292 else:
1286 comm1 = repo[commit_id_2]
1293 comm1 = repo[commit_id_2]
1287 comm2 = repo[commit_id_1]
1294 comm2 = repo[commit_id_1]
1288 diff_obj = comm1.tree.diff_to_tree(
1295 diff_obj = comm1.tree.diff_to_tree(
1289 comm2.tree, flags=flags, context_lines=context, swap=swap)
1296 comm2.tree, flags=flags, context_lines=context, swap=swap)
1290 similar_flags = 0
1297 similar_flags = 0
1291 similar_flags |= pygit2.GIT_DIFF_FIND_RENAMES
1298 similar_flags |= pygit2.GIT_DIFF_FIND_RENAMES
1292 diff_obj.find_similar(flags=similar_flags)
1299 diff_obj.find_similar(flags=similar_flags)
1293
1300
1294 if file_filter:
1301 if file_filter:
1295 for p in diff_obj:
1302 for p in diff_obj:
1296 if p.delta.old_file.path == file_filter:
1303 if p.delta.old_file.path == file_filter:
1297 return BytesEnvelope(p.data) or BytesEnvelope(b'')
1304 return BytesEnvelope(p.data) or BytesEnvelope(b'')
1298 # fo matching path == no diff
1305 # fo matching path == no diff
1299 return BytesEnvelope(b'')
1306 return BytesEnvelope(b'')
1300
1307
1301 return BytesEnvelope(safe_bytes(diff_obj.patch)) or BytesEnvelope(b'')
1308 return BytesEnvelope(safe_bytes(diff_obj.patch)) or BytesEnvelope(b'')
1302
1309
1303 @reraise_safe_exceptions
1310 @reraise_safe_exceptions
1304 def node_history(self, wire, commit_id, path, limit):
1311 def node_history(self, wire, commit_id, path, limit):
1305 cache_on, context_uid, repo_id = self._cache_on(wire)
1312 cache_on, context_uid, repo_id = self._cache_on(wire)
1306 region = self._region(wire)
1313 region = self._region(wire)
1307
1314
1308 @region.conditional_cache_on_arguments(condition=cache_on)
1315 @region.conditional_cache_on_arguments(condition=cache_on)
1309 def _node_history(_context_uid, _repo_id, _commit_id, _path, _limit):
1316 def _node_history(_context_uid, _repo_id, _commit_id, _path, _limit):
1310 # optimize for n==1, rev-list is much faster for that use-case
1317 # optimize for n==1, rev-list is much faster for that use-case
1311 if limit == 1:
1318 if limit == 1:
1312 cmd = ['rev-list', '-1', commit_id, '--', path]
1319 cmd = ['rev-list', '-1', commit_id, '--', path]
1313 else:
1320 else:
1314 cmd = ['log']
1321 cmd = ['log']
1315 if limit:
1322 if limit:
1316 cmd.extend(['-n', str(safe_int(limit, 0))])
1323 cmd.extend(['-n', str(safe_int(limit, 0))])
1317 cmd.extend(['--pretty=format: %H', '-s', commit_id, '--', path])
1324 cmd.extend(['--pretty=format: %H', '-s', commit_id, '--', path])
1318
1325
1319 output, __ = self.run_git_command(wire, cmd)
1326 output, __ = self.run_git_command(wire, cmd)
1320 commit_ids = re.findall(rb'[0-9a-fA-F]{40}', output)
1327 commit_ids = re.findall(rb'[0-9a-fA-F]{40}', output)
1321
1328
1322 return [x for x in commit_ids]
1329 return [x for x in commit_ids]
1323 return _node_history(context_uid, repo_id, commit_id, path, limit)
1330 return _node_history(context_uid, repo_id, commit_id, path, limit)
1324
1331
1325 @reraise_safe_exceptions
1332 @reraise_safe_exceptions
1326 def node_annotate_legacy(self, wire, commit_id, path):
1333 def node_annotate_legacy(self, wire, commit_id, path):
1327 # note: replaced by pygit2 implementation
1334 # note: replaced by pygit2 implementation
1328 cmd = ['blame', '-l', '--root', '-r', commit_id, '--', path]
1335 cmd = ['blame', '-l', '--root', '-r', commit_id, '--', path]
1329 # -l ==> outputs long shas (and we need all 40 characters)
1336 # -l ==> outputs long shas (and we need all 40 characters)
1330 # --root ==> doesn't put '^' character for boundaries
1337 # --root ==> doesn't put '^' character for boundaries
1331 # -r commit_id ==> blames for the given commit
1338 # -r commit_id ==> blames for the given commit
1332 output, __ = self.run_git_command(wire, cmd)
1339 output, __ = self.run_git_command(wire, cmd)
1333
1340
1334 result = []
1341 result = []
1335 for i, blame_line in enumerate(output.splitlines()[:-1]):
1342 for i, blame_line in enumerate(output.splitlines()[:-1]):
1336 line_no = i + 1
1343 line_no = i + 1
1337 blame_commit_id, line = re.split(rb' ', blame_line, 1)
1344 blame_commit_id, line = re.split(rb' ', blame_line, 1)
1338 result.append((line_no, blame_commit_id, line))
1345 result.append((line_no, blame_commit_id, line))
1339
1346
1340 return result
1347 return result
1341
1348
1342 @reraise_safe_exceptions
1349 @reraise_safe_exceptions
1343 def node_annotate(self, wire, commit_id, path):
1350 def node_annotate(self, wire, commit_id, path):
1344
1351
1345 result_libgit = []
1352 result_libgit = []
1346 repo_init = self._factory.repo_libgit2(wire)
1353 repo_init = self._factory.repo_libgit2(wire)
1347 with repo_init as repo:
1354 with repo_init as repo:
1348 commit = repo[commit_id]
1355 commit = repo[commit_id]
1349 blame_obj = repo.blame(path, newest_commit=commit_id)
1356 blame_obj = repo.blame(path, newest_commit=commit_id)
1350 file_content = commit.tree[path].data
1357 file_content = commit.tree[path].data
1351 for i, line in enumerate(splitnewlines(file_content)):
1358 for i, line in enumerate(splitnewlines(file_content)):
1352 line_no = i + 1
1359 line_no = i + 1
1353 hunk = blame_obj.for_line(line_no)
1360 hunk = blame_obj.for_line(line_no)
1354 blame_commit_id = hunk.final_commit_id.hex
1361 blame_commit_id = hunk.final_commit_id.hex
1355
1362
1356 result_libgit.append((line_no, blame_commit_id, line))
1363 result_libgit.append((line_no, blame_commit_id, line))
1357
1364
1358 return BinaryEnvelope(result_libgit)
1365 return BinaryEnvelope(result_libgit)
1359
1366
1360 @reraise_safe_exceptions
1367 @reraise_safe_exceptions
1361 def update_server_info(self, wire, force=False):
1368 def update_server_info(self, wire, force=False):
1362 cmd = ['update-server-info']
1369 cmd = ['update-server-info']
1363 if force:
1370 if force:
1364 cmd += ['--force']
1371 cmd += ['--force']
1365 output, __ = self.run_git_command(wire, cmd)
1372 output, __ = self.run_git_command(wire, cmd)
1366 return output.splitlines()
1373 return output.splitlines()
1367
1374
1368 @reraise_safe_exceptions
1375 @reraise_safe_exceptions
1369 def get_all_commit_ids(self, wire):
1376 def get_all_commit_ids(self, wire):
1370
1377
1371 cache_on, context_uid, repo_id = self._cache_on(wire)
1378 cache_on, context_uid, repo_id = self._cache_on(wire)
1372 region = self._region(wire)
1379 region = self._region(wire)
1373
1380
1374 @region.conditional_cache_on_arguments(condition=cache_on)
1381 @region.conditional_cache_on_arguments(condition=cache_on)
1375 def _get_all_commit_ids(_context_uid, _repo_id):
1382 def _get_all_commit_ids(_context_uid, _repo_id):
1376
1383
1377 cmd = ['rev-list', '--reverse', '--date-order', '--branches', '--tags']
1384 cmd = ['rev-list', '--reverse', '--date-order', '--branches', '--tags']
1378 try:
1385 try:
1379 output, __ = self.run_git_command(wire, cmd)
1386 output, __ = self.run_git_command(wire, cmd)
1380 return output.splitlines()
1387 return output.splitlines()
1381 except Exception:
1388 except Exception:
1382 # Can be raised for empty repositories
1389 # Can be raised for empty repositories
1383 return []
1390 return []
1384
1391
1385 @region.conditional_cache_on_arguments(condition=cache_on)
1392 @region.conditional_cache_on_arguments(condition=cache_on)
1386 def _get_all_commit_ids_pygit2(_context_uid, _repo_id):
1393 def _get_all_commit_ids_pygit2(_context_uid, _repo_id):
1387 repo_init = self._factory.repo_libgit2(wire)
1394 repo_init = self._factory.repo_libgit2(wire)
1388 from pygit2 import GIT_SORT_REVERSE, GIT_SORT_TIME, GIT_BRANCH_ALL
1395 from pygit2 import GIT_SORT_REVERSE, GIT_SORT_TIME, GIT_BRANCH_ALL
1389 results = []
1396 results = []
1390 with repo_init as repo:
1397 with repo_init as repo:
1391 for commit in repo.walk(repo.head.target, GIT_SORT_TIME | GIT_BRANCH_ALL | GIT_SORT_REVERSE):
1398 for commit in repo.walk(repo.head.target, GIT_SORT_TIME | GIT_BRANCH_ALL | GIT_SORT_REVERSE):
1392 results.append(commit.id.hex)
1399 results.append(commit.id.hex)
1393
1400
1394 return _get_all_commit_ids(context_uid, repo_id)
1401 return _get_all_commit_ids(context_uid, repo_id)
1395
1402
1396 @reraise_safe_exceptions
1403 @reraise_safe_exceptions
1397 def run_git_command(self, wire, cmd, **opts):
1404 def run_git_command(self, wire, cmd, **opts):
1398 path = wire.get('path', None)
1405 path = wire.get('path', None)
1399 debug_mode = rhodecode.ConfigGet().get_bool('debug')
1406 debug_mode = rhodecode.ConfigGet().get_bool('debug')
1400
1407
1401 if path and os.path.isdir(path):
1408 if path and os.path.isdir(path):
1402 opts['cwd'] = path
1409 opts['cwd'] = path
1403
1410
1404 if '_bare' in opts:
1411 if '_bare' in opts:
1405 _copts = []
1412 _copts = []
1406 del opts['_bare']
1413 del opts['_bare']
1407 else:
1414 else:
1408 _copts = ['-c', 'core.quotepath=false', '-c', 'advice.diverging=false']
1415 _copts = ['-c', 'core.quotepath=false', '-c', 'advice.diverging=false']
1409 safe_call = False
1416 safe_call = False
1410 if '_safe' in opts:
1417 if '_safe' in opts:
1411 # no exc on failure
1418 # no exc on failure
1412 del opts['_safe']
1419 del opts['_safe']
1413 safe_call = True
1420 safe_call = True
1414
1421
1415 if '_copts' in opts:
1422 if '_copts' in opts:
1416 _copts.extend(opts['_copts'] or [])
1423 _copts.extend(opts['_copts'] or [])
1417 del opts['_copts']
1424 del opts['_copts']
1418
1425
1419 gitenv = os.environ.copy()
1426 gitenv = os.environ.copy()
1420 gitenv.update(opts.pop('extra_env', {}))
1427 gitenv.update(opts.pop('extra_env', {}))
1421 # need to clean fix GIT_DIR !
1428 # need to clean fix GIT_DIR !
1422 if 'GIT_DIR' in gitenv:
1429 if 'GIT_DIR' in gitenv:
1423 del gitenv['GIT_DIR']
1430 del gitenv['GIT_DIR']
1424 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
1431 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
1425 gitenv['GIT_DISCOVERY_ACROSS_FILESYSTEM'] = '1'
1432 gitenv['GIT_DISCOVERY_ACROSS_FILESYSTEM'] = '1'
1426
1433
1427 cmd = [settings.GIT_EXECUTABLE()] + _copts + cmd
1434 cmd = [settings.GIT_EXECUTABLE()] + _copts + cmd
1428 _opts = {'env': gitenv, 'shell': False}
1435 _opts = {'env': gitenv, 'shell': False}
1429
1436
1430 proc = None
1437 proc = None
1431 try:
1438 try:
1432 _opts.update(opts)
1439 _opts.update(opts)
1433 proc = subprocessio.SubprocessIOChunker(cmd, **_opts)
1440 proc = subprocessio.SubprocessIOChunker(cmd, **_opts)
1434
1441
1435 return b''.join(proc), b''.join(proc.stderr)
1442 return b''.join(proc), b''.join(proc.stderr)
1436 except OSError as err:
1443 except OSError as err:
1437 cmd = ' '.join(map(safe_str, cmd)) # human friendly CMD
1444 cmd = ' '.join(map(safe_str, cmd)) # human friendly CMD
1438 call_opts = {}
1445 call_opts = {}
1439 if debug_mode:
1446 if debug_mode:
1440 call_opts = _opts
1447 call_opts = _opts
1441
1448
1442 tb_err = ("Couldn't run git command ({}).\n"
1449 tb_err = ("Couldn't run git command ({}).\n"
1443 "Original error was:{}\n"
1450 "Original error was:{}\n"
1444 "Call options:{}\n"
1451 "Call options:{}\n"
1445 .format(cmd, err, call_opts))
1452 .format(cmd, err, call_opts))
1446 log.exception(tb_err)
1453 log.exception(tb_err)
1447 if safe_call:
1454 if safe_call:
1448 return '', err
1455 return '', err
1449 else:
1456 else:
1450 raise exceptions.VcsException()(tb_err)
1457 raise exceptions.VcsException()(tb_err)
1451 finally:
1458 finally:
1452 if proc:
1459 if proc:
1453 proc.close()
1460 proc.close()
1454
1461
1455 @reraise_safe_exceptions
1462 @reraise_safe_exceptions
1456 def install_hooks(self, wire, force=False):
1463 def install_hooks(self, wire, force=False):
1457 from vcsserver.hook_utils import install_git_hooks
1464 from vcsserver.hook_utils import install_git_hooks
1458 bare = self.bare(wire)
1465 bare = self.bare(wire)
1459 path = wire['path']
1466 path = wire['path']
1460 binary_dir = settings.BINARY_DIR
1467 binary_dir = settings.BINARY_DIR
1461 if binary_dir:
1468 if binary_dir:
1462 os.path.join(binary_dir, 'python3')
1469 os.path.join(binary_dir, 'python3')
1463 return install_git_hooks(path, bare, force_create=force)
1470 return install_git_hooks(path, bare, force_create=force)
1464
1471
1465 @reraise_safe_exceptions
1472 @reraise_safe_exceptions
1466 def get_hooks_info(self, wire):
1473 def get_hooks_info(self, wire):
1467 from vcsserver.hook_utils import (
1474 from vcsserver.hook_utils import (
1468 get_git_pre_hook_version, get_git_post_hook_version)
1475 get_git_pre_hook_version, get_git_post_hook_version)
1469 bare = self.bare(wire)
1476 bare = self.bare(wire)
1470 path = wire['path']
1477 path = wire['path']
1471 return {
1478 return {
1472 'pre_version': get_git_pre_hook_version(path, bare),
1479 'pre_version': get_git_pre_hook_version(path, bare),
1473 'post_version': get_git_post_hook_version(path, bare),
1480 'post_version': get_git_post_hook_version(path, bare),
1474 }
1481 }
1475
1482
1476 @reraise_safe_exceptions
1483 @reraise_safe_exceptions
1477 def set_head_ref(self, wire, head_name):
1484 def set_head_ref(self, wire, head_name):
1478 log.debug('Setting refs/head to `%s`', head_name)
1485 log.debug('Setting refs/head to `%s`', head_name)
1479 repo_init = self._factory.repo_libgit2(wire)
1486 repo_init = self._factory.repo_libgit2(wire)
1480 with repo_init as repo:
1487 with repo_init as repo:
1481 repo.set_head(f'refs/heads/{head_name}')
1488 repo.set_head(f'refs/heads/{head_name}')
1482
1489
1483 return [head_name] + [f'set HEAD to refs/heads/{head_name}']
1490 return [head_name] + [f'set HEAD to refs/heads/{head_name}']
1484
1491
1485 @reraise_safe_exceptions
1492 @reraise_safe_exceptions
1486 def archive_repo(self, wire, archive_name_key, kind, mtime, archive_at_path,
1493 def archive_repo(self, wire, archive_name_key, kind, mtime, archive_at_path,
1487 archive_dir_name, commit_id, cache_config):
1494 archive_dir_name, commit_id, cache_config):
1488
1495
1489 def file_walker(_commit_id, path):
1496 def file_walker(_commit_id, path):
1490 repo_init = self._factory.repo_libgit2(wire)
1497 repo_init = self._factory.repo_libgit2(wire)
1491
1498
1492 with repo_init as repo:
1499 with repo_init as repo:
1493 commit = repo[commit_id]
1500 commit = repo[commit_id]
1494
1501
1495 if path in ['', '/']:
1502 if path in ['', '/']:
1496 tree = commit.tree
1503 tree = commit.tree
1497 else:
1504 else:
1498 tree = commit.tree[path.rstrip('/')]
1505 tree = commit.tree[path.rstrip('/')]
1499 tree_id = tree.id.hex
1506 tree_id = tree.id.hex
1500 try:
1507 try:
1501 tree = repo[tree_id]
1508 tree = repo[tree_id]
1502 except KeyError:
1509 except KeyError:
1503 raise ObjectMissing(f'No tree with id: {tree_id}')
1510 raise ObjectMissing(f'No tree with id: {tree_id}')
1504
1511
1505 index = LibGit2Index.Index()
1512 index = LibGit2Index.Index()
1506 index.read_tree(tree)
1513 index.read_tree(tree)
1507 file_iter = index
1514 file_iter = index
1508
1515
1509 for file_node in file_iter:
1516 for file_node in file_iter:
1510 file_path = file_node.path
1517 file_path = file_node.path
1511 mode = file_node.mode
1518 mode = file_node.mode
1512 is_link = stat.S_ISLNK(mode)
1519 is_link = stat.S_ISLNK(mode)
1513 if mode == pygit2.GIT_FILEMODE_COMMIT:
1520 if mode == pygit2.GIT_FILEMODE_COMMIT:
1514 log.debug('Skipping path %s as a commit node', file_path)
1521 log.debug('Skipping path %s as a commit node', file_path)
1515 continue
1522 continue
1516 yield ArchiveNode(file_path, mode, is_link, repo[file_node.hex].read_raw)
1523 yield ArchiveNode(file_path, mode, is_link, repo[file_node.hex].read_raw)
1517
1524
1518 return store_archive_in_cache(
1525 return store_archive_in_cache(
1519 file_walker, archive_name_key, kind, mtime, archive_at_path, archive_dir_name, commit_id, cache_config=cache_config)
1526 file_walker, archive_name_key, kind, mtime, archive_at_path, archive_dir_name, commit_id, cache_config=cache_config)
General Comments 0
You need to be logged in to leave comments. Login now