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