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