##// END OF EJS Templates
core: few python3 fixes found during ce tests runs
super-admin -
r1085:4d8f2d38 python3
parent child Browse files
Show More
@@ -1,136 +1,136 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-2020 RhodeCode GmbH
2 # Copyright (C) 2014-2020 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 import os
17 import os
18 import sys
18 import sys
19 import traceback
19 import traceback
20 import logging
20 import logging
21 import urllib.parse
21 import urllib.parse
22
22
23 from vcsserver.lib.rc_cache import region_meta
23 from vcsserver.lib.rc_cache import region_meta
24
24
25 from vcsserver import exceptions
25 from vcsserver import exceptions
26 from vcsserver.exceptions import NoContentException
26 from vcsserver.exceptions import NoContentException
27 from vcsserver.hgcompat import archival
27 from vcsserver.hgcompat import archival
28 from vcsserver.str_utils import safe_bytes
28 from vcsserver.str_utils import safe_bytes
29
29
30 log = logging.getLogger(__name__)
30 log = logging.getLogger(__name__)
31
31
32
32
33 class RepoFactory(object):
33 class RepoFactory(object):
34 """
34 """
35 Utility to create instances of repository
35 Utility to create instances of repository
36
36
37 It provides internal caching of the `repo` object based on
37 It provides internal caching of the `repo` object based on
38 the :term:`call context`.
38 the :term:`call context`.
39 """
39 """
40 repo_type = None
40 repo_type = None
41
41
42 def __init__(self):
42 def __init__(self):
43 self._cache_region = region_meta.dogpile_cache_regions['repo_object']
43 self._cache_region = region_meta.dogpile_cache_regions['repo_object']
44
44
45 def _create_config(self, path, config):
45 def _create_config(self, path, config):
46 config = {}
46 config = {}
47 return config
47 return config
48
48
49 def _create_repo(self, wire, create):
49 def _create_repo(self, wire, create):
50 raise NotImplementedError()
50 raise NotImplementedError()
51
51
52 def repo(self, wire, create=False):
52 def repo(self, wire, create=False):
53 raise NotImplementedError()
53 raise NotImplementedError()
54
54
55
55
56 def obfuscate_qs(query_string):
56 def obfuscate_qs(query_string):
57 if query_string is None:
57 if query_string is None:
58 return None
58 return None
59
59
60 parsed = []
60 parsed = []
61 for k, v in urllib.parse.parse_qsl(query_string, keep_blank_values=True):
61 for k, v in urllib.parse.parse_qsl(query_string, keep_blank_values=True):
62 if k in ['auth_token', 'api_key']:
62 if k in ['auth_token', 'api_key']:
63 v = "*****"
63 v = "*****"
64 parsed.append((k, v))
64 parsed.append((k, v))
65
65
66 return '&'.join('{}{}'.format(
66 return '&'.join('{}{}'.format(
67 k, '={}'.format(v) if v else '') for k, v in parsed)
67 k, '={}'.format(v) if v else '') for k, v in parsed)
68
68
69
69
70 def raise_from_original(new_type, org_exc: Exception):
70 def raise_from_original(new_type, org_exc: Exception):
71 """
71 """
72 Raise a new exception type with original args and traceback.
72 Raise a new exception type with original args and traceback.
73 """
73 """
74
74
75 exc_type, exc_value, exc_traceback = sys.exc_info()
75 exc_type, exc_value, exc_traceback = sys.exc_info()
76 new_exc = new_type(*exc_value.args)
76 new_exc = new_type(*exc_value.args)
77
77
78 # store the original traceback into the new exc
78 # store the original traceback into the new exc
79 new_exc._org_exc_tb = traceback.format_tb(exc_traceback)
79 new_exc._org_exc_tb = traceback.format_tb(exc_traceback)
80
80
81 try:
81 try:
82 raise new_exc.with_traceback(exc_traceback)
82 raise new_exc.with_traceback(exc_traceback)
83 finally:
83 finally:
84 del exc_traceback
84 del exc_traceback
85
85
86
86
87 class ArchiveNode(object):
87 class ArchiveNode(object):
88 def __init__(self, path, mode, is_link, raw_bytes):
88 def __init__(self, path, mode, is_link, raw_bytes):
89 self.path = path
89 self.path = path
90 self.mode = mode
90 self.mode = mode
91 self.is_link = is_link
91 self.is_link = is_link
92 self.raw_bytes = raw_bytes
92 self.raw_bytes = raw_bytes
93
93
94
94
95 def archive_repo(walker, archive_dest_path, kind, mtime, archive_at_path,
95 def archive_repo(walker, archive_dest_path, kind, mtime, archive_at_path,
96 archive_dir_name, commit_id, write_metadata=True, extra_metadata=None):
96 archive_dir_name, commit_id, write_metadata=True, extra_metadata=None):
97 """
97 """
98 walker should be a file walker, for example:
98 walker should be a file walker, for example:
99 def walker():
99 def walker():
100 for file_info in files:
100 for file_info in files:
101 yield ArchiveNode(fn, mode, is_link, ctx[fn].data)
101 yield ArchiveNode(fn, mode, is_link, ctx[fn].data)
102 """
102 """
103 extra_metadata = extra_metadata or {}
103 extra_metadata = extra_metadata or {}
104 archive_dest_path = safe_bytes(archive_dest_path)
104 archive_dest_path = safe_bytes(archive_dest_path)
105
105
106 if kind == "tgz":
106 if kind == "tgz":
107 archiver = archival.tarit(archive_dest_path, mtime, b"gz")
107 archiver = archival.tarit(archive_dest_path, mtime, b"gz")
108 elif kind == "tbz2":
108 elif kind == "tbz2":
109 archiver = archival.tarit(archive_dest_path, mtime, b"bz2")
109 archiver = archival.tarit(archive_dest_path, mtime, b"bz2")
110 elif kind == 'zip':
110 elif kind == 'zip':
111 archiver = archival.zipit(archive_dest_path, mtime)
111 archiver = archival.zipit(archive_dest_path, mtime)
112 else:
112 else:
113 raise exceptions.ArchiveException()(
113 raise exceptions.ArchiveException()(
114 f'Remote does not support: "{kind}" archive type.')
114 f'Remote does not support: "{kind}" archive type.')
115
115
116 for f in walker(commit_id, archive_at_path):
116 for f in walker(commit_id, archive_at_path):
117 f_path = os.path.join(safe_bytes(archive_dir_name), f.path.lstrip(b'/'))
117 f_path = os.path.join(safe_bytes(archive_dir_name), safe_bytes(f.path).lstrip(b'/'))
118 try:
118 try:
119 archiver.addfile(f_path, f.mode, f.is_link, f.raw_bytes())
119 archiver.addfile(f_path, f.mode, f.is_link, f.raw_bytes())
120 except NoContentException:
120 except NoContentException:
121 # NOTE(marcink): this is a special case for SVN so we can create "empty"
121 # NOTE(marcink): this is a special case for SVN so we can create "empty"
122 # directories which arent supported by archiver
122 # directories which arent supported by archiver
123 archiver.addfile(os.path.join(f_path, b'.dir'), f.mode, f.is_link, '')
123 archiver.addfile(os.path.join(f_path, b'.dir'), f.mode, f.is_link, b'')
124
124
125 if write_metadata:
125 if write_metadata:
126 metadata = dict([
126 metadata = dict([
127 ('commit_id', commit_id),
127 ('commit_id', commit_id),
128 ('mtime', mtime),
128 ('mtime', mtime),
129 ])
129 ])
130 metadata.update(extra_metadata)
130 metadata.update(extra_metadata)
131
131
132 meta = [safe_bytes(f"{f_name}:{value}") for f_name, value in metadata.items()]
132 meta = [safe_bytes(f"{f_name}:{value}") for f_name, value in metadata.items()]
133 f_path = os.path.join(safe_bytes(archive_dir_name), b'.archival.txt')
133 f_path = os.path.join(safe_bytes(archive_dir_name), b'.archival.txt')
134 archiver.addfile(f_path, 0o644, False, b'\n'.join(meta))
134 archiver.addfile(f_path, 0o644, False, b'\n'.join(meta))
135
135
136 return archiver.done()
136 return archiver.done()
@@ -1,1366 +1,1367 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-2020 RhodeCode GmbH
2 # Copyright (C) 2014-2020 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 posixpath as vcspath
21 import posixpath as vcspath
22 import re
22 import re
23 import stat
23 import stat
24 import traceback
24 import traceback
25 import urllib.request, urllib.parse, urllib.error
25 import urllib.request, urllib.parse, urllib.error
26 import urllib.request, urllib.error, urllib.parse
26 import urllib.request, urllib.error, urllib.parse
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
34 from dulwich.client import HttpGitClient, LocalGitClient
35 from dulwich.errors import (
35 from dulwich.errors import (
36 NotGitRepository, ChecksumMismatch, WrongObjectException,
36 NotGitRepository, ChecksumMismatch, WrongObjectException,
37 MissingCommitError, ObjectMissing, HangupException,
37 MissingCommitError, ObjectMissing, HangupException,
38 UnexpectedCommandError)
38 UnexpectedCommandError)
39 from dulwich.repo import Repo as DulwichRepo
39 from dulwich.repo import Repo as DulwichRepo
40 from dulwich.server import update_server_info
40 from dulwich.server import update_server_info
41
41
42 from vcsserver import exceptions, settings, subprocessio
42 from vcsserver import exceptions, settings, subprocessio
43 from vcsserver.str_utils import safe_str, safe_int, safe_bytes, ascii_str, ascii_bytes
43 from vcsserver.str_utils import safe_str, safe_int, safe_bytes, ascii_str, ascii_bytes
44 from vcsserver.base import RepoFactory, obfuscate_qs, ArchiveNode, archive_repo
44 from vcsserver.base import RepoFactory, obfuscate_qs, ArchiveNode, archive_repo
45 from vcsserver.hgcompat import (
45 from vcsserver.hgcompat import (
46 hg_url as url_parser, httpbasicauthhandler, httpdigestauthhandler)
46 hg_url as url_parser, httpbasicauthhandler, httpdigestauthhandler)
47 from vcsserver.git_lfs.lib import LFSOidStore
47 from vcsserver.git_lfs.lib import LFSOidStore
48 from vcsserver.vcs_base import RemoteBase
48 from vcsserver.vcs_base import RemoteBase
49
49
50 DIR_STAT = stat.S_IFDIR
50 DIR_STAT = stat.S_IFDIR
51 FILE_MODE = stat.S_IFMT
51 FILE_MODE = stat.S_IFMT
52 GIT_LINK = objects.S_IFGITLINK
52 GIT_LINK = objects.S_IFGITLINK
53 PEELED_REF_MARKER = b'^{}'
53 PEELED_REF_MARKER = b'^{}'
54 HEAD_MARKER = b'HEAD'
54 HEAD_MARKER = b'HEAD'
55
55
56 log = logging.getLogger(__name__)
56 log = logging.getLogger(__name__)
57
57
58
58
59 def reraise_safe_exceptions(func):
59 def reraise_safe_exceptions(func):
60 """Converts Dulwich exceptions to something neutral."""
60 """Converts Dulwich exceptions to something neutral."""
61
61
62 @wraps(func)
62 @wraps(func)
63 def wrapper(*args, **kwargs):
63 def wrapper(*args, **kwargs):
64 try:
64 try:
65 return func(*args, **kwargs)
65 return func(*args, **kwargs)
66 except (ChecksumMismatch, WrongObjectException, MissingCommitError, ObjectMissing,) as e:
66 except (ChecksumMismatch, WrongObjectException, MissingCommitError, ObjectMissing,) as e:
67 exc = exceptions.LookupException(org_exc=e)
67 exc = exceptions.LookupException(org_exc=e)
68 raise exc(safe_str(e))
68 raise exc(safe_str(e))
69 except (HangupException, UnexpectedCommandError) as e:
69 except (HangupException, UnexpectedCommandError) as e:
70 exc = exceptions.VcsException(org_exc=e)
70 exc = exceptions.VcsException(org_exc=e)
71 raise exc(safe_str(e))
71 raise exc(safe_str(e))
72 except Exception as e:
72 except Exception as e:
73 # NOTE(marcink): becuase of how dulwich handles some exceptions
73 # NOTE(marcink): becuase of how dulwich handles some exceptions
74 # (KeyError on empty repos), we cannot track this and catch all
74 # (KeyError on empty repos), we cannot track this and catch all
75 # exceptions, it's an exceptions from other handlers
75 # exceptions, it's an exceptions from other handlers
76 #if not hasattr(e, '_vcs_kind'):
76 #if not hasattr(e, '_vcs_kind'):
77 #log.exception("Unhandled exception in git remote call")
77 #log.exception("Unhandled exception in git remote call")
78 #raise_from_original(exceptions.UnhandledException)
78 #raise_from_original(exceptions.UnhandledException)
79 raise
79 raise
80 return wrapper
80 return wrapper
81
81
82
82
83 class Repo(DulwichRepo):
83 class Repo(DulwichRepo):
84 """
84 """
85 A wrapper for dulwich Repo class.
85 A wrapper for dulwich Repo class.
86
86
87 Since dulwich is sometimes keeping .idx file descriptors open, it leads to
87 Since dulwich is sometimes keeping .idx file descriptors open, it leads to
88 "Too many open files" error. We need to close all opened file descriptors
88 "Too many open files" error. We need to close all opened file descriptors
89 once the repo object is destroyed.
89 once the repo object is destroyed.
90 """
90 """
91 def __del__(self):
91 def __del__(self):
92 if hasattr(self, 'object_store'):
92 if hasattr(self, 'object_store'):
93 self.close()
93 self.close()
94
94
95
95
96 class Repository(LibGit2Repo):
96 class Repository(LibGit2Repo):
97
97
98 def __enter__(self):
98 def __enter__(self):
99 return self
99 return self
100
100
101 def __exit__(self, exc_type, exc_val, exc_tb):
101 def __exit__(self, exc_type, exc_val, exc_tb):
102 self.free()
102 self.free()
103
103
104
104
105 class GitFactory(RepoFactory):
105 class GitFactory(RepoFactory):
106 repo_type = 'git'
106 repo_type = 'git'
107
107
108 def _create_repo(self, wire, create, use_libgit2=False):
108 def _create_repo(self, wire, create, use_libgit2=False):
109 if use_libgit2:
109 if use_libgit2:
110 return Repository(safe_bytes(wire['path']))
110 return Repository(safe_bytes(wire['path']))
111 else:
111 else:
112 # dulwich mode
112 # dulwich mode
113 repo_path = safe_str(wire['path'], to_encoding=settings.WIRE_ENCODING)
113 repo_path = safe_str(wire['path'], to_encoding=settings.WIRE_ENCODING)
114 return Repo(repo_path)
114 return Repo(repo_path)
115
115
116 def repo(self, wire, create=False, use_libgit2=False):
116 def repo(self, wire, create=False, use_libgit2=False):
117 """
117 """
118 Get a repository instance for the given path.
118 Get a repository instance for the given path.
119 """
119 """
120 return self._create_repo(wire, create, use_libgit2)
120 return self._create_repo(wire, create, use_libgit2)
121
121
122 def repo_libgit2(self, wire):
122 def repo_libgit2(self, wire):
123 return self.repo(wire, use_libgit2=True)
123 return self.repo(wire, use_libgit2=True)
124
124
125
125
126 class GitRemote(RemoteBase):
126 class GitRemote(RemoteBase):
127
127
128 def __init__(self, factory):
128 def __init__(self, factory):
129 self._factory = factory
129 self._factory = factory
130 self._bulk_methods = {
130 self._bulk_methods = {
131 "date": self.date,
131 "date": self.date,
132 "author": self.author,
132 "author": self.author,
133 "branch": self.branch,
133 "branch": self.branch,
134 "message": self.message,
134 "message": self.message,
135 "parents": self.parents,
135 "parents": self.parents,
136 "_commit": self.revision,
136 "_commit": self.revision,
137 }
137 }
138
138
139 def _wire_to_config(self, wire):
139 def _wire_to_config(self, wire):
140 if 'config' in wire:
140 if 'config' in wire:
141 return dict([(x[0] + '_' + x[1], x[2]) for x in wire['config']])
141 return dict([(x[0] + '_' + x[1], x[2]) for x in wire['config']])
142 return {}
142 return {}
143
143
144 def _remote_conf(self, config):
144 def _remote_conf(self, config):
145 params = [
145 params = [
146 '-c', 'core.askpass=""',
146 '-c', 'core.askpass=""',
147 ]
147 ]
148 ssl_cert_dir = config.get('vcs_ssl_dir')
148 ssl_cert_dir = config.get('vcs_ssl_dir')
149 if ssl_cert_dir:
149 if ssl_cert_dir:
150 params.extend(['-c', 'http.sslCAinfo={}'.format(ssl_cert_dir)])
150 params.extend(['-c', 'http.sslCAinfo={}'.format(ssl_cert_dir)])
151 return params
151 return params
152
152
153 @reraise_safe_exceptions
153 @reraise_safe_exceptions
154 def discover_git_version(self):
154 def discover_git_version(self):
155 stdout, _ = self.run_git_command(
155 stdout, _ = self.run_git_command(
156 {}, ['--version'], _bare=True, _safe=True)
156 {}, ['--version'], _bare=True, _safe=True)
157 prefix = b'git version'
157 prefix = b'git version'
158 if stdout.startswith(prefix):
158 if stdout.startswith(prefix):
159 stdout = stdout[len(prefix):]
159 stdout = stdout[len(prefix):]
160 return safe_str(stdout.strip())
160 return safe_str(stdout.strip())
161
161
162 @reraise_safe_exceptions
162 @reraise_safe_exceptions
163 def is_empty(self, wire):
163 def is_empty(self, wire):
164 repo_init = self._factory.repo_libgit2(wire)
164 repo_init = self._factory.repo_libgit2(wire)
165 with repo_init as repo:
165 with repo_init as repo:
166
166
167 try:
167 try:
168 has_head = repo.head.name
168 has_head = repo.head.name
169 if has_head:
169 if has_head:
170 return False
170 return False
171
171
172 # NOTE(marcink): check again using more expensive method
172 # NOTE(marcink): check again using more expensive method
173 return repo.is_empty
173 return repo.is_empty
174 except Exception:
174 except Exception:
175 pass
175 pass
176
176
177 return True
177 return True
178
178
179 @reraise_safe_exceptions
179 @reraise_safe_exceptions
180 def assert_correct_path(self, wire):
180 def assert_correct_path(self, wire):
181 cache_on, context_uid, repo_id = self._cache_on(wire)
181 cache_on, context_uid, repo_id = self._cache_on(wire)
182 region = self._region(wire)
182 region = self._region(wire)
183
183
184 @region.conditional_cache_on_arguments(condition=cache_on)
184 @region.conditional_cache_on_arguments(condition=cache_on)
185 def _assert_correct_path(_context_uid, _repo_id):
185 def _assert_correct_path(_context_uid, _repo_id):
186 try:
186 try:
187 repo_init = self._factory.repo_libgit2(wire)
187 repo_init = self._factory.repo_libgit2(wire)
188 with repo_init as repo:
188 with repo_init as repo:
189 pass
189 pass
190 except pygit2.GitError:
190 except pygit2.GitError:
191 path = wire.get('path')
191 path = wire.get('path')
192 tb = traceback.format_exc()
192 tb = traceback.format_exc()
193 log.debug("Invalid Git path `%s`, tb: %s", path, tb)
193 log.debug("Invalid Git path `%s`, tb: %s", path, tb)
194 return False
194 return False
195
195
196 return True
196 return True
197 return _assert_correct_path(context_uid, repo_id)
197 return _assert_correct_path(context_uid, repo_id)
198
198
199 @reraise_safe_exceptions
199 @reraise_safe_exceptions
200 def bare(self, wire):
200 def bare(self, wire):
201 repo_init = self._factory.repo_libgit2(wire)
201 repo_init = self._factory.repo_libgit2(wire)
202 with repo_init as repo:
202 with repo_init as repo:
203 return repo.is_bare
203 return repo.is_bare
204
204
205 @reraise_safe_exceptions
205 @reraise_safe_exceptions
206 def blob_as_pretty_string(self, wire, sha):
206 def blob_as_pretty_string(self, wire, sha):
207 repo_init = self._factory.repo_libgit2(wire)
207 repo_init = self._factory.repo_libgit2(wire)
208 with repo_init as repo:
208 with repo_init as repo:
209 blob_obj = repo[sha]
209 blob_obj = repo[sha]
210 blob = blob_obj.data
210 blob = blob_obj.data
211 return blob
211 return blob
212
212
213 @reraise_safe_exceptions
213 @reraise_safe_exceptions
214 def blob_raw_length(self, wire, sha):
214 def blob_raw_length(self, wire, sha):
215 cache_on, context_uid, repo_id = self._cache_on(wire)
215 cache_on, context_uid, repo_id = self._cache_on(wire)
216 region = self._region(wire)
216 region = self._region(wire)
217
217
218 @region.conditional_cache_on_arguments(condition=cache_on)
218 @region.conditional_cache_on_arguments(condition=cache_on)
219 def _blob_raw_length(_repo_id, _sha):
219 def _blob_raw_length(_repo_id, _sha):
220
220
221 repo_init = self._factory.repo_libgit2(wire)
221 repo_init = self._factory.repo_libgit2(wire)
222 with repo_init as repo:
222 with repo_init as repo:
223 blob = repo[sha]
223 blob = repo[sha]
224 return blob.size
224 return blob.size
225
225
226 return _blob_raw_length(repo_id, sha)
226 return _blob_raw_length(repo_id, sha)
227
227
228 def _parse_lfs_pointer(self, raw_content):
228 def _parse_lfs_pointer(self, raw_content):
229 spec_string = b'version https://git-lfs.github.com/spec'
229 spec_string = b'version https://git-lfs.github.com/spec'
230 if raw_content and raw_content.startswith(spec_string):
230 if raw_content and raw_content.startswith(spec_string):
231
231
232 pattern = re.compile(rb"""
232 pattern = re.compile(rb"""
233 (?:\n)?
233 (?:\n)?
234 ^version[ ]https://git-lfs\.github\.com/spec/(?P<spec_ver>v\d+)\n
234 ^version[ ]https://git-lfs\.github\.com/spec/(?P<spec_ver>v\d+)\n
235 ^oid[ ] sha256:(?P<oid_hash>[0-9a-f]{64})\n
235 ^oid[ ] sha256:(?P<oid_hash>[0-9a-f]{64})\n
236 ^size[ ](?P<oid_size>[0-9]+)\n
236 ^size[ ](?P<oid_size>[0-9]+)\n
237 (?:\n)?
237 (?:\n)?
238 """, re.VERBOSE | re.MULTILINE)
238 """, re.VERBOSE | re.MULTILINE)
239 match = pattern.match(raw_content)
239 match = pattern.match(raw_content)
240 if match:
240 if match:
241 return match.groupdict()
241 return match.groupdict()
242
242
243 return {}
243 return {}
244
244
245 @reraise_safe_exceptions
245 @reraise_safe_exceptions
246 def is_large_file(self, wire, commit_id):
246 def is_large_file(self, wire, commit_id):
247 cache_on, context_uid, repo_id = self._cache_on(wire)
247 cache_on, context_uid, repo_id = self._cache_on(wire)
248 region = self._region(wire)
248 region = self._region(wire)
249
249
250 @region.conditional_cache_on_arguments(condition=cache_on)
250 @region.conditional_cache_on_arguments(condition=cache_on)
251 def _is_large_file(_repo_id, _sha):
251 def _is_large_file(_repo_id, _sha):
252 repo_init = self._factory.repo_libgit2(wire)
252 repo_init = self._factory.repo_libgit2(wire)
253 with repo_init as repo:
253 with repo_init as repo:
254 blob = repo[commit_id]
254 blob = repo[commit_id]
255 if blob.is_binary:
255 if blob.is_binary:
256 return {}
256 return {}
257
257
258 return self._parse_lfs_pointer(blob.data)
258 return self._parse_lfs_pointer(blob.data)
259
259
260 return _is_large_file(repo_id, commit_id)
260 return _is_large_file(repo_id, commit_id)
261
261
262 @reraise_safe_exceptions
262 @reraise_safe_exceptions
263 def is_binary(self, wire, tree_id):
263 def is_binary(self, wire, tree_id):
264 cache_on, context_uid, repo_id = self._cache_on(wire)
264 cache_on, context_uid, repo_id = self._cache_on(wire)
265 region = self._region(wire)
265 region = self._region(wire)
266
266
267 @region.conditional_cache_on_arguments(condition=cache_on)
267 @region.conditional_cache_on_arguments(condition=cache_on)
268 def _is_binary(_repo_id, _tree_id):
268 def _is_binary(_repo_id, _tree_id):
269 repo_init = self._factory.repo_libgit2(wire)
269 repo_init = self._factory.repo_libgit2(wire)
270 with repo_init as repo:
270 with repo_init as repo:
271 blob_obj = repo[tree_id]
271 blob_obj = repo[tree_id]
272 return blob_obj.is_binary
272 return blob_obj.is_binary
273
273
274 return _is_binary(repo_id, tree_id)
274 return _is_binary(repo_id, tree_id)
275
275
276 @reraise_safe_exceptions
276 @reraise_safe_exceptions
277 def md5_hash(self, wire, tree_id):
277 def md5_hash(self, wire, tree_id):
278 cache_on, context_uid, repo_id = self._cache_on(wire)
278 cache_on, context_uid, repo_id = self._cache_on(wire)
279 region = self._region(wire)
279 region = self._region(wire)
280
280
281 @region.conditional_cache_on_arguments(condition=cache_on)
281 @region.conditional_cache_on_arguments(condition=cache_on)
282 def _md5_hash(_repo_id, _tree_id):
282 def _md5_hash(_repo_id, _tree_id):
283 return ''
283 return ''
284
284
285 return _md5_hash(repo_id, tree_id)
285 return _md5_hash(repo_id, tree_id)
286
286
287 @reraise_safe_exceptions
287 @reraise_safe_exceptions
288 def in_largefiles_store(self, wire, oid):
288 def in_largefiles_store(self, wire, oid):
289 conf = self._wire_to_config(wire)
289 conf = self._wire_to_config(wire)
290 repo_init = self._factory.repo_libgit2(wire)
290 repo_init = self._factory.repo_libgit2(wire)
291 with repo_init as repo:
291 with repo_init as repo:
292 repo_name = repo.path
292 repo_name = repo.path
293
293
294 store_location = conf.get('vcs_git_lfs_store_location')
294 store_location = conf.get('vcs_git_lfs_store_location')
295 if store_location:
295 if store_location:
296
296
297 store = LFSOidStore(
297 store = LFSOidStore(
298 oid=oid, repo=repo_name, store_location=store_location)
298 oid=oid, repo=repo_name, store_location=store_location)
299 return store.has_oid()
299 return store.has_oid()
300
300
301 return False
301 return False
302
302
303 @reraise_safe_exceptions
303 @reraise_safe_exceptions
304 def store_path(self, wire, oid):
304 def store_path(self, wire, oid):
305 conf = self._wire_to_config(wire)
305 conf = self._wire_to_config(wire)
306 repo_init = self._factory.repo_libgit2(wire)
306 repo_init = self._factory.repo_libgit2(wire)
307 with repo_init as repo:
307 with repo_init as repo:
308 repo_name = repo.path
308 repo_name = repo.path
309
309
310 store_location = conf.get('vcs_git_lfs_store_location')
310 store_location = conf.get('vcs_git_lfs_store_location')
311 if store_location:
311 if store_location:
312 store = LFSOidStore(
312 store = LFSOidStore(
313 oid=oid, repo=repo_name, store_location=store_location)
313 oid=oid, repo=repo_name, store_location=store_location)
314 return store.oid_path
314 return store.oid_path
315 raise ValueError('Unable to fetch oid with path {}'.format(oid))
315 raise ValueError('Unable to fetch oid with path {}'.format(oid))
316
316
317 @reraise_safe_exceptions
317 @reraise_safe_exceptions
318 def bulk_request(self, wire, rev, pre_load):
318 def bulk_request(self, wire, rev, pre_load):
319 cache_on, context_uid, repo_id = self._cache_on(wire)
319 cache_on, context_uid, repo_id = self._cache_on(wire)
320 region = self._region(wire)
320 region = self._region(wire)
321
321
322 @region.conditional_cache_on_arguments(condition=cache_on)
322 @region.conditional_cache_on_arguments(condition=cache_on)
323 def _bulk_request(_repo_id, _rev, _pre_load):
323 def _bulk_request(_repo_id, _rev, _pre_load):
324 result = {}
324 result = {}
325 for attr in pre_load:
325 for attr in pre_load:
326 try:
326 try:
327 method = self._bulk_methods[attr]
327 method = self._bulk_methods[attr]
328 wire.update({'cache': False}) # disable cache for bulk calls so we don't double cache
328 wire.update({'cache': False}) # disable cache for bulk calls so we don't double cache
329 args = [wire, rev]
329 args = [wire, rev]
330 result[attr] = method(*args)
330 result[attr] = method(*args)
331 except KeyError as e:
331 except KeyError as e:
332 raise exceptions.VcsException(e)(f"Unknown bulk attribute: {attr}")
332 raise exceptions.VcsException(e)(f"Unknown bulk attribute: {attr}")
333 return result
333 return result
334
334
335 return _bulk_request(repo_id, rev, sorted(pre_load))
335 return _bulk_request(repo_id, rev, sorted(pre_load))
336
336
337 def _build_opener(self, url):
337 def _build_opener(self, url):
338 handlers = []
338 handlers = []
339 url_obj = url_parser(url)
339 url_obj = url_parser(url)
340 _, authinfo = url_obj.authinfo()
340 _, authinfo = url_obj.authinfo()
341
341
342 if authinfo:
342 if authinfo:
343 # create a password manager
343 # create a password manager
344 passmgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()
344 passmgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()
345 passmgr.add_password(*authinfo)
345 passmgr.add_password(*authinfo)
346
346
347 handlers.extend((httpbasicauthhandler(passmgr),
347 handlers.extend((httpbasicauthhandler(passmgr),
348 httpdigestauthhandler(passmgr)))
348 httpdigestauthhandler(passmgr)))
349
349
350 return urllib.request.build_opener(*handlers)
350 return urllib.request.build_opener(*handlers)
351
351
352 def _type_id_to_name(self, type_id: int):
352 def _type_id_to_name(self, type_id: int):
353 return {
353 return {
354 1: 'commit',
354 1: 'commit',
355 2: 'tree',
355 2: 'tree',
356 3: 'blob',
356 3: 'blob',
357 4: 'tag'
357 4: 'tag'
358 }[type_id]
358 }[type_id]
359
359
360 @reraise_safe_exceptions
360 @reraise_safe_exceptions
361 def check_url(self, url, config):
361 def check_url(self, url, config):
362 url_obj = url_parser(safe_bytes(url))
362 url_obj = url_parser(safe_bytes(url))
363 test_uri, _ = url_obj.authinfo()
363 test_uri, _ = url_obj.authinfo()
364 url_obj.passwd = '*****' if url_obj.passwd else url_obj.passwd
364 url_obj.passwd = '*****' if url_obj.passwd else url_obj.passwd
365 url_obj.query = obfuscate_qs(url_obj.query)
365 url_obj.query = obfuscate_qs(url_obj.query)
366 cleaned_uri = str(url_obj)
366 cleaned_uri = str(url_obj)
367 log.info("Checking URL for remote cloning/import: %s", cleaned_uri)
367 log.info("Checking URL for remote cloning/import: %s", cleaned_uri)
368
368
369 if not test_uri.endswith('info/refs'):
369 if not test_uri.endswith('info/refs'):
370 test_uri = test_uri.rstrip('/') + '/info/refs'
370 test_uri = test_uri.rstrip('/') + '/info/refs'
371
371
372 o = self._build_opener(url)
372 o = self._build_opener(url)
373 o.addheaders = [('User-Agent', 'git/1.7.8.0')] # fake some git
373 o.addheaders = [('User-Agent', 'git/1.7.8.0')] # fake some git
374
374
375 q = {"service": 'git-upload-pack'}
375 q = {"service": 'git-upload-pack'}
376 qs = '?%s' % urllib.parse.urlencode(q)
376 qs = '?%s' % urllib.parse.urlencode(q)
377 cu = "%s%s" % (test_uri, qs)
377 cu = "%s%s" % (test_uri, qs)
378 req = urllib.request.Request(cu, None, {})
378 req = urllib.request.Request(cu, None, {})
379
379
380 try:
380 try:
381 log.debug("Trying to open URL %s", cleaned_uri)
381 log.debug("Trying to open URL %s", cleaned_uri)
382 resp = o.open(req)
382 resp = o.open(req)
383 if resp.code != 200:
383 if resp.code != 200:
384 raise exceptions.URLError()('Return Code is not 200')
384 raise exceptions.URLError()('Return Code is not 200')
385 except Exception as e:
385 except Exception as e:
386 log.warning("URL cannot be opened: %s", cleaned_uri, exc_info=True)
386 log.warning("URL cannot be opened: %s", cleaned_uri, exc_info=True)
387 # means it cannot be cloned
387 # means it cannot be cloned
388 raise exceptions.URLError(e)("[%s] org_exc: %s" % (cleaned_uri, e))
388 raise exceptions.URLError(e)("[%s] org_exc: %s" % (cleaned_uri, e))
389
389
390 # now detect if it's proper git repo
390 # now detect if it's proper git repo
391 gitdata = resp.read()
391 gitdata = resp.read()
392 if 'service=git-upload-pack' in gitdata:
392 if 'service=git-upload-pack' in gitdata:
393 pass
393 pass
394 elif re.findall(r'[0-9a-fA-F]{40}\s+refs', gitdata):
394 elif re.findall(r'[0-9a-fA-F]{40}\s+refs', gitdata):
395 # old style git can return some other format !
395 # old style git can return some other format !
396 pass
396 pass
397 else:
397 else:
398 raise exceptions.URLError()(
398 raise exceptions.URLError()(
399 "url [%s] does not look like an git" % (cleaned_uri,))
399 "url [%s] does not look like an git" % (cleaned_uri,))
400
400
401 return True
401 return True
402
402
403 @reraise_safe_exceptions
403 @reraise_safe_exceptions
404 def clone(self, wire, url, deferred, valid_refs, update_after_clone):
404 def clone(self, wire, url, deferred, valid_refs, update_after_clone):
405 # TODO(marcink): deprecate this method. Last i checked we don't use it anymore
405 # TODO(marcink): deprecate this method. Last i checked we don't use it anymore
406 remote_refs = self.pull(wire, url, apply_refs=False)
406 remote_refs = self.pull(wire, url, apply_refs=False)
407 repo = self._factory.repo(wire)
407 repo = self._factory.repo(wire)
408 if isinstance(valid_refs, list):
408 if isinstance(valid_refs, list):
409 valid_refs = tuple(valid_refs)
409 valid_refs = tuple(valid_refs)
410
410
411 for k in remote_refs:
411 for k in remote_refs:
412 # only parse heads/tags and skip so called deferred tags
412 # only parse heads/tags and skip so called deferred tags
413 if k.startswith(valid_refs) and not k.endswith(deferred):
413 if k.startswith(valid_refs) and not k.endswith(deferred):
414 repo[k] = remote_refs[k]
414 repo[k] = remote_refs[k]
415
415
416 if update_after_clone:
416 if update_after_clone:
417 # we want to checkout HEAD
417 # we want to checkout HEAD
418 repo["HEAD"] = remote_refs["HEAD"]
418 repo["HEAD"] = remote_refs["HEAD"]
419 index.build_index_from_tree(repo.path, repo.index_path(),
419 index.build_index_from_tree(repo.path, repo.index_path(),
420 repo.object_store, repo["HEAD"].tree)
420 repo.object_store, repo["HEAD"].tree)
421
421
422 @reraise_safe_exceptions
422 @reraise_safe_exceptions
423 def branch(self, wire, commit_id):
423 def branch(self, wire, commit_id):
424 cache_on, context_uid, repo_id = self._cache_on(wire)
424 cache_on, context_uid, repo_id = self._cache_on(wire)
425 region = self._region(wire)
425 region = self._region(wire)
426 @region.conditional_cache_on_arguments(condition=cache_on)
426 @region.conditional_cache_on_arguments(condition=cache_on)
427 def _branch(_context_uid, _repo_id, _commit_id):
427 def _branch(_context_uid, _repo_id, _commit_id):
428 regex = re.compile('^refs/heads')
428 regex = re.compile('^refs/heads')
429
429
430 def filter_with(ref):
430 def filter_with(ref):
431 return regex.match(ref[0]) and ref[1] == _commit_id
431 return regex.match(ref[0]) and ref[1] == _commit_id
432
432
433 branches = list(filter(filter_with, list(self.get_refs(wire).items())))
433 branches = list(filter(filter_with, list(self.get_refs(wire).items())))
434 return [x[0].split('refs/heads/')[-1] for x in branches]
434 return [x[0].split('refs/heads/')[-1] for x in branches]
435
435
436 return _branch(context_uid, repo_id, commit_id)
436 return _branch(context_uid, repo_id, commit_id)
437
437
438 @reraise_safe_exceptions
438 @reraise_safe_exceptions
439 def commit_branches(self, wire, commit_id):
439 def commit_branches(self, wire, commit_id):
440 cache_on, context_uid, repo_id = self._cache_on(wire)
440 cache_on, context_uid, repo_id = self._cache_on(wire)
441 region = self._region(wire)
441 region = self._region(wire)
442 @region.conditional_cache_on_arguments(condition=cache_on)
442 @region.conditional_cache_on_arguments(condition=cache_on)
443 def _commit_branches(_context_uid, _repo_id, _commit_id):
443 def _commit_branches(_context_uid, _repo_id, _commit_id):
444 repo_init = self._factory.repo_libgit2(wire)
444 repo_init = self._factory.repo_libgit2(wire)
445 with repo_init as repo:
445 with repo_init as repo:
446 branches = [x for x in repo.branches.with_commit(_commit_id)]
446 branches = [x for x in repo.branches.with_commit(_commit_id)]
447 return branches
447 return branches
448
448
449 return _commit_branches(context_uid, repo_id, commit_id)
449 return _commit_branches(context_uid, repo_id, commit_id)
450
450
451 @reraise_safe_exceptions
451 @reraise_safe_exceptions
452 def add_object(self, wire, content):
452 def add_object(self, wire, content):
453 repo_init = self._factory.repo_libgit2(wire)
453 repo_init = self._factory.repo_libgit2(wire)
454 with repo_init as repo:
454 with repo_init as repo:
455 blob = objects.Blob()
455 blob = objects.Blob()
456 blob.set_raw_string(content)
456 blob.set_raw_string(content)
457 repo.object_store.add_object(blob)
457 repo.object_store.add_object(blob)
458 return blob.id
458 return blob.id
459
459
460 # TODO: this is quite complex, check if that can be simplified
460 # TODO: this is quite complex, check if that can be simplified
461 @reraise_safe_exceptions
461 @reraise_safe_exceptions
462 def commit(self, wire, commit_data, branch, commit_tree, updated, removed):
462 def commit(self, wire, commit_data, branch, commit_tree, updated, removed):
463 # Defines the root tree
463 # Defines the root tree
464 class _Root(object):
464 class _Root(object):
465 def __repr__(self):
465 def __repr__(self):
466 return 'ROOT TREE'
466 return 'ROOT TREE'
467 ROOT = _Root()
467 ROOT = _Root()
468
468
469 repo = self._factory.repo(wire)
469 repo = self._factory.repo(wire)
470 object_store = repo.object_store
470 object_store = repo.object_store
471
471
472 # Create tree and populates it with blobs
472 # Create tree and populates it with blobs
473 if commit_tree:
473 if commit_tree:
474 commit_tree = safe_bytes(commit_tree)
474 commit_tree = safe_bytes(commit_tree)
475
475
476 if commit_tree and repo[commit_tree]:
476 if commit_tree and repo[commit_tree]:
477 git_commit = repo[safe_bytes(commit_data['parents'][0])]
477 git_commit = repo[safe_bytes(commit_data['parents'][0])]
478 commit_tree = repo[git_commit.tree] # root tree
478 commit_tree = repo[git_commit.tree] # root tree
479 else:
479 else:
480 commit_tree = objects.Tree()
480 commit_tree = objects.Tree()
481
481
482 for node in updated:
482 for node in updated:
483 # Compute subdirs if needed
483 # Compute subdirs if needed
484 dirpath, nodename = vcspath.split(node['path'])
484 dirpath, nodename = vcspath.split(node['path'])
485 dirnames = list(map(safe_str, dirpath and dirpath.split('/') or []))
485 dirnames = list(map(safe_str, dirpath and dirpath.split('/') or []))
486 parent = commit_tree
486 parent = commit_tree
487 ancestors = [('', parent)]
487 ancestors = [('', parent)]
488
488
489 # Tries to dig for the deepest existing tree
489 # Tries to dig for the deepest existing tree
490 while dirnames:
490 while dirnames:
491 curdir = dirnames.pop(0)
491 curdir = dirnames.pop(0)
492 try:
492 try:
493 dir_id = parent[curdir][1]
493 dir_id = parent[curdir][1]
494 except KeyError:
494 except KeyError:
495 # put curdir back into dirnames and stops
495 # put curdir back into dirnames and stops
496 dirnames.insert(0, curdir)
496 dirnames.insert(0, curdir)
497 break
497 break
498 else:
498 else:
499 # If found, updates parent
499 # If found, updates parent
500 parent = repo[dir_id]
500 parent = repo[dir_id]
501 ancestors.append((curdir, parent))
501 ancestors.append((curdir, parent))
502 # Now parent is deepest existing tree and we need to create
502 # Now parent is deepest existing tree and we need to create
503 # subtrees for dirnames (in reverse order)
503 # subtrees for dirnames (in reverse order)
504 # [this only applies for nodes from added]
504 # [this only applies for nodes from added]
505 new_trees = []
505 new_trees = []
506
506
507 blob = objects.Blob.from_string(node['content'])
507 blob = objects.Blob.from_string(node['content'])
508
508
509 if dirnames:
509 if dirnames:
510 # If there are trees which should be created we need to build
510 # If there are trees which should be created we need to build
511 # them now (in reverse order)
511 # them now (in reverse order)
512 reversed_dirnames = list(reversed(dirnames))
512 reversed_dirnames = list(reversed(dirnames))
513 curtree = objects.Tree()
513 curtree = objects.Tree()
514 curtree[node['node_path']] = node['mode'], blob.id
514 curtree[node['node_path']] = node['mode'], blob.id
515 new_trees.append(curtree)
515 new_trees.append(curtree)
516 for dirname in reversed_dirnames[:-1]:
516 for dirname in reversed_dirnames[:-1]:
517 newtree = objects.Tree()
517 newtree = objects.Tree()
518 newtree[dirname] = (DIR_STAT, curtree.id)
518 newtree[dirname] = (DIR_STAT, curtree.id)
519 new_trees.append(newtree)
519 new_trees.append(newtree)
520 curtree = newtree
520 curtree = newtree
521 parent[reversed_dirnames[-1]] = (DIR_STAT, curtree.id)
521 parent[reversed_dirnames[-1]] = (DIR_STAT, curtree.id)
522 else:
522 else:
523 parent.add(name=node['node_path'], mode=node['mode'], hexsha=blob.id)
523 parent.add(name=node['node_path'], mode=node['mode'], hexsha=blob.id)
524
524
525 new_trees.append(parent)
525 new_trees.append(parent)
526 # Update ancestors
526 # Update ancestors
527 reversed_ancestors = reversed(
527 reversed_ancestors = reversed(
528 [(a[1], b[1], b[0]) for a, b in zip(ancestors, ancestors[1:])])
528 [(a[1], b[1], b[0]) for a, b in zip(ancestors, ancestors[1:])])
529 for parent, tree, path in reversed_ancestors:
529 for parent, tree, path in reversed_ancestors:
530 parent[path] = (DIR_STAT, tree.id)
530 parent[path] = (DIR_STAT, tree.id)
531 object_store.add_object(tree)
531 object_store.add_object(tree)
532
532
533 object_store.add_object(blob)
533 object_store.add_object(blob)
534 for tree in new_trees:
534 for tree in new_trees:
535 object_store.add_object(tree)
535 object_store.add_object(tree)
536
536
537 for node_path in removed:
537 for node_path in removed:
538 paths = node_path.split('/')
538 paths = node_path.split('/')
539 tree = commit_tree # start with top-level
539 tree = commit_tree # start with top-level
540 trees = [{'tree': tree, 'path': ROOT}]
540 trees = [{'tree': tree, 'path': ROOT}]
541 # Traverse deep into the forest...
541 # Traverse deep into the forest...
542 # resolve final tree by iterating the path.
542 # resolve final tree by iterating the path.
543 # e.g a/b/c.txt will get
543 # e.g a/b/c.txt will get
544 # - root as tree then
544 # - root as tree then
545 # - 'a' as tree,
545 # - 'a' as tree,
546 # - 'b' as tree,
546 # - 'b' as tree,
547 # - stop at c as blob.
547 # - stop at c as blob.
548 for path in paths:
548 for path in paths:
549 try:
549 try:
550 obj = repo[tree[path][1]]
550 obj = repo[tree[path][1]]
551 if isinstance(obj, objects.Tree):
551 if isinstance(obj, objects.Tree):
552 trees.append({'tree': obj, 'path': path})
552 trees.append({'tree': obj, 'path': path})
553 tree = obj
553 tree = obj
554 except KeyError:
554 except KeyError:
555 break
555 break
556 #PROBLEM:
556 #PROBLEM:
557 """
557 """
558 We're not editing same reference tree object
558 We're not editing same reference tree object
559 """
559 """
560 # Cut down the blob and all rotten trees on the way back...
560 # Cut down the blob and all rotten trees on the way back...
561 for path, tree_data in reversed(list(zip(paths, trees))):
561 for path, tree_data in reversed(list(zip(paths, trees))):
562 tree = tree_data['tree']
562 tree = tree_data['tree']
563 tree.__delitem__(path)
563 tree.__delitem__(path)
564 # This operation edits the tree, we need to mark new commit back
564 # This operation edits the tree, we need to mark new commit back
565
565
566 if len(tree) > 0:
566 if len(tree) > 0:
567 # This tree still has elements - don't remove it or any
567 # This tree still has elements - don't remove it or any
568 # of it's parents
568 # of it's parents
569 break
569 break
570
570
571 object_store.add_object(commit_tree)
571 object_store.add_object(commit_tree)
572
572
573 # Create commit
573 # Create commit
574 commit = objects.Commit()
574 commit = objects.Commit()
575 commit.tree = commit_tree.id
575 commit.tree = commit_tree.id
576 bytes_keys = [
576 bytes_keys = [
577 'author',
577 'author',
578 'committer',
578 'committer',
579 'message',
579 'message',
580 'encoding',
580 'encoding',
581 'parents'
581 'parents'
582 ]
582 ]
583
583
584 for k, v in commit_data.items():
584 for k, v in commit_data.items():
585 if k in bytes_keys:
585 if k in bytes_keys:
586 if k == 'parents':
586 if k == 'parents':
587 v = [safe_bytes(x) for x in v]
587 v = [safe_bytes(x) for x in v]
588 else:
588 else:
589 v = safe_bytes(v)
589 v = safe_bytes(v)
590 setattr(commit, k, v)
590 setattr(commit, k, v)
591
591
592 object_store.add_object(commit)
592 object_store.add_object(commit)
593
593
594 self.create_branch(wire, branch, safe_str(commit.id))
594 self.create_branch(wire, branch, safe_str(commit.id))
595
595
596 # dulwich set-ref
596 # dulwich set-ref
597 repo.refs[safe_bytes(f'refs/heads/{branch}')] = commit.id
597 repo.refs[safe_bytes(f'refs/heads/{branch}')] = commit.id
598
598
599 return commit.id
599 return commit.id
600
600
601 @reraise_safe_exceptions
601 @reraise_safe_exceptions
602 def pull(self, wire, url, apply_refs=True, refs=None, update_after=False):
602 def pull(self, wire, url, apply_refs=True, refs=None, update_after=False):
603 if url != 'default' and '://' not in url:
603 if url != 'default' and '://' not in url:
604 client = LocalGitClient(url)
604 client = LocalGitClient(url)
605 else:
605 else:
606 url_obj = url_parser(url)
606 url_obj = url_parser(url)
607 o = self._build_opener(url)
607 o = self._build_opener(url)
608 url, _ = url_obj.authinfo()
608 url, _ = url_obj.authinfo()
609 client = HttpGitClient(base_url=url, opener=o)
609 client = HttpGitClient(base_url=url, opener=o)
610 repo = self._factory.repo(wire)
610 repo = self._factory.repo(wire)
611
611
612 determine_wants = repo.object_store.determine_wants_all
612 determine_wants = repo.object_store.determine_wants_all
613 if refs:
613 if refs:
614 refs = [ascii_bytes(x) for x in refs]
614 refs = [ascii_bytes(x) for x in refs]
615
615
616 def determine_wants_requested(remote_refs):
616 def determine_wants_requested(remote_refs):
617 determined = []
617 determined = []
618 for ref_name, ref_hash in remote_refs.items():
618 for ref_name, ref_hash in remote_refs.items():
619 bytes_ref_name = safe_bytes(ref_name)
619 bytes_ref_name = safe_bytes(ref_name)
620
620
621 if bytes_ref_name in refs:
621 if bytes_ref_name in refs:
622 bytes_ref_hash = safe_bytes(ref_hash)
622 bytes_ref_hash = safe_bytes(ref_hash)
623 determined.append(bytes_ref_hash)
623 determined.append(bytes_ref_hash)
624 return determined
624 return determined
625
625
626 # swap with our custom requested wants
626 # swap with our custom requested wants
627 determine_wants = determine_wants_requested
627 determine_wants = determine_wants_requested
628
628
629 try:
629 try:
630 remote_refs = client.fetch(
630 remote_refs = client.fetch(
631 path=url, target=repo, determine_wants=determine_wants)
631 path=url, target=repo, determine_wants=determine_wants)
632
632
633 except NotGitRepository as e:
633 except NotGitRepository as e:
634 log.warning(
634 log.warning(
635 'Trying to fetch from "%s" failed, not a Git repository.', url)
635 'Trying to fetch from "%s" failed, not a Git repository.', url)
636 # Exception can contain unicode which we convert
636 # Exception can contain unicode which we convert
637 raise exceptions.AbortException(e)(repr(e))
637 raise exceptions.AbortException(e)(repr(e))
638
638
639 # mikhail: client.fetch() returns all the remote refs, but fetches only
639 # mikhail: client.fetch() returns all the remote refs, but fetches only
640 # refs filtered by `determine_wants` function. We need to filter result
640 # refs filtered by `determine_wants` function. We need to filter result
641 # as well
641 # as well
642 if refs:
642 if refs:
643 remote_refs = {k: remote_refs[k] for k in remote_refs if k in refs}
643 remote_refs = {k: remote_refs[k] for k in remote_refs if k in refs}
644
644
645 if apply_refs:
645 if apply_refs:
646 # TODO: johbo: Needs proper test coverage with a git repository
646 # TODO: johbo: Needs proper test coverage with a git repository
647 # that contains a tag object, so that we would end up with
647 # that contains a tag object, so that we would end up with
648 # a peeled ref at this point.
648 # a peeled ref at this point.
649 for k in remote_refs:
649 for k in remote_refs:
650 if k.endswith(PEELED_REF_MARKER):
650 if k.endswith(PEELED_REF_MARKER):
651 log.debug("Skipping peeled reference %s", k)
651 log.debug("Skipping peeled reference %s", k)
652 continue
652 continue
653 repo[k] = remote_refs[k]
653 repo[k] = remote_refs[k]
654
654
655 if refs and not update_after:
655 if refs and not update_after:
656 # mikhail: explicitly set the head to the last ref.
656 # mikhail: explicitly set the head to the last ref.
657 repo[HEAD_MARKER] = remote_refs[refs[-1]]
657 repo[HEAD_MARKER] = remote_refs[refs[-1]]
658
658
659 if update_after:
659 if update_after:
660 # we want to checkout HEAD
660 # we want to checkout HEAD
661 repo[HEAD_MARKER] = remote_refs[HEAD_MARKER]
661 repo[HEAD_MARKER] = remote_refs[HEAD_MARKER]
662 index.build_index_from_tree(repo.path, repo.index_path(),
662 index.build_index_from_tree(repo.path, repo.index_path(),
663 repo.object_store, repo[HEAD_MARKER].tree)
663 repo.object_store, repo[HEAD_MARKER].tree)
664 return remote_refs
664 return remote_refs
665
665
666 @reraise_safe_exceptions
666 @reraise_safe_exceptions
667 def sync_fetch(self, wire, url, refs=None, all_refs=False):
667 def sync_fetch(self, wire, url, refs=None, all_refs=False):
668 repo = self._factory.repo(wire)
668 repo = self._factory.repo(wire)
669 if refs and not isinstance(refs, (list, tuple)):
669 if refs and not isinstance(refs, (list, tuple)):
670 refs = [refs]
670 refs = [refs]
671
671
672 config = self._wire_to_config(wire)
672 config = self._wire_to_config(wire)
673 # get all remote refs we'll use to fetch later
673 # get all remote refs we'll use to fetch later
674 cmd = ['ls-remote']
674 cmd = ['ls-remote']
675 if not all_refs:
675 if not all_refs:
676 cmd += ['--heads', '--tags']
676 cmd += ['--heads', '--tags']
677 cmd += [url]
677 cmd += [url]
678 output, __ = self.run_git_command(
678 output, __ = self.run_git_command(
679 wire, cmd, fail_on_stderr=False,
679 wire, cmd, fail_on_stderr=False,
680 _copts=self._remote_conf(config),
680 _copts=self._remote_conf(config),
681 extra_env={'GIT_TERMINAL_PROMPT': '0'})
681 extra_env={'GIT_TERMINAL_PROMPT': '0'})
682
682
683 remote_refs = collections.OrderedDict()
683 remote_refs = collections.OrderedDict()
684 fetch_refs = []
684 fetch_refs = []
685
685
686 for ref_line in output.splitlines():
686 for ref_line in output.splitlines():
687 sha, ref = ref_line.split(b'\t')
687 sha, ref = ref_line.split(b'\t')
688 sha = sha.strip()
688 sha = sha.strip()
689 if ref in remote_refs:
689 if ref in remote_refs:
690 # duplicate, skip
690 # duplicate, skip
691 continue
691 continue
692 if ref.endswith(PEELED_REF_MARKER):
692 if ref.endswith(PEELED_REF_MARKER):
693 log.debug("Skipping peeled reference %s", ref)
693 log.debug("Skipping peeled reference %s", ref)
694 continue
694 continue
695 # don't sync HEAD
695 # don't sync HEAD
696 if ref in [HEAD_MARKER]:
696 if ref in [HEAD_MARKER]:
697 continue
697 continue
698
698
699 remote_refs[ref] = sha
699 remote_refs[ref] = sha
700
700
701 if refs and sha in refs:
701 if refs and sha in refs:
702 # we filter fetch using our specified refs
702 # we filter fetch using our specified refs
703 fetch_refs.append(f'{safe_str(ref)}:{safe_str(ref)}')
703 fetch_refs.append(f'{safe_str(ref)}:{safe_str(ref)}')
704 elif not refs:
704 elif not refs:
705 fetch_refs.append(f'{safe_str(ref)}:{safe_str(ref)}')
705 fetch_refs.append(f'{safe_str(ref)}:{safe_str(ref)}')
706 log.debug('Finished obtaining fetch refs, total: %s', len(fetch_refs))
706 log.debug('Finished obtaining fetch refs, total: %s', len(fetch_refs))
707
707
708 if fetch_refs:
708 if fetch_refs:
709 for chunk in more_itertools.chunked(fetch_refs, 1024 * 4):
709 for chunk in more_itertools.chunked(fetch_refs, 1024 * 4):
710 fetch_refs_chunks = list(chunk)
710 fetch_refs_chunks = list(chunk)
711 log.debug('Fetching %s refs from import url', len(fetch_refs_chunks))
711 log.debug('Fetching %s refs from import url', len(fetch_refs_chunks))
712 self.run_git_command(
712 self.run_git_command(
713 wire, ['fetch', url, '--force', '--prune', '--'] + fetch_refs_chunks,
713 wire, ['fetch', url, '--force', '--prune', '--'] + fetch_refs_chunks,
714 fail_on_stderr=False,
714 fail_on_stderr=False,
715 _copts=self._remote_conf(config),
715 _copts=self._remote_conf(config),
716 extra_env={'GIT_TERMINAL_PROMPT': '0'})
716 extra_env={'GIT_TERMINAL_PROMPT': '0'})
717
717
718 return remote_refs
718 return remote_refs
719
719
720 @reraise_safe_exceptions
720 @reraise_safe_exceptions
721 def sync_push(self, wire, url, refs=None):
721 def sync_push(self, wire, url, refs=None):
722 if not self.check_url(url, wire):
722 if not self.check_url(url, wire):
723 return
723 return
724 config = self._wire_to_config(wire)
724 config = self._wire_to_config(wire)
725 self._factory.repo(wire)
725 self._factory.repo(wire)
726 self.run_git_command(
726 self.run_git_command(
727 wire, ['push', url, '--mirror'], fail_on_stderr=False,
727 wire, ['push', url, '--mirror'], fail_on_stderr=False,
728 _copts=self._remote_conf(config),
728 _copts=self._remote_conf(config),
729 extra_env={'GIT_TERMINAL_PROMPT': '0'})
729 extra_env={'GIT_TERMINAL_PROMPT': '0'})
730
730
731 @reraise_safe_exceptions
731 @reraise_safe_exceptions
732 def get_remote_refs(self, wire, url):
732 def get_remote_refs(self, wire, url):
733 repo = Repo(url)
733 repo = Repo(url)
734 return repo.get_refs()
734 return repo.get_refs()
735
735
736 @reraise_safe_exceptions
736 @reraise_safe_exceptions
737 def get_description(self, wire):
737 def get_description(self, wire):
738 repo = self._factory.repo(wire)
738 repo = self._factory.repo(wire)
739 return repo.get_description()
739 return repo.get_description()
740
740
741 @reraise_safe_exceptions
741 @reraise_safe_exceptions
742 def get_missing_revs(self, wire, rev1, rev2, path2):
742 def get_missing_revs(self, wire, rev1, rev2, path2):
743 repo = self._factory.repo(wire)
743 repo = self._factory.repo(wire)
744 LocalGitClient(thin_packs=False).fetch(path2, repo)
744 LocalGitClient(thin_packs=False).fetch(path2, repo)
745
745
746 wire_remote = wire.copy()
746 wire_remote = wire.copy()
747 wire_remote['path'] = path2
747 wire_remote['path'] = path2
748 repo_remote = self._factory.repo(wire_remote)
748 repo_remote = self._factory.repo(wire_remote)
749 LocalGitClient(thin_packs=False).fetch(wire["path"], repo_remote)
749 LocalGitClient(thin_packs=False).fetch(wire["path"], repo_remote)
750
750
751 revs = [
751 revs = [
752 x.commit.id
752 x.commit.id
753 for x in repo_remote.get_walker(include=[rev2], exclude=[rev1])]
753 for x in repo_remote.get_walker(include=[rev2], exclude=[rev1])]
754 return revs
754 return revs
755
755
756 @reraise_safe_exceptions
756 @reraise_safe_exceptions
757 def get_object(self, wire, sha, maybe_unreachable=False):
757 def get_object(self, wire, sha, maybe_unreachable=False):
758 cache_on, context_uid, repo_id = self._cache_on(wire)
758 cache_on, context_uid, repo_id = self._cache_on(wire)
759 region = self._region(wire)
759 region = self._region(wire)
760
760
761 @region.conditional_cache_on_arguments(condition=cache_on)
761 @region.conditional_cache_on_arguments(condition=cache_on)
762 def _get_object(_context_uid, _repo_id, _sha):
762 def _get_object(_context_uid, _repo_id, _sha):
763 repo_init = self._factory.repo_libgit2(wire)
763 repo_init = self._factory.repo_libgit2(wire)
764 with repo_init as repo:
764 with repo_init as repo:
765
765
766 missing_commit_err = 'Commit {} does not exist for `{}`'.format(sha, wire['path'])
766 missing_commit_err = 'Commit {} does not exist for `{}`'.format(sha, wire['path'])
767 try:
767 try:
768 commit = repo.revparse_single(sha)
768 commit = repo.revparse_single(sha)
769 except KeyError:
769 except KeyError:
770 # NOTE(marcink): KeyError doesn't give us any meaningful information
770 # NOTE(marcink): KeyError doesn't give us any meaningful information
771 # here, we instead give something more explicit
771 # here, we instead give something more explicit
772 e = exceptions.RefNotFoundException('SHA: %s not found', sha)
772 e = exceptions.RefNotFoundException('SHA: %s not found', sha)
773 raise exceptions.LookupException(e)(missing_commit_err)
773 raise exceptions.LookupException(e)(missing_commit_err)
774 except ValueError as e:
774 except ValueError as e:
775 raise exceptions.LookupException(e)(missing_commit_err)
775 raise exceptions.LookupException(e)(missing_commit_err)
776
776
777 is_tag = False
777 is_tag = False
778 if isinstance(commit, pygit2.Tag):
778 if isinstance(commit, pygit2.Tag):
779 commit = repo.get(commit.target)
779 commit = repo.get(commit.target)
780 is_tag = True
780 is_tag = True
781
781
782 check_dangling = True
782 check_dangling = True
783 if is_tag:
783 if is_tag:
784 check_dangling = False
784 check_dangling = False
785
785
786 if check_dangling and maybe_unreachable:
786 if check_dangling and maybe_unreachable:
787 check_dangling = False
787 check_dangling = False
788
788
789 # we used a reference and it parsed means we're not having a dangling commit
789 # we used a reference and it parsed means we're not having a dangling commit
790 if sha != commit.hex:
790 if sha != commit.hex:
791 check_dangling = False
791 check_dangling = False
792
792
793 if check_dangling:
793 if check_dangling:
794 # check for dangling commit
794 # check for dangling commit
795 for branch in repo.branches.with_commit(commit.hex):
795 for branch in repo.branches.with_commit(commit.hex):
796 if branch:
796 if branch:
797 break
797 break
798 else:
798 else:
799 # NOTE(marcink): Empty error doesn't give us any meaningful information
799 # NOTE(marcink): Empty error doesn't give us any meaningful information
800 # here, we instead give something more explicit
800 # here, we instead give something more explicit
801 e = exceptions.RefNotFoundException('SHA: %s not found in branches', sha)
801 e = exceptions.RefNotFoundException('SHA: %s not found in branches', sha)
802 raise exceptions.LookupException(e)(missing_commit_err)
802 raise exceptions.LookupException(e)(missing_commit_err)
803
803
804 commit_id = commit.hex
804 commit_id = commit.hex
805 type_id = commit.type
805 type_id = commit.type
806
806
807 return {
807 return {
808 'id': commit_id,
808 'id': commit_id,
809 'type': self._type_id_to_name(type_id),
809 'type': self._type_id_to_name(type_id),
810 'commit_id': commit_id,
810 'commit_id': commit_id,
811 'idx': 0
811 'idx': 0
812 }
812 }
813
813
814 return _get_object(context_uid, repo_id, sha)
814 return _get_object(context_uid, repo_id, sha)
815
815
816 @reraise_safe_exceptions
816 @reraise_safe_exceptions
817 def get_refs(self, wire):
817 def get_refs(self, wire):
818 cache_on, context_uid, repo_id = self._cache_on(wire)
818 cache_on, context_uid, repo_id = self._cache_on(wire)
819 region = self._region(wire)
819 region = self._region(wire)
820
820
821 @region.conditional_cache_on_arguments(condition=cache_on)
821 @region.conditional_cache_on_arguments(condition=cache_on)
822 def _get_refs(_context_uid, _repo_id):
822 def _get_refs(_context_uid, _repo_id):
823
823
824 repo_init = self._factory.repo_libgit2(wire)
824 repo_init = self._factory.repo_libgit2(wire)
825 with repo_init as repo:
825 with repo_init as repo:
826 regex = re.compile('^refs/(heads|tags)/')
826 regex = re.compile('^refs/(heads|tags)/')
827 return {x.name: x.target.hex for x in
827 return {x.name: x.target.hex for x in
828 [ref for ref in repo.listall_reference_objects() if regex.match(ref.name)]}
828 [ref for ref in repo.listall_reference_objects() if regex.match(ref.name)]}
829
829
830 return _get_refs(context_uid, repo_id)
830 return _get_refs(context_uid, repo_id)
831
831
832 @reraise_safe_exceptions
832 @reraise_safe_exceptions
833 def get_branch_pointers(self, wire):
833 def get_branch_pointers(self, wire):
834 cache_on, context_uid, repo_id = self._cache_on(wire)
834 cache_on, context_uid, repo_id = self._cache_on(wire)
835 region = self._region(wire)
835 region = self._region(wire)
836
836
837 @region.conditional_cache_on_arguments(condition=cache_on)
837 @region.conditional_cache_on_arguments(condition=cache_on)
838 def _get_branch_pointers(_context_uid, _repo_id):
838 def _get_branch_pointers(_context_uid, _repo_id):
839
839
840 repo_init = self._factory.repo_libgit2(wire)
840 repo_init = self._factory.repo_libgit2(wire)
841 regex = re.compile('^refs/heads')
841 regex = re.compile('^refs/heads')
842 with repo_init as repo:
842 with repo_init as repo:
843 branches = [ref for ref in repo.listall_reference_objects() if regex.match(ref.name)]
843 branches = [ref for ref in repo.listall_reference_objects() if regex.match(ref.name)]
844 return {x.target.hex: x.shorthand for x in branches}
844 return {x.target.hex: x.shorthand for x in branches}
845
845
846 return _get_branch_pointers(context_uid, repo_id)
846 return _get_branch_pointers(context_uid, repo_id)
847
847
848 @reraise_safe_exceptions
848 @reraise_safe_exceptions
849 def head(self, wire, show_exc=True):
849 def head(self, wire, show_exc=True):
850 cache_on, context_uid, repo_id = self._cache_on(wire)
850 cache_on, context_uid, repo_id = self._cache_on(wire)
851 region = self._region(wire)
851 region = self._region(wire)
852
852
853 @region.conditional_cache_on_arguments(condition=cache_on)
853 @region.conditional_cache_on_arguments(condition=cache_on)
854 def _head(_context_uid, _repo_id, _show_exc):
854 def _head(_context_uid, _repo_id, _show_exc):
855 repo_init = self._factory.repo_libgit2(wire)
855 repo_init = self._factory.repo_libgit2(wire)
856 with repo_init as repo:
856 with repo_init as repo:
857 try:
857 try:
858 return repo.head.peel().hex
858 return repo.head.peel().hex
859 except Exception:
859 except Exception:
860 if show_exc:
860 if show_exc:
861 raise
861 raise
862 return _head(context_uid, repo_id, show_exc)
862 return _head(context_uid, repo_id, show_exc)
863
863
864 @reraise_safe_exceptions
864 @reraise_safe_exceptions
865 def init(self, wire):
865 def init(self, wire):
866 repo_path = safe_str(wire['path'])
866 repo_path = safe_str(wire['path'])
867 self.repo = Repo.init(repo_path)
867 self.repo = Repo.init(repo_path)
868
868
869 @reraise_safe_exceptions
869 @reraise_safe_exceptions
870 def init_bare(self, wire):
870 def init_bare(self, wire):
871 repo_path = safe_str(wire['path'])
871 repo_path = safe_str(wire['path'])
872 self.repo = Repo.init_bare(repo_path)
872 self.repo = Repo.init_bare(repo_path)
873
873
874 @reraise_safe_exceptions
874 @reraise_safe_exceptions
875 def revision(self, wire, rev):
875 def revision(self, wire, rev):
876
876
877 cache_on, context_uid, repo_id = self._cache_on(wire)
877 cache_on, context_uid, repo_id = self._cache_on(wire)
878 region = self._region(wire)
878 region = self._region(wire)
879
879
880 @region.conditional_cache_on_arguments(condition=cache_on)
880 @region.conditional_cache_on_arguments(condition=cache_on)
881 def _revision(_context_uid, _repo_id, _rev):
881 def _revision(_context_uid, _repo_id, _rev):
882 repo_init = self._factory.repo_libgit2(wire)
882 repo_init = self._factory.repo_libgit2(wire)
883 with repo_init as repo:
883 with repo_init as repo:
884 commit = repo[rev]
884 commit = repo[rev]
885 obj_data = {
885 obj_data = {
886 'id': commit.id.hex,
886 'id': commit.id.hex,
887 }
887 }
888 # tree objects itself don't have tree_id attribute
888 # tree objects itself don't have tree_id attribute
889 if hasattr(commit, 'tree_id'):
889 if hasattr(commit, 'tree_id'):
890 obj_data['tree'] = commit.tree_id.hex
890 obj_data['tree'] = commit.tree_id.hex
891
891
892 return obj_data
892 return obj_data
893 return _revision(context_uid, repo_id, rev)
893 return _revision(context_uid, repo_id, rev)
894
894
895 @reraise_safe_exceptions
895 @reraise_safe_exceptions
896 def date(self, wire, commit_id):
896 def date(self, wire, commit_id):
897 cache_on, context_uid, repo_id = self._cache_on(wire)
897 cache_on, context_uid, repo_id = self._cache_on(wire)
898 region = self._region(wire)
898 region = self._region(wire)
899
899
900 @region.conditional_cache_on_arguments(condition=cache_on)
900 @region.conditional_cache_on_arguments(condition=cache_on)
901 def _date(_repo_id, _commit_id):
901 def _date(_repo_id, _commit_id):
902 repo_init = self._factory.repo_libgit2(wire)
902 repo_init = self._factory.repo_libgit2(wire)
903 with repo_init as repo:
903 with repo_init as repo:
904 commit = repo[commit_id]
904 commit = repo[commit_id]
905
905
906 if hasattr(commit, 'commit_time'):
906 if hasattr(commit, 'commit_time'):
907 commit_time, commit_time_offset = commit.commit_time, commit.commit_time_offset
907 commit_time, commit_time_offset = commit.commit_time, commit.commit_time_offset
908 else:
908 else:
909 commit = commit.get_object()
909 commit = commit.get_object()
910 commit_time, commit_time_offset = commit.commit_time, commit.commit_time_offset
910 commit_time, commit_time_offset = commit.commit_time, commit.commit_time_offset
911
911
912 # TODO(marcink): check dulwich difference of offset vs timezone
912 # TODO(marcink): check dulwich difference of offset vs timezone
913 return [commit_time, commit_time_offset]
913 return [commit_time, commit_time_offset]
914 return _date(repo_id, commit_id)
914 return _date(repo_id, commit_id)
915
915
916 @reraise_safe_exceptions
916 @reraise_safe_exceptions
917 def author(self, wire, commit_id):
917 def author(self, wire, commit_id):
918 cache_on, context_uid, repo_id = self._cache_on(wire)
918 cache_on, context_uid, repo_id = self._cache_on(wire)
919 region = self._region(wire)
919 region = self._region(wire)
920
920
921 @region.conditional_cache_on_arguments(condition=cache_on)
921 @region.conditional_cache_on_arguments(condition=cache_on)
922 def _author(_repo_id, _commit_id):
922 def _author(_repo_id, _commit_id):
923 repo_init = self._factory.repo_libgit2(wire)
923 repo_init = self._factory.repo_libgit2(wire)
924 with repo_init as repo:
924 with repo_init as repo:
925 commit = repo[commit_id]
925 commit = repo[commit_id]
926
926
927 if hasattr(commit, 'author'):
927 if hasattr(commit, 'author'):
928 author = commit.author
928 author = commit.author
929 else:
929 else:
930 author = commit.get_object().author
930 author = commit.get_object().author
931
931
932 if author.email:
932 if author.email:
933 return "{} <{}>".format(author.name, author.email)
933 return "{} <{}>".format(author.name, author.email)
934
934
935 try:
935 try:
936 return "{}".format(author.name)
936 return "{}".format(author.name)
937 except Exception:
937 except Exception:
938 return "{}".format(safe_str(author.raw_name))
938 return "{}".format(safe_str(author.raw_name))
939
939
940 return _author(repo_id, commit_id)
940 return _author(repo_id, commit_id)
941
941
942 @reraise_safe_exceptions
942 @reraise_safe_exceptions
943 def message(self, wire, commit_id):
943 def message(self, wire, commit_id):
944 cache_on, context_uid, repo_id = self._cache_on(wire)
944 cache_on, context_uid, repo_id = self._cache_on(wire)
945 region = self._region(wire)
945 region = self._region(wire)
946 @region.conditional_cache_on_arguments(condition=cache_on)
946 @region.conditional_cache_on_arguments(condition=cache_on)
947 def _message(_repo_id, _commit_id):
947 def _message(_repo_id, _commit_id):
948 repo_init = self._factory.repo_libgit2(wire)
948 repo_init = self._factory.repo_libgit2(wire)
949 with repo_init as repo:
949 with repo_init as repo:
950 commit = repo[commit_id]
950 commit = repo[commit_id]
951 return commit.message
951 return commit.message
952 return _message(repo_id, commit_id)
952 return _message(repo_id, commit_id)
953
953
954 @reraise_safe_exceptions
954 @reraise_safe_exceptions
955 def parents(self, wire, commit_id):
955 def parents(self, wire, commit_id):
956 cache_on, context_uid, repo_id = self._cache_on(wire)
956 cache_on, context_uid, repo_id = self._cache_on(wire)
957 region = self._region(wire)
957 region = self._region(wire)
958
958
959 @region.conditional_cache_on_arguments(condition=cache_on)
959 @region.conditional_cache_on_arguments(condition=cache_on)
960 def _parents(_repo_id, _commit_id):
960 def _parents(_repo_id, _commit_id):
961 repo_init = self._factory.repo_libgit2(wire)
961 repo_init = self._factory.repo_libgit2(wire)
962 with repo_init as repo:
962 with repo_init as repo:
963 commit = repo[commit_id]
963 commit = repo[commit_id]
964 if hasattr(commit, 'parent_ids'):
964 if hasattr(commit, 'parent_ids'):
965 parent_ids = commit.parent_ids
965 parent_ids = commit.parent_ids
966 else:
966 else:
967 parent_ids = commit.get_object().parent_ids
967 parent_ids = commit.get_object().parent_ids
968
968
969 return [x.hex for x in parent_ids]
969 return [x.hex for x in parent_ids]
970 return _parents(repo_id, commit_id)
970 return _parents(repo_id, commit_id)
971
971
972 @reraise_safe_exceptions
972 @reraise_safe_exceptions
973 def children(self, wire, commit_id):
973 def children(self, wire, commit_id):
974 cache_on, context_uid, repo_id = self._cache_on(wire)
974 cache_on, context_uid, repo_id = self._cache_on(wire)
975 region = self._region(wire)
975 region = self._region(wire)
976
976
977 head = self.head(wire)
977 head = self.head(wire)
978
978
979 @region.conditional_cache_on_arguments(condition=cache_on)
979 @region.conditional_cache_on_arguments(condition=cache_on)
980 def _children(_repo_id, _commit_id):
980 def _children(_repo_id, _commit_id):
981
981
982 output, __ = self.run_git_command(
982 output, __ = self.run_git_command(
983 wire, ['rev-list', '--all', '--children', f'{commit_id}^..{head}'])
983 wire, ['rev-list', '--all', '--children', f'{commit_id}^..{head}'])
984
984
985 child_ids = []
985 child_ids = []
986 pat = re.compile(r'^{}'.format(commit_id))
986 pat = re.compile(r'^{}'.format(commit_id))
987 for line in output.splitlines():
987 for line in output.splitlines():
988 line = safe_str(line)
988 line = safe_str(line)
989 if pat.match(line):
989 if pat.match(line):
990 found_ids = line.split(' ')[1:]
990 found_ids = line.split(' ')[1:]
991 child_ids.extend(found_ids)
991 child_ids.extend(found_ids)
992 break
992 break
993
993
994 return child_ids
994 return child_ids
995 return _children(repo_id, commit_id)
995 return _children(repo_id, commit_id)
996
996
997 @reraise_safe_exceptions
997 @reraise_safe_exceptions
998 def set_refs(self, wire, key, value):
998 def set_refs(self, wire, key, value):
999 repo_init = self._factory.repo_libgit2(wire)
999 repo_init = self._factory.repo_libgit2(wire)
1000 with repo_init as repo:
1000 with repo_init as repo:
1001 repo.references.create(key, value, force=True)
1001 repo.references.create(key, value, force=True)
1002
1002
1003 @reraise_safe_exceptions
1003 @reraise_safe_exceptions
1004 def create_branch(self, wire, branch_name, commit_id, force=False):
1004 def create_branch(self, wire, branch_name, commit_id, force=False):
1005 repo_init = self._factory.repo_libgit2(wire)
1005 repo_init = self._factory.repo_libgit2(wire)
1006 with repo_init as repo:
1006 with repo_init as repo:
1007 commit = repo[commit_id]
1007 commit = repo[commit_id]
1008
1008
1009 if force:
1009 if force:
1010 repo.branches.local.create(branch_name, commit, force=force)
1010 repo.branches.local.create(branch_name, commit, force=force)
1011 elif not repo.branches.get(branch_name):
1011 elif not repo.branches.get(branch_name):
1012 # create only if that branch isn't existing
1012 # create only if that branch isn't existing
1013 repo.branches.local.create(branch_name, commit, force=force)
1013 repo.branches.local.create(branch_name, commit, force=force)
1014
1014
1015 @reraise_safe_exceptions
1015 @reraise_safe_exceptions
1016 def remove_ref(self, wire, key):
1016 def remove_ref(self, wire, key):
1017 repo_init = self._factory.repo_libgit2(wire)
1017 repo_init = self._factory.repo_libgit2(wire)
1018 with repo_init as repo:
1018 with repo_init as repo:
1019 repo.references.delete(key)
1019 repo.references.delete(key)
1020
1020
1021 @reraise_safe_exceptions
1021 @reraise_safe_exceptions
1022 def tag_remove(self, wire, tag_name):
1022 def tag_remove(self, wire, tag_name):
1023 repo_init = self._factory.repo_libgit2(wire)
1023 repo_init = self._factory.repo_libgit2(wire)
1024 with repo_init as repo:
1024 with repo_init as repo:
1025 key = 'refs/tags/{}'.format(tag_name)
1025 key = 'refs/tags/{}'.format(tag_name)
1026 repo.references.delete(key)
1026 repo.references.delete(key)
1027
1027
1028 @reraise_safe_exceptions
1028 @reraise_safe_exceptions
1029 def tree_changes(self, wire, source_id, target_id):
1029 def tree_changes(self, wire, source_id, target_id):
1030 # TODO(marcink): remove this seems it's only used by tests
1030 # TODO(marcink): remove this seems it's only used by tests
1031 repo = self._factory.repo(wire)
1031 repo = self._factory.repo(wire)
1032 source = repo[source_id].tree if source_id else None
1032 source = repo[source_id].tree if source_id else None
1033 target = repo[target_id].tree
1033 target = repo[target_id].tree
1034 result = repo.object_store.tree_changes(source, target)
1034 result = repo.object_store.tree_changes(source, target)
1035 return list(result)
1035 return list(result)
1036
1036
1037 @reraise_safe_exceptions
1037 @reraise_safe_exceptions
1038 def tree_and_type_for_path(self, wire, commit_id, path):
1038 def tree_and_type_for_path(self, wire, commit_id, path):
1039
1039
1040 cache_on, context_uid, repo_id = self._cache_on(wire)
1040 cache_on, context_uid, repo_id = self._cache_on(wire)
1041 region = self._region(wire)
1041 region = self._region(wire)
1042
1042
1043 @region.conditional_cache_on_arguments(condition=cache_on)
1043 @region.conditional_cache_on_arguments(condition=cache_on)
1044 def _tree_and_type_for_path(_context_uid, _repo_id, _commit_id, _path):
1044 def _tree_and_type_for_path(_context_uid, _repo_id, _commit_id, _path):
1045 repo_init = self._factory.repo_libgit2(wire)
1045 repo_init = self._factory.repo_libgit2(wire)
1046
1046
1047 with repo_init as repo:
1047 with repo_init as repo:
1048 commit = repo[commit_id]
1048 commit = repo[commit_id]
1049 try:
1049 try:
1050 tree = commit.tree[path]
1050 tree = commit.tree[path]
1051 except KeyError:
1051 except KeyError:
1052 return None, None, None
1052 return None, None, None
1053
1053
1054 return tree.id.hex, tree.type_str, tree.filemode
1054 return tree.id.hex, tree.type_str, tree.filemode
1055 return _tree_and_type_for_path(context_uid, repo_id, commit_id, path)
1055 return _tree_and_type_for_path(context_uid, repo_id, commit_id, path)
1056
1056
1057 @reraise_safe_exceptions
1057 @reraise_safe_exceptions
1058 def tree_items(self, wire, tree_id):
1058 def tree_items(self, wire, tree_id):
1059 cache_on, context_uid, repo_id = self._cache_on(wire)
1059 cache_on, context_uid, repo_id = self._cache_on(wire)
1060 region = self._region(wire)
1060 region = self._region(wire)
1061
1061
1062 @region.conditional_cache_on_arguments(condition=cache_on)
1062 @region.conditional_cache_on_arguments(condition=cache_on)
1063 def _tree_items(_repo_id, _tree_id):
1063 def _tree_items(_repo_id, _tree_id):
1064
1064
1065 repo_init = self._factory.repo_libgit2(wire)
1065 repo_init = self._factory.repo_libgit2(wire)
1066 with repo_init as repo:
1066 with repo_init as repo:
1067 try:
1067 try:
1068 tree = repo[tree_id]
1068 tree = repo[tree_id]
1069 except KeyError:
1069 except KeyError:
1070 raise ObjectMissing('No tree with id: {}'.format(tree_id))
1070 raise ObjectMissing('No tree with id: {}'.format(tree_id))
1071
1071
1072 result = []
1072 result = []
1073 for item in tree:
1073 for item in tree:
1074 item_sha = item.hex
1074 item_sha = item.hex
1075 item_mode = item.filemode
1075 item_mode = item.filemode
1076 item_type = item.type_str
1076 item_type = item.type_str
1077
1077
1078 if item_type == 'commit':
1078 if item_type == 'commit':
1079 # NOTE(marcink): submodules we translate to 'link' for backward compat
1079 # NOTE(marcink): submodules we translate to 'link' for backward compat
1080 item_type = 'link'
1080 item_type = 'link'
1081
1081
1082 result.append((item.name, item_mode, item_sha, item_type))
1082 result.append((item.name, item_mode, item_sha, item_type))
1083 return result
1083 return result
1084 return _tree_items(repo_id, tree_id)
1084 return _tree_items(repo_id, tree_id)
1085
1085
1086 @reraise_safe_exceptions
1086 @reraise_safe_exceptions
1087 def diff_2(self, wire, commit_id_1, commit_id_2, file_filter, opt_ignorews, context):
1087 def diff_2(self, wire, commit_id_1, commit_id_2, file_filter, opt_ignorews, context):
1088 """
1088 """
1089 Old version that uses subprocess to call diff
1089 Old version that uses subprocess to call diff
1090 """
1090 """
1091
1091
1092 flags = [
1092 flags = [
1093 '-U%s' % context, '--patch',
1093 '-U%s' % context, '--patch',
1094 '--binary',
1094 '--binary',
1095 '--find-renames',
1095 '--find-renames',
1096 '--no-indent-heuristic',
1096 '--no-indent-heuristic',
1097 # '--indent-heuristic',
1097 # '--indent-heuristic',
1098 #'--full-index',
1098 #'--full-index',
1099 #'--abbrev=40'
1099 #'--abbrev=40'
1100 ]
1100 ]
1101
1101
1102 if opt_ignorews:
1102 if opt_ignorews:
1103 flags.append('--ignore-all-space')
1103 flags.append('--ignore-all-space')
1104
1104
1105 if commit_id_1 == self.EMPTY_COMMIT:
1105 if commit_id_1 == self.EMPTY_COMMIT:
1106 cmd = ['show'] + flags + [commit_id_2]
1106 cmd = ['show'] + flags + [commit_id_2]
1107 else:
1107 else:
1108 cmd = ['diff'] + flags + [commit_id_1, commit_id_2]
1108 cmd = ['diff'] + flags + [commit_id_1, commit_id_2]
1109
1109
1110 if file_filter:
1110 if file_filter:
1111 cmd.extend(['--', file_filter])
1111 cmd.extend(['--', file_filter])
1112
1112
1113 diff, __ = self.run_git_command(wire, cmd)
1113 diff, __ = self.run_git_command(wire, cmd)
1114 # If we used 'show' command, strip first few lines (until actual diff
1114 # If we used 'show' command, strip first few lines (until actual diff
1115 # starts)
1115 # starts)
1116 if commit_id_1 == self.EMPTY_COMMIT:
1116 if commit_id_1 == self.EMPTY_COMMIT:
1117 lines = diff.splitlines()
1117 lines = diff.splitlines()
1118 x = 0
1118 x = 0
1119 for line in lines:
1119 for line in lines:
1120 if line.startswith(b'diff'):
1120 if line.startswith(b'diff'):
1121 break
1121 break
1122 x += 1
1122 x += 1
1123 # Append new line just like 'diff' command do
1123 # Append new line just like 'diff' command do
1124 diff = '\n'.join(lines[x:]) + '\n'
1124 diff = '\n'.join(lines[x:]) + '\n'
1125 return diff
1125 return diff
1126
1126
1127 @reraise_safe_exceptions
1127 @reraise_safe_exceptions
1128 def diff(self, wire, commit_id_1, commit_id_2, file_filter, opt_ignorews, context):
1128 def diff(self, wire, commit_id_1, commit_id_2, file_filter, opt_ignorews, context):
1129 repo_init = self._factory.repo_libgit2(wire)
1129 repo_init = self._factory.repo_libgit2(wire)
1130
1130 with repo_init as repo:
1131 with repo_init as repo:
1131 swap = True
1132 swap = True
1132 flags = 0
1133 flags = 0
1133 flags |= pygit2.GIT_DIFF_SHOW_BINARY
1134 flags |= pygit2.GIT_DIFF_SHOW_BINARY
1134
1135
1135 if opt_ignorews:
1136 if opt_ignorews:
1136 flags |= pygit2.GIT_DIFF_IGNORE_WHITESPACE
1137 flags |= pygit2.GIT_DIFF_IGNORE_WHITESPACE
1137
1138
1138 if commit_id_1 == self.EMPTY_COMMIT:
1139 if commit_id_1 == self.EMPTY_COMMIT:
1139 comm1 = repo[commit_id_2]
1140 comm1 = repo[commit_id_2]
1140 diff_obj = comm1.tree.diff_to_tree(
1141 diff_obj = comm1.tree.diff_to_tree(
1141 flags=flags, context_lines=context, swap=swap)
1142 flags=flags, context_lines=context, swap=swap)
1142
1143
1143 else:
1144 else:
1144 comm1 = repo[commit_id_2]
1145 comm1 = repo[commit_id_2]
1145 comm2 = repo[commit_id_1]
1146 comm2 = repo[commit_id_1]
1146 diff_obj = comm1.tree.diff_to_tree(
1147 diff_obj = comm1.tree.diff_to_tree(
1147 comm2.tree, flags=flags, context_lines=context, swap=swap)
1148 comm2.tree, flags=flags, context_lines=context, swap=swap)
1148 similar_flags = 0
1149 similar_flags = 0
1149 similar_flags |= pygit2.GIT_DIFF_FIND_RENAMES
1150 similar_flags |= pygit2.GIT_DIFF_FIND_RENAMES
1150 diff_obj.find_similar(flags=similar_flags)
1151 diff_obj.find_similar(flags=similar_flags)
1151
1152
1152 if file_filter:
1153 if file_filter:
1153 for p in diff_obj:
1154 for p in diff_obj:
1154 if p.delta.old_file.path == file_filter:
1155 if p.delta.old_file.path == file_filter:
1155 return p.patch or ''
1156 return p.data or ''
1156 # fo matching path == no diff
1157 # fo matching path == no diff
1157 return ''
1158 return ''
1158 return diff_obj.patch or ''
1159 return diff_obj.patch or ''
1159
1160
1160 @reraise_safe_exceptions
1161 @reraise_safe_exceptions
1161 def node_history(self, wire, commit_id, path, limit):
1162 def node_history(self, wire, commit_id, path, limit):
1162 cache_on, context_uid, repo_id = self._cache_on(wire)
1163 cache_on, context_uid, repo_id = self._cache_on(wire)
1163 region = self._region(wire)
1164 region = self._region(wire)
1164
1165
1165 @region.conditional_cache_on_arguments(condition=cache_on)
1166 @region.conditional_cache_on_arguments(condition=cache_on)
1166 def _node_history(_context_uid, _repo_id, _commit_id, _path, _limit):
1167 def _node_history(_context_uid, _repo_id, _commit_id, _path, _limit):
1167 # optimize for n==1, rev-list is much faster for that use-case
1168 # optimize for n==1, rev-list is much faster for that use-case
1168 if limit == 1:
1169 if limit == 1:
1169 cmd = ['rev-list', '-1', commit_id, '--', path]
1170 cmd = ['rev-list', '-1', commit_id, '--', path]
1170 else:
1171 else:
1171 cmd = ['log']
1172 cmd = ['log']
1172 if limit:
1173 if limit:
1173 cmd.extend(['-n', str(safe_int(limit, 0))])
1174 cmd.extend(['-n', str(safe_int(limit, 0))])
1174 cmd.extend(['--pretty=format: %H', '-s', commit_id, '--', path])
1175 cmd.extend(['--pretty=format: %H', '-s', commit_id, '--', path])
1175
1176
1176 output, __ = self.run_git_command(wire, cmd)
1177 output, __ = self.run_git_command(wire, cmd)
1177 commit_ids = re.findall(rb'[0-9a-fA-F]{40}', output)
1178 commit_ids = re.findall(rb'[0-9a-fA-F]{40}', output)
1178
1179
1179 return [x for x in commit_ids]
1180 return [x for x in commit_ids]
1180 return _node_history(context_uid, repo_id, commit_id, path, limit)
1181 return _node_history(context_uid, repo_id, commit_id, path, limit)
1181
1182
1182 @reraise_safe_exceptions
1183 @reraise_safe_exceptions
1183 def node_annotate_legacy(self, wire, commit_id, path):
1184 def node_annotate_legacy(self, wire, commit_id, path):
1184 #note: replaced by pygit2 impelementation
1185 #note: replaced by pygit2 impelementation
1185 cmd = ['blame', '-l', '--root', '-r', commit_id, '--', path]
1186 cmd = ['blame', '-l', '--root', '-r', commit_id, '--', path]
1186 # -l ==> outputs long shas (and we need all 40 characters)
1187 # -l ==> outputs long shas (and we need all 40 characters)
1187 # --root ==> doesn't put '^' character for boundaries
1188 # --root ==> doesn't put '^' character for boundaries
1188 # -r commit_id ==> blames for the given commit
1189 # -r commit_id ==> blames for the given commit
1189 output, __ = self.run_git_command(wire, cmd)
1190 output, __ = self.run_git_command(wire, cmd)
1190
1191
1191 result = []
1192 result = []
1192 for i, blame_line in enumerate(output.splitlines()[:-1]):
1193 for i, blame_line in enumerate(output.splitlines()[:-1]):
1193 line_no = i + 1
1194 line_no = i + 1
1194 blame_commit_id, line = re.split(rb' ', blame_line, 1)
1195 blame_commit_id, line = re.split(rb' ', blame_line, 1)
1195 result.append((line_no, blame_commit_id, line))
1196 result.append((line_no, blame_commit_id, line))
1196
1197
1197 return result
1198 return result
1198
1199
1199 @reraise_safe_exceptions
1200 @reraise_safe_exceptions
1200 def node_annotate(self, wire, commit_id, path):
1201 def node_annotate(self, wire, commit_id, path):
1201
1202
1202 result_libgit = []
1203 result_libgit = []
1203 repo_init = self._factory.repo_libgit2(wire)
1204 repo_init = self._factory.repo_libgit2(wire)
1204 with repo_init as repo:
1205 with repo_init as repo:
1205 commit = repo[commit_id]
1206 commit = repo[commit_id]
1206 blame_obj = repo.blame(path, newest_commit=commit_id)
1207 blame_obj = repo.blame(path, newest_commit=commit_id)
1207 for i, line in enumerate(commit.tree[path].data.splitlines()):
1208 for i, line in enumerate(commit.tree[path].data.splitlines()):
1208 line_no = i + 1
1209 line_no = i + 1
1209 hunk = blame_obj.for_line(line_no)
1210 hunk = blame_obj.for_line(line_no)
1210 blame_commit_id = hunk.final_commit_id.hex
1211 blame_commit_id = hunk.final_commit_id.hex
1211
1212
1212 result_libgit.append((line_no, blame_commit_id, line))
1213 result_libgit.append((line_no, blame_commit_id, line))
1213
1214
1214 return result_libgit
1215 return result_libgit
1215
1216
1216 @reraise_safe_exceptions
1217 @reraise_safe_exceptions
1217 def update_server_info(self, wire):
1218 def update_server_info(self, wire):
1218 repo = self._factory.repo(wire)
1219 repo = self._factory.repo(wire)
1219 update_server_info(repo)
1220 update_server_info(repo)
1220
1221
1221 @reraise_safe_exceptions
1222 @reraise_safe_exceptions
1222 def get_all_commit_ids(self, wire):
1223 def get_all_commit_ids(self, wire):
1223
1224
1224 cache_on, context_uid, repo_id = self._cache_on(wire)
1225 cache_on, context_uid, repo_id = self._cache_on(wire)
1225 region = self._region(wire)
1226 region = self._region(wire)
1226
1227
1227 @region.conditional_cache_on_arguments(condition=cache_on)
1228 @region.conditional_cache_on_arguments(condition=cache_on)
1228 def _get_all_commit_ids(_context_uid, _repo_id):
1229 def _get_all_commit_ids(_context_uid, _repo_id):
1229
1230
1230 cmd = ['rev-list', '--reverse', '--date-order', '--branches', '--tags']
1231 cmd = ['rev-list', '--reverse', '--date-order', '--branches', '--tags']
1231 try:
1232 try:
1232 output, __ = self.run_git_command(wire, cmd)
1233 output, __ = self.run_git_command(wire, cmd)
1233 return output.splitlines()
1234 return output.splitlines()
1234 except Exception:
1235 except Exception:
1235 # Can be raised for empty repositories
1236 # Can be raised for empty repositories
1236 return []
1237 return []
1237
1238
1238 @region.conditional_cache_on_arguments(condition=cache_on)
1239 @region.conditional_cache_on_arguments(condition=cache_on)
1239 def _get_all_commit_ids_pygit2(_context_uid, _repo_id):
1240 def _get_all_commit_ids_pygit2(_context_uid, _repo_id):
1240 repo_init = self._factory.repo_libgit2(wire)
1241 repo_init = self._factory.repo_libgit2(wire)
1241 from pygit2 import GIT_SORT_REVERSE, GIT_SORT_TIME, GIT_BRANCH_ALL
1242 from pygit2 import GIT_SORT_REVERSE, GIT_SORT_TIME, GIT_BRANCH_ALL
1242 results = []
1243 results = []
1243 with repo_init as repo:
1244 with repo_init as repo:
1244 for commit in repo.walk(repo.head.target, GIT_SORT_TIME | GIT_BRANCH_ALL | GIT_SORT_REVERSE):
1245 for commit in repo.walk(repo.head.target, GIT_SORT_TIME | GIT_BRANCH_ALL | GIT_SORT_REVERSE):
1245 results.append(commit.id.hex)
1246 results.append(commit.id.hex)
1246
1247
1247 return _get_all_commit_ids(context_uid, repo_id)
1248 return _get_all_commit_ids(context_uid, repo_id)
1248
1249
1249 @reraise_safe_exceptions
1250 @reraise_safe_exceptions
1250 def run_git_command(self, wire, cmd, **opts):
1251 def run_git_command(self, wire, cmd, **opts):
1251 path = wire.get('path', None)
1252 path = wire.get('path', None)
1252
1253
1253 if path and os.path.isdir(path):
1254 if path and os.path.isdir(path):
1254 opts['cwd'] = path
1255 opts['cwd'] = path
1255
1256
1256 if '_bare' in opts:
1257 if '_bare' in opts:
1257 _copts = []
1258 _copts = []
1258 del opts['_bare']
1259 del opts['_bare']
1259 else:
1260 else:
1260 _copts = ['-c', 'core.quotepath=false', ]
1261 _copts = ['-c', 'core.quotepath=false', ]
1261 safe_call = False
1262 safe_call = False
1262 if '_safe' in opts:
1263 if '_safe' in opts:
1263 # no exc on failure
1264 # no exc on failure
1264 del opts['_safe']
1265 del opts['_safe']
1265 safe_call = True
1266 safe_call = True
1266
1267
1267 if '_copts' in opts:
1268 if '_copts' in opts:
1268 _copts.extend(opts['_copts'] or [])
1269 _copts.extend(opts['_copts'] or [])
1269 del opts['_copts']
1270 del opts['_copts']
1270
1271
1271 gitenv = os.environ.copy()
1272 gitenv = os.environ.copy()
1272 gitenv.update(opts.pop('extra_env', {}))
1273 gitenv.update(opts.pop('extra_env', {}))
1273 # need to clean fix GIT_DIR !
1274 # need to clean fix GIT_DIR !
1274 if 'GIT_DIR' in gitenv:
1275 if 'GIT_DIR' in gitenv:
1275 del gitenv['GIT_DIR']
1276 del gitenv['GIT_DIR']
1276 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
1277 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
1277 gitenv['GIT_DISCOVERY_ACROSS_FILESYSTEM'] = '1'
1278 gitenv['GIT_DISCOVERY_ACROSS_FILESYSTEM'] = '1'
1278
1279
1279 cmd = [settings.GIT_EXECUTABLE] + _copts + cmd
1280 cmd = [settings.GIT_EXECUTABLE] + _copts + cmd
1280 _opts = {'env': gitenv, 'shell': False}
1281 _opts = {'env': gitenv, 'shell': False}
1281
1282
1282 proc = None
1283 proc = None
1283 try:
1284 try:
1284 _opts.update(opts)
1285 _opts.update(opts)
1285 proc = subprocessio.SubprocessIOChunker(cmd, **_opts)
1286 proc = subprocessio.SubprocessIOChunker(cmd, **_opts)
1286
1287
1287 return b''.join(proc), b''.join(proc.stderr)
1288 return b''.join(proc), b''.join(proc.stderr)
1288 except OSError as err:
1289 except OSError as err:
1289 cmd = ' '.join(map(safe_str, cmd)) # human friendly CMD
1290 cmd = ' '.join(map(safe_str, cmd)) # human friendly CMD
1290 tb_err = ("Couldn't run git command (%s).\n"
1291 tb_err = ("Couldn't run git command (%s).\n"
1291 "Original error was:%s\n"
1292 "Original error was:%s\n"
1292 "Call options:%s\n"
1293 "Call options:%s\n"
1293 % (cmd, err, _opts))
1294 % (cmd, err, _opts))
1294 log.exception(tb_err)
1295 log.exception(tb_err)
1295 if safe_call:
1296 if safe_call:
1296 return '', err
1297 return '', err
1297 else:
1298 else:
1298 raise exceptions.VcsException()(tb_err)
1299 raise exceptions.VcsException()(tb_err)
1299 finally:
1300 finally:
1300 if proc:
1301 if proc:
1301 proc.close()
1302 proc.close()
1302
1303
1303 @reraise_safe_exceptions
1304 @reraise_safe_exceptions
1304 def install_hooks(self, wire, force=False):
1305 def install_hooks(self, wire, force=False):
1305 from vcsserver.hook_utils import install_git_hooks
1306 from vcsserver.hook_utils import install_git_hooks
1306 bare = self.bare(wire)
1307 bare = self.bare(wire)
1307 path = wire['path']
1308 path = wire['path']
1308 binary_dir = settings.BINARY_DIR
1309 binary_dir = settings.BINARY_DIR
1309 executable = None
1310 executable = None
1310 if binary_dir:
1311 if binary_dir:
1311 executable = os.path.join(binary_dir, 'python3')
1312 executable = os.path.join(binary_dir, 'python3')
1312 return install_git_hooks(path, bare, force_create=force)
1313 return install_git_hooks(path, bare, force_create=force)
1313
1314
1314 @reraise_safe_exceptions
1315 @reraise_safe_exceptions
1315 def get_hooks_info(self, wire):
1316 def get_hooks_info(self, wire):
1316 from vcsserver.hook_utils import (
1317 from vcsserver.hook_utils import (
1317 get_git_pre_hook_version, get_git_post_hook_version)
1318 get_git_pre_hook_version, get_git_post_hook_version)
1318 bare = self.bare(wire)
1319 bare = self.bare(wire)
1319 path = wire['path']
1320 path = wire['path']
1320 return {
1321 return {
1321 'pre_version': get_git_pre_hook_version(path, bare),
1322 'pre_version': get_git_pre_hook_version(path, bare),
1322 'post_version': get_git_post_hook_version(path, bare),
1323 'post_version': get_git_post_hook_version(path, bare),
1323 }
1324 }
1324
1325
1325 @reraise_safe_exceptions
1326 @reraise_safe_exceptions
1326 def set_head_ref(self, wire, head_name):
1327 def set_head_ref(self, wire, head_name):
1327 log.debug('Setting refs/head to `%s`', head_name)
1328 log.debug('Setting refs/head to `%s`', head_name)
1328 cmd = ['symbolic-ref', '"HEAD"', '"refs/heads/%s"' % head_name]
1329 cmd = ['symbolic-ref', '"HEAD"', '"refs/heads/%s"' % head_name]
1329 output, __ = self.run_git_command(wire, cmd)
1330 output, __ = self.run_git_command(wire, cmd)
1330 return [head_name] + output.splitlines()
1331 return [head_name] + output.splitlines()
1331
1332
1332 @reraise_safe_exceptions
1333 @reraise_safe_exceptions
1333 def archive_repo(self, wire, archive_dest_path, kind, mtime, archive_at_path,
1334 def archive_repo(self, wire, archive_dest_path, kind, mtime, archive_at_path,
1334 archive_dir_name, commit_id):
1335 archive_dir_name, commit_id):
1335
1336
1336 def file_walker(_commit_id, path):
1337 def file_walker(_commit_id, path):
1337 repo_init = self._factory.repo_libgit2(wire)
1338 repo_init = self._factory.repo_libgit2(wire)
1338
1339
1339 with repo_init as repo:
1340 with repo_init as repo:
1340 commit = repo[commit_id]
1341 commit = repo[commit_id]
1341
1342
1342 if path in ['', '/']:
1343 if path in ['', '/']:
1343 tree = commit.tree
1344 tree = commit.tree
1344 else:
1345 else:
1345 tree = commit.tree[path.rstrip('/')]
1346 tree = commit.tree[path.rstrip('/')]
1346 tree_id = tree.id.hex
1347 tree_id = tree.id.hex
1347 try:
1348 try:
1348 tree = repo[tree_id]
1349 tree = repo[tree_id]
1349 except KeyError:
1350 except KeyError:
1350 raise ObjectMissing('No tree with id: {}'.format(tree_id))
1351 raise ObjectMissing('No tree with id: {}'.format(tree_id))
1351
1352
1352 index = LibGit2Index.Index()
1353 index = LibGit2Index.Index()
1353 index.read_tree(tree)
1354 index.read_tree(tree)
1354 file_iter = index
1355 file_iter = index
1355
1356
1356 for fn in file_iter:
1357 for fn in file_iter:
1357 file_path = fn.path
1358 file_path = fn.path
1358 mode = fn.mode
1359 mode = fn.mode
1359 is_link = stat.S_ISLNK(mode)
1360 is_link = stat.S_ISLNK(mode)
1360 if mode == pygit2.GIT_FILEMODE_COMMIT:
1361 if mode == pygit2.GIT_FILEMODE_COMMIT:
1361 log.debug('Skipping path %s as a commit node', file_path)
1362 log.debug('Skipping path %s as a commit node', file_path)
1362 continue
1363 continue
1363 yield ArchiveNode(file_path, mode, is_link, repo[fn.hex].read_raw)
1364 yield ArchiveNode(file_path, mode, is_link, repo[fn.hex].read_raw)
1364
1365
1365 return archive_repo(file_walker, archive_dest_path, kind, mtime, archive_at_path,
1366 return archive_repo(file_walker, archive_dest_path, kind, mtime, archive_at_path,
1366 archive_dir_name, commit_id)
1367 archive_dir_name, commit_id)
@@ -1,1101 +1,1103 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-2020 RhodeCode GmbH
2 # Copyright (C) 2014-2020 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 import binascii
18 import io
18 import io
19 import logging
19 import logging
20 import stat
20 import stat
21 import urllib.request
21 import urllib.request
22 import urllib.parse
22 import urllib.parse
23 import traceback
23 import traceback
24 import hashlib
24 import hashlib
25
25
26 from hgext import largefiles, rebase, purge
26 from hgext import largefiles, rebase, purge
27
27
28 from mercurial import commands
28 from mercurial import commands
29 from mercurial import unionrepo
29 from mercurial import unionrepo
30 from mercurial import verify
30 from mercurial import verify
31 from mercurial import repair
31 from mercurial import repair
32
32
33 import vcsserver
33 import vcsserver
34 from vcsserver import exceptions
34 from vcsserver import exceptions
35 from vcsserver.base import RepoFactory, obfuscate_qs, raise_from_original, archive_repo, ArchiveNode
35 from vcsserver.base import RepoFactory, obfuscate_qs, raise_from_original, archive_repo, ArchiveNode
36 from vcsserver.hgcompat import (
36 from vcsserver.hgcompat import (
37 archival, bin, clone, config as hgconfig, diffopts, hex, get_ctx,
37 archival, bin, clone, config as hgconfig, diffopts, hex, get_ctx,
38 hg_url as url_parser, httpbasicauthhandler, httpdigestauthhandler,
38 hg_url as url_parser, httpbasicauthhandler, httpdigestauthhandler,
39 makepeer, instance, match, memctx, exchange, memfilectx, nullrev, hg_merge,
39 makepeer, instance, match, memctx, exchange, memfilectx, nullrev, hg_merge,
40 patch, peer, revrange, ui, hg_tag, Abort, LookupError, RepoError,
40 patch, peer, revrange, ui, hg_tag, Abort, LookupError, RepoError,
41 RepoLookupError, InterventionRequired, RequirementError,
41 RepoLookupError, InterventionRequired, RequirementError,
42 alwaysmatcher, patternmatcher, hgutil, hgext_strip)
42 alwaysmatcher, patternmatcher, hgutil, hgext_strip)
43 from vcsserver.str_utils import ascii_bytes, ascii_str, safe_str, safe_bytes
43 from vcsserver.str_utils import ascii_bytes, ascii_str, safe_str, safe_bytes
44 from vcsserver.vcs_base import RemoteBase
44 from vcsserver.vcs_base import RemoteBase
45
45
46 log = logging.getLogger(__name__)
46 log = logging.getLogger(__name__)
47
47
48
48
49 def make_ui_from_config(repo_config):
49 def make_ui_from_config(repo_config):
50
50
51 class LoggingUI(ui.ui):
51 class LoggingUI(ui.ui):
52
52
53 def status(self, *msg, **opts):
53 def status(self, *msg, **opts):
54 str_msg = map(safe_str, msg)
54 str_msg = map(safe_str, msg)
55 log.info(' '.join(str_msg).rstrip('\n'))
55 log.info(' '.join(str_msg).rstrip('\n'))
56 #super(LoggingUI, self).status(*msg, **opts)
56 #super(LoggingUI, self).status(*msg, **opts)
57
57
58 def warn(self, *msg, **opts):
58 def warn(self, *msg, **opts):
59 str_msg = map(safe_str, msg)
59 str_msg = map(safe_str, msg)
60 log.warning('ui_logger:'+' '.join(str_msg).rstrip('\n'))
60 log.warning('ui_logger:'+' '.join(str_msg).rstrip('\n'))
61 #super(LoggingUI, self).warn(*msg, **opts)
61 #super(LoggingUI, self).warn(*msg, **opts)
62
62
63 def error(self, *msg, **opts):
63 def error(self, *msg, **opts):
64 str_msg = map(safe_str, msg)
64 str_msg = map(safe_str, msg)
65 log.error('ui_logger:'+' '.join(str_msg).rstrip('\n'))
65 log.error('ui_logger:'+' '.join(str_msg).rstrip('\n'))
66 #super(LoggingUI, self).error(*msg, **opts)
66 #super(LoggingUI, self).error(*msg, **opts)
67
67
68 def note(self, *msg, **opts):
68 def note(self, *msg, **opts):
69 str_msg = map(safe_str, msg)
69 str_msg = map(safe_str, msg)
70 log.info('ui_logger:'+' '.join(str_msg).rstrip('\n'))
70 log.info('ui_logger:'+' '.join(str_msg).rstrip('\n'))
71 #super(LoggingUI, self).note(*msg, **opts)
71 #super(LoggingUI, self).note(*msg, **opts)
72
72
73 def debug(self, *msg, **opts):
73 def debug(self, *msg, **opts):
74 str_msg = map(safe_str, msg)
74 str_msg = map(safe_str, msg)
75 log.debug('ui_logger:'+' '.join(str_msg).rstrip('\n'))
75 log.debug('ui_logger:'+' '.join(str_msg).rstrip('\n'))
76 #super(LoggingUI, self).debug(*msg, **opts)
76 #super(LoggingUI, self).debug(*msg, **opts)
77
77
78 baseui = LoggingUI()
78 baseui = LoggingUI()
79
79
80 # clean the baseui object
80 # clean the baseui object
81 baseui._ocfg = hgconfig.config()
81 baseui._ocfg = hgconfig.config()
82 baseui._ucfg = hgconfig.config()
82 baseui._ucfg = hgconfig.config()
83 baseui._tcfg = hgconfig.config()
83 baseui._tcfg = hgconfig.config()
84
84
85 for section, option, value in repo_config:
85 for section, option, value in repo_config:
86 baseui.setconfig(ascii_bytes(section), ascii_bytes(option), ascii_bytes(value))
86 baseui.setconfig(ascii_bytes(section), ascii_bytes(option), ascii_bytes(value))
87
87
88 # make our hgweb quiet so it doesn't print output
88 # make our hgweb quiet so it doesn't print output
89 baseui.setconfig(b'ui', b'quiet', b'true')
89 baseui.setconfig(b'ui', b'quiet', b'true')
90
90
91 baseui.setconfig(b'ui', b'paginate', b'never')
91 baseui.setconfig(b'ui', b'paginate', b'never')
92 # for better Error reporting of Mercurial
92 # for better Error reporting of Mercurial
93 baseui.setconfig(b'ui', b'message-output', b'stderr')
93 baseui.setconfig(b'ui', b'message-output', b'stderr')
94
94
95 # force mercurial to only use 1 thread, otherwise it may try to set a
95 # force mercurial to only use 1 thread, otherwise it may try to set a
96 # signal in a non-main thread, thus generating a ValueError.
96 # signal in a non-main thread, thus generating a ValueError.
97 baseui.setconfig(b'worker', b'numcpus', 1)
97 baseui.setconfig(b'worker', b'numcpus', 1)
98
98
99 # If there is no config for the largefiles extension, we explicitly disable
99 # If there is no config for the largefiles extension, we explicitly disable
100 # it here. This overrides settings from repositories hgrc file. Recent
100 # it here. This overrides settings from repositories hgrc file. Recent
101 # mercurial versions enable largefiles in hgrc on clone from largefile
101 # mercurial versions enable largefiles in hgrc on clone from largefile
102 # repo.
102 # repo.
103 if not baseui.hasconfig(b'extensions', b'largefiles'):
103 if not baseui.hasconfig(b'extensions', b'largefiles'):
104 log.debug('Explicitly disable largefiles extension for repo.')
104 log.debug('Explicitly disable largefiles extension for repo.')
105 baseui.setconfig(b'extensions', b'largefiles', b'!')
105 baseui.setconfig(b'extensions', b'largefiles', b'!')
106
106
107 return baseui
107 return baseui
108
108
109
109
110 def reraise_safe_exceptions(func):
110 def reraise_safe_exceptions(func):
111 """Decorator for converting mercurial exceptions to something neutral."""
111 """Decorator for converting mercurial exceptions to something neutral."""
112
112
113 def wrapper(*args, **kwargs):
113 def wrapper(*args, **kwargs):
114 try:
114 try:
115 return func(*args, **kwargs)
115 return func(*args, **kwargs)
116 except (Abort, InterventionRequired) as e:
116 except (Abort, InterventionRequired) as e:
117 raise_from_original(exceptions.AbortException(e), e)
117 raise_from_original(exceptions.AbortException(e), e)
118 except RepoLookupError as e:
118 except RepoLookupError as e:
119 raise_from_original(exceptions.LookupException(e), e)
119 raise_from_original(exceptions.LookupException(e), e)
120 except RequirementError as e:
120 except RequirementError as e:
121 raise_from_original(exceptions.RequirementException(e), e)
121 raise_from_original(exceptions.RequirementException(e), e)
122 except RepoError as e:
122 except RepoError as e:
123 raise_from_original(exceptions.VcsException(e), e)
123 raise_from_original(exceptions.VcsException(e), e)
124 except LookupError as e:
124 except LookupError as e:
125 raise_from_original(exceptions.LookupException(e), e)
125 raise_from_original(exceptions.LookupException(e), e)
126 except Exception as e:
126 except Exception as e:
127 if not hasattr(e, '_vcs_kind'):
127 if not hasattr(e, '_vcs_kind'):
128 log.exception("Unhandled exception in hg remote call")
128 log.exception("Unhandled exception in hg remote call")
129 raise_from_original(exceptions.UnhandledException(e), e)
129 raise_from_original(exceptions.UnhandledException(e), e)
130
130
131 raise
131 raise
132 return wrapper
132 return wrapper
133
133
134
134
135 class MercurialFactory(RepoFactory):
135 class MercurialFactory(RepoFactory):
136 repo_type = 'hg'
136 repo_type = 'hg'
137
137
138 def _create_config(self, config, hooks=True):
138 def _create_config(self, config, hooks=True):
139 if not hooks:
139 if not hooks:
140 hooks_to_clean = frozenset((
140 hooks_to_clean = frozenset((
141 'changegroup.repo_size', 'preoutgoing.pre_pull',
141 'changegroup.repo_size', 'preoutgoing.pre_pull',
142 'outgoing.pull_logger', 'prechangegroup.pre_push'))
142 'outgoing.pull_logger', 'prechangegroup.pre_push'))
143 new_config = []
143 new_config = []
144 for section, option, value in config:
144 for section, option, value in config:
145 if section == 'hooks' and option in hooks_to_clean:
145 if section == 'hooks' and option in hooks_to_clean:
146 continue
146 continue
147 new_config.append((section, option, value))
147 new_config.append((section, option, value))
148 config = new_config
148 config = new_config
149
149
150 baseui = make_ui_from_config(config)
150 baseui = make_ui_from_config(config)
151 return baseui
151 return baseui
152
152
153 def _create_repo(self, wire, create):
153 def _create_repo(self, wire, create):
154 baseui = self._create_config(wire["config"])
154 baseui = self._create_config(wire["config"])
155 return instance(baseui, safe_bytes(wire["path"]), create)
155 return instance(baseui, safe_bytes(wire["path"]), create)
156
156
157 def repo(self, wire, create=False):
157 def repo(self, wire, create=False):
158 """
158 """
159 Get a repository instance for the given path.
159 Get a repository instance for the given path.
160 """
160 """
161 return self._create_repo(wire, create)
161 return self._create_repo(wire, create)
162
162
163
163
164 def patch_ui_message_output(baseui):
164 def patch_ui_message_output(baseui):
165 baseui.setconfig(b'ui', b'quiet', b'false')
165 baseui.setconfig(b'ui', b'quiet', b'false')
166 output = io.BytesIO()
166 output = io.BytesIO()
167
167
168 def write(data, **unused_kwargs):
168 def write(data, **unused_kwargs):
169 output.write(data)
169 output.write(data)
170
170
171 baseui.status = write
171 baseui.status = write
172 baseui.write = write
172 baseui.write = write
173 baseui.warn = write
173 baseui.warn = write
174 baseui.debug = write
174 baseui.debug = write
175
175
176 return baseui, output
176 return baseui, output
177
177
178
178
179 class HgRemote(RemoteBase):
179 class HgRemote(RemoteBase):
180
180
181 def __init__(self, factory):
181 def __init__(self, factory):
182 self._factory = factory
182 self._factory = factory
183 self._bulk_methods = {
183 self._bulk_methods = {
184 "affected_files": self.ctx_files,
184 "affected_files": self.ctx_files,
185 "author": self.ctx_user,
185 "author": self.ctx_user,
186 "branch": self.ctx_branch,
186 "branch": self.ctx_branch,
187 "children": self.ctx_children,
187 "children": self.ctx_children,
188 "date": self.ctx_date,
188 "date": self.ctx_date,
189 "message": self.ctx_description,
189 "message": self.ctx_description,
190 "parents": self.ctx_parents,
190 "parents": self.ctx_parents,
191 "status": self.ctx_status,
191 "status": self.ctx_status,
192 "obsolete": self.ctx_obsolete,
192 "obsolete": self.ctx_obsolete,
193 "phase": self.ctx_phase,
193 "phase": self.ctx_phase,
194 "hidden": self.ctx_hidden,
194 "hidden": self.ctx_hidden,
195 "_file_paths": self.ctx_list,
195 "_file_paths": self.ctx_list,
196 }
196 }
197
197
198 def _get_ctx(self, repo, ref):
198 def _get_ctx(self, repo, ref):
199 return get_ctx(repo, ref)
199 return get_ctx(repo, ref)
200
200
201 @reraise_safe_exceptions
201 @reraise_safe_exceptions
202 def discover_hg_version(self):
202 def discover_hg_version(self):
203 from mercurial import util
203 from mercurial import util
204 return safe_str(util.version())
204 return safe_str(util.version())
205
205
206 @reraise_safe_exceptions
206 @reraise_safe_exceptions
207 def is_empty(self, wire):
207 def is_empty(self, wire):
208 repo = self._factory.repo(wire)
208 repo = self._factory.repo(wire)
209
209
210 try:
210 try:
211 return len(repo) == 0
211 return len(repo) == 0
212 except Exception:
212 except Exception:
213 log.exception("failed to read object_store")
213 log.exception("failed to read object_store")
214 return False
214 return False
215
215
216 @reraise_safe_exceptions
216 @reraise_safe_exceptions
217 def bookmarks(self, wire):
217 def bookmarks(self, wire):
218 cache_on, context_uid, repo_id = self._cache_on(wire)
218 cache_on, context_uid, repo_id = self._cache_on(wire)
219 region = self._region(wire)
219 region = self._region(wire)
220
220
221 @region.conditional_cache_on_arguments(condition=cache_on)
221 @region.conditional_cache_on_arguments(condition=cache_on)
222 def _bookmarks(_context_uid, _repo_id):
222 def _bookmarks(_context_uid, _repo_id):
223 repo = self._factory.repo(wire)
223 repo = self._factory.repo(wire)
224 return {safe_str(name): ascii_str(hex(sha)) for name, sha in repo._bookmarks.items()}
224 return {safe_str(name): ascii_str(hex(sha)) for name, sha in repo._bookmarks.items()}
225
225
226 return _bookmarks(context_uid, repo_id)
226 return _bookmarks(context_uid, repo_id)
227
227
228 @reraise_safe_exceptions
228 @reraise_safe_exceptions
229 def branches(self, wire, normal, closed):
229 def branches(self, wire, normal, closed):
230 cache_on, context_uid, repo_id = self._cache_on(wire)
230 cache_on, context_uid, repo_id = self._cache_on(wire)
231 region = self._region(wire)
231 region = self._region(wire)
232
232
233 @region.conditional_cache_on_arguments(condition=cache_on)
233 @region.conditional_cache_on_arguments(condition=cache_on)
234 def _branches(_context_uid, _repo_id, _normal, _closed):
234 def _branches(_context_uid, _repo_id, _normal, _closed):
235 repo = self._factory.repo(wire)
235 repo = self._factory.repo(wire)
236 iter_branches = repo.branchmap().iterbranches()
236 iter_branches = repo.branchmap().iterbranches()
237 bt = {}
237 bt = {}
238 for branch_name, _heads, tip_node, is_closed in iter_branches:
238 for branch_name, _heads, tip_node, is_closed in iter_branches:
239 if normal and not is_closed:
239 if normal and not is_closed:
240 bt[safe_str(branch_name)] = ascii_str(hex(tip_node))
240 bt[safe_str(branch_name)] = ascii_str(hex(tip_node))
241 if closed and is_closed:
241 if closed and is_closed:
242 bt[safe_str(branch_name)] = ascii_str(hex(tip_node))
242 bt[safe_str(branch_name)] = ascii_str(hex(tip_node))
243
243
244 return bt
244 return bt
245
245
246 return _branches(context_uid, repo_id, normal, closed)
246 return _branches(context_uid, repo_id, normal, closed)
247
247
248 @reraise_safe_exceptions
248 @reraise_safe_exceptions
249 def bulk_request(self, wire, commit_id, pre_load):
249 def bulk_request(self, wire, commit_id, pre_load):
250 cache_on, context_uid, repo_id = self._cache_on(wire)
250 cache_on, context_uid, repo_id = self._cache_on(wire)
251 region = self._region(wire)
251 region = self._region(wire)
252
252
253 @region.conditional_cache_on_arguments(condition=cache_on)
253 @region.conditional_cache_on_arguments(condition=cache_on)
254 def _bulk_request(_repo_id, _commit_id, _pre_load):
254 def _bulk_request(_repo_id, _commit_id, _pre_load):
255 result = {}
255 result = {}
256 for attr in pre_load:
256 for attr in pre_load:
257 try:
257 try:
258 method = self._bulk_methods[attr]
258 method = self._bulk_methods[attr]
259 wire.update({'cache': False}) # disable cache for bulk calls so we don't double cache
259 wire.update({'cache': False}) # disable cache for bulk calls so we don't double cache
260 result[attr] = method(wire, commit_id)
260 result[attr] = method(wire, commit_id)
261 except KeyError as e:
261 except KeyError as e:
262 raise exceptions.VcsException(e)(
262 raise exceptions.VcsException(e)(
263 'Unknown bulk attribute: "%s"' % attr)
263 'Unknown bulk attribute: "%s"' % attr)
264 return result
264 return result
265
265
266 return _bulk_request(repo_id, commit_id, sorted(pre_load))
266 return _bulk_request(repo_id, commit_id, sorted(pre_load))
267
267
268 @reraise_safe_exceptions
268 @reraise_safe_exceptions
269 def ctx_branch(self, wire, commit_id):
269 def ctx_branch(self, wire, commit_id):
270 cache_on, context_uid, repo_id = self._cache_on(wire)
270 cache_on, context_uid, repo_id = self._cache_on(wire)
271 region = self._region(wire)
271 region = self._region(wire)
272
272
273 @region.conditional_cache_on_arguments(condition=cache_on)
273 @region.conditional_cache_on_arguments(condition=cache_on)
274 def _ctx_branch(_repo_id, _commit_id):
274 def _ctx_branch(_repo_id, _commit_id):
275 repo = self._factory.repo(wire)
275 repo = self._factory.repo(wire)
276 ctx = self._get_ctx(repo, commit_id)
276 ctx = self._get_ctx(repo, commit_id)
277 return ctx.branch()
277 return ctx.branch()
278 return _ctx_branch(repo_id, commit_id)
278 return _ctx_branch(repo_id, commit_id)
279
279
280 @reraise_safe_exceptions
280 @reraise_safe_exceptions
281 def ctx_date(self, wire, commit_id):
281 def ctx_date(self, wire, commit_id):
282 cache_on, context_uid, repo_id = self._cache_on(wire)
282 cache_on, context_uid, repo_id = self._cache_on(wire)
283 region = self._region(wire)
283 region = self._region(wire)
284
284
285 @region.conditional_cache_on_arguments(condition=cache_on)
285 @region.conditional_cache_on_arguments(condition=cache_on)
286 def _ctx_date(_repo_id, _commit_id):
286 def _ctx_date(_repo_id, _commit_id):
287 repo = self._factory.repo(wire)
287 repo = self._factory.repo(wire)
288 ctx = self._get_ctx(repo, commit_id)
288 ctx = self._get_ctx(repo, commit_id)
289 return ctx.date()
289 return ctx.date()
290 return _ctx_date(repo_id, commit_id)
290 return _ctx_date(repo_id, commit_id)
291
291
292 @reraise_safe_exceptions
292 @reraise_safe_exceptions
293 def ctx_description(self, wire, revision):
293 def ctx_description(self, wire, revision):
294 repo = self._factory.repo(wire)
294 repo = self._factory.repo(wire)
295 ctx = self._get_ctx(repo, revision)
295 ctx = self._get_ctx(repo, revision)
296 return ctx.description()
296 return ctx.description()
297
297
298 @reraise_safe_exceptions
298 @reraise_safe_exceptions
299 def ctx_files(self, wire, commit_id):
299 def ctx_files(self, wire, commit_id):
300 cache_on, context_uid, repo_id = self._cache_on(wire)
300 cache_on, context_uid, repo_id = self._cache_on(wire)
301 region = self._region(wire)
301 region = self._region(wire)
302
302
303 @region.conditional_cache_on_arguments(condition=cache_on)
303 @region.conditional_cache_on_arguments(condition=cache_on)
304 def _ctx_files(_repo_id, _commit_id):
304 def _ctx_files(_repo_id, _commit_id):
305 repo = self._factory.repo(wire)
305 repo = self._factory.repo(wire)
306 ctx = self._get_ctx(repo, commit_id)
306 ctx = self._get_ctx(repo, commit_id)
307 return ctx.files()
307 return ctx.files()
308
308
309 return _ctx_files(repo_id, commit_id)
309 return _ctx_files(repo_id, commit_id)
310
310
311 @reraise_safe_exceptions
311 @reraise_safe_exceptions
312 def ctx_list(self, path, revision):
312 def ctx_list(self, path, revision):
313 repo = self._factory.repo(path)
313 repo = self._factory.repo(path)
314 ctx = self._get_ctx(repo, revision)
314 ctx = self._get_ctx(repo, revision)
315 return list(ctx)
315 return list(ctx)
316
316
317 @reraise_safe_exceptions
317 @reraise_safe_exceptions
318 def ctx_parents(self, wire, commit_id):
318 def ctx_parents(self, wire, commit_id):
319 cache_on, context_uid, repo_id = self._cache_on(wire)
319 cache_on, context_uid, repo_id = self._cache_on(wire)
320 region = self._region(wire)
320 region = self._region(wire)
321
321
322 @region.conditional_cache_on_arguments(condition=cache_on)
322 @region.conditional_cache_on_arguments(condition=cache_on)
323 def _ctx_parents(_repo_id, _commit_id):
323 def _ctx_parents(_repo_id, _commit_id):
324 repo = self._factory.repo(wire)
324 repo = self._factory.repo(wire)
325 ctx = self._get_ctx(repo, commit_id)
325 ctx = self._get_ctx(repo, commit_id)
326 return [parent.hex() for parent in ctx.parents()
326 return [parent.hex() for parent in ctx.parents()
327 if not (parent.hidden() or parent.obsolete())]
327 if not (parent.hidden() or parent.obsolete())]
328
328
329 return _ctx_parents(repo_id, commit_id)
329 return _ctx_parents(repo_id, commit_id)
330
330
331 @reraise_safe_exceptions
331 @reraise_safe_exceptions
332 def ctx_children(self, wire, commit_id):
332 def ctx_children(self, wire, commit_id):
333 cache_on, context_uid, repo_id = self._cache_on(wire)
333 cache_on, context_uid, repo_id = self._cache_on(wire)
334 region = self._region(wire)
334 region = self._region(wire)
335
335
336 @region.conditional_cache_on_arguments(condition=cache_on)
336 @region.conditional_cache_on_arguments(condition=cache_on)
337 def _ctx_children(_repo_id, _commit_id):
337 def _ctx_children(_repo_id, _commit_id):
338 repo = self._factory.repo(wire)
338 repo = self._factory.repo(wire)
339 ctx = self._get_ctx(repo, commit_id)
339 ctx = self._get_ctx(repo, commit_id)
340 return [child.hex() for child in ctx.children()
340 return [child.hex() for child in ctx.children()
341 if not (child.hidden() or child.obsolete())]
341 if not (child.hidden() or child.obsolete())]
342
342
343 return _ctx_children(repo_id, commit_id)
343 return _ctx_children(repo_id, commit_id)
344
344
345 @reraise_safe_exceptions
345 @reraise_safe_exceptions
346 def ctx_phase(self, wire, commit_id):
346 def ctx_phase(self, wire, commit_id):
347 cache_on, context_uid, repo_id = self._cache_on(wire)
347 cache_on, context_uid, repo_id = self._cache_on(wire)
348 region = self._region(wire)
348 region = self._region(wire)
349
349
350 @region.conditional_cache_on_arguments(condition=cache_on)
350 @region.conditional_cache_on_arguments(condition=cache_on)
351 def _ctx_phase(_context_uid, _repo_id, _commit_id):
351 def _ctx_phase(_context_uid, _repo_id, _commit_id):
352 repo = self._factory.repo(wire)
352 repo = self._factory.repo(wire)
353 ctx = self._get_ctx(repo, commit_id)
353 ctx = self._get_ctx(repo, commit_id)
354 # public=0, draft=1, secret=3
354 # public=0, draft=1, secret=3
355 return ctx.phase()
355 return ctx.phase()
356 return _ctx_phase(context_uid, repo_id, commit_id)
356 return _ctx_phase(context_uid, repo_id, commit_id)
357
357
358 @reraise_safe_exceptions
358 @reraise_safe_exceptions
359 def ctx_obsolete(self, wire, commit_id):
359 def ctx_obsolete(self, wire, commit_id):
360 cache_on, context_uid, repo_id = self._cache_on(wire)
360 cache_on, context_uid, repo_id = self._cache_on(wire)
361 region = self._region(wire)
361 region = self._region(wire)
362
362
363 @region.conditional_cache_on_arguments(condition=cache_on)
363 @region.conditional_cache_on_arguments(condition=cache_on)
364 def _ctx_obsolete(_context_uid, _repo_id, _commit_id):
364 def _ctx_obsolete(_context_uid, _repo_id, _commit_id):
365 repo = self._factory.repo(wire)
365 repo = self._factory.repo(wire)
366 ctx = self._get_ctx(repo, commit_id)
366 ctx = self._get_ctx(repo, commit_id)
367 return ctx.obsolete()
367 return ctx.obsolete()
368 return _ctx_obsolete(context_uid, repo_id, commit_id)
368 return _ctx_obsolete(context_uid, repo_id, commit_id)
369
369
370 @reraise_safe_exceptions
370 @reraise_safe_exceptions
371 def ctx_hidden(self, wire, commit_id):
371 def ctx_hidden(self, wire, commit_id):
372 cache_on, context_uid, repo_id = self._cache_on(wire)
372 cache_on, context_uid, repo_id = self._cache_on(wire)
373 region = self._region(wire)
373 region = self._region(wire)
374
374
375 @region.conditional_cache_on_arguments(condition=cache_on)
375 @region.conditional_cache_on_arguments(condition=cache_on)
376 def _ctx_hidden(_context_uid, _repo_id, _commit_id):
376 def _ctx_hidden(_context_uid, _repo_id, _commit_id):
377 repo = self._factory.repo(wire)
377 repo = self._factory.repo(wire)
378 ctx = self._get_ctx(repo, commit_id)
378 ctx = self._get_ctx(repo, commit_id)
379 return ctx.hidden()
379 return ctx.hidden()
380 return _ctx_hidden(context_uid, repo_id, commit_id)
380 return _ctx_hidden(context_uid, repo_id, commit_id)
381
381
382 @reraise_safe_exceptions
382 @reraise_safe_exceptions
383 def ctx_substate(self, wire, revision):
383 def ctx_substate(self, wire, revision):
384 repo = self._factory.repo(wire)
384 repo = self._factory.repo(wire)
385 ctx = self._get_ctx(repo, revision)
385 ctx = self._get_ctx(repo, revision)
386 return ctx.substate
386 return ctx.substate
387
387
388 @reraise_safe_exceptions
388 @reraise_safe_exceptions
389 def ctx_status(self, wire, revision):
389 def ctx_status(self, wire, revision):
390 repo = self._factory.repo(wire)
390 repo = self._factory.repo(wire)
391 ctx = self._get_ctx(repo, revision)
391 ctx = self._get_ctx(repo, revision)
392 status = repo[ctx.p1().node()].status(other=ctx.node())
392 status = repo[ctx.p1().node()].status(other=ctx.node())
393 # object of status (odd, custom named tuple in mercurial) is not
393 # object of status (odd, custom named tuple in mercurial) is not
394 # correctly serializable, we make it a list, as the underling
394 # correctly serializable, we make it a list, as the underling
395 # API expects this to be a list
395 # API expects this to be a list
396 return list(status)
396 return list(status)
397
397
398 @reraise_safe_exceptions
398 @reraise_safe_exceptions
399 def ctx_user(self, wire, revision):
399 def ctx_user(self, wire, revision):
400 repo = self._factory.repo(wire)
400 repo = self._factory.repo(wire)
401 ctx = self._get_ctx(repo, revision)
401 ctx = self._get_ctx(repo, revision)
402 return ctx.user()
402 return ctx.user()
403
403
404 @reraise_safe_exceptions
404 @reraise_safe_exceptions
405 def check_url(self, url, config):
405 def check_url(self, url, config):
406 _proto = None
406 _proto = None
407 if '+' in url[:url.find('://')]:
407 if '+' in url[:url.find('://')]:
408 _proto = url[0:url.find('+')]
408 _proto = url[0:url.find('+')]
409 url = url[url.find('+') + 1:]
409 url = url[url.find('+') + 1:]
410 handlers = []
410 handlers = []
411 url_obj = url_parser(url)
411 url_obj = url_parser(url)
412 test_uri, authinfo = url_obj.authinfo()
412 test_uri, authinfo = url_obj.authinfo()
413 url_obj.passwd = '*****' if url_obj.passwd else url_obj.passwd
413 url_obj.passwd = '*****' if url_obj.passwd else url_obj.passwd
414 url_obj.query = obfuscate_qs(url_obj.query)
414 url_obj.query = obfuscate_qs(url_obj.query)
415
415
416 cleaned_uri = str(url_obj)
416 cleaned_uri = str(url_obj)
417 log.info("Checking URL for remote cloning/import: %s", cleaned_uri)
417 log.info("Checking URL for remote cloning/import: %s", cleaned_uri)
418
418
419 if authinfo:
419 if authinfo:
420 # create a password manager
420 # create a password manager
421 passmgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()
421 passmgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()
422 passmgr.add_password(*authinfo)
422 passmgr.add_password(*authinfo)
423
423
424 handlers.extend((httpbasicauthhandler(passmgr),
424 handlers.extend((httpbasicauthhandler(passmgr),
425 httpdigestauthhandler(passmgr)))
425 httpdigestauthhandler(passmgr)))
426
426
427 o = urllib.request.build_opener(*handlers)
427 o = urllib.request.build_opener(*handlers)
428 o.addheaders = [('Content-Type', 'application/mercurial-0.1'),
428 o.addheaders = [('Content-Type', 'application/mercurial-0.1'),
429 ('Accept', 'application/mercurial-0.1')]
429 ('Accept', 'application/mercurial-0.1')]
430
430
431 q = {"cmd": 'between'}
431 q = {"cmd": 'between'}
432 q.update({'pairs': "%s-%s" % ('0' * 40, '0' * 40)})
432 q.update({'pairs': "%s-%s" % ('0' * 40, '0' * 40)})
433 qs = '?%s' % urllib.parse.urlencode(q)
433 qs = '?%s' % urllib.parse.urlencode(q)
434 cu = "%s%s" % (test_uri, qs)
434 cu = "%s%s" % (test_uri, qs)
435 req = urllib.request.Request(cu, None, {})
435 req = urllib.request.Request(cu, None, {})
436
436
437 try:
437 try:
438 log.debug("Trying to open URL %s", cleaned_uri)
438 log.debug("Trying to open URL %s", cleaned_uri)
439 resp = o.open(req)
439 resp = o.open(req)
440 if resp.code != 200:
440 if resp.code != 200:
441 raise exceptions.URLError()('Return Code is not 200')
441 raise exceptions.URLError()('Return Code is not 200')
442 except Exception as e:
442 except Exception as e:
443 log.warning("URL cannot be opened: %s", cleaned_uri, exc_info=True)
443 log.warning("URL cannot be opened: %s", cleaned_uri, exc_info=True)
444 # means it cannot be cloned
444 # means it cannot be cloned
445 raise exceptions.URLError(e)("[%s] org_exc: %s" % (cleaned_uri, e))
445 raise exceptions.URLError(e)("[%s] org_exc: %s" % (cleaned_uri, e))
446
446
447 # now check if it's a proper hg repo, but don't do it for svn
447 # now check if it's a proper hg repo, but don't do it for svn
448 try:
448 try:
449 if _proto == 'svn':
449 if _proto == 'svn':
450 pass
450 pass
451 else:
451 else:
452 # check for pure hg repos
452 # check for pure hg repos
453 log.debug(
453 log.debug(
454 "Verifying if URL is a Mercurial repository: %s",
454 "Verifying if URL is a Mercurial repository: %s",
455 cleaned_uri)
455 cleaned_uri)
456 ui = make_ui_from_config(config)
456 ui = make_ui_from_config(config)
457 peer_checker = makepeer(ui, url)
457 peer_checker = makepeer(ui, url)
458 peer_checker.lookup('tip')
458 peer_checker.lookup('tip')
459 except Exception as e:
459 except Exception as e:
460 log.warning("URL is not a valid Mercurial repository: %s",
460 log.warning("URL is not a valid Mercurial repository: %s",
461 cleaned_uri)
461 cleaned_uri)
462 raise exceptions.URLError(e)(
462 raise exceptions.URLError(e)(
463 "url [%s] does not look like an hg repo org_exc: %s"
463 "url [%s] does not look like an hg repo org_exc: %s"
464 % (cleaned_uri, e))
464 % (cleaned_uri, e))
465
465
466 log.info("URL is a valid Mercurial repository: %s", cleaned_uri)
466 log.info("URL is a valid Mercurial repository: %s", cleaned_uri)
467 return True
467 return True
468
468
469 @reraise_safe_exceptions
469 @reraise_safe_exceptions
470 def diff(self, wire, commit_id_1, commit_id_2, file_filter, opt_git, opt_ignorews, context):
470 def diff(self, wire, commit_id_1, commit_id_2, file_filter, opt_git, opt_ignorews, context):
471 repo = self._factory.repo(wire)
471 repo = self._factory.repo(wire)
472
472
473 if file_filter:
473 if file_filter:
474 match_filter = match(file_filter[0], '', [file_filter[1]])
474 # unpack the file-filter
475 repo_path, node_path = file_filter
476 match_filter = match(safe_bytes(repo_path), b'', [safe_bytes(node_path)])
475 else:
477 else:
476 match_filter = file_filter
478 match_filter = file_filter
477 opts = diffopts(git=opt_git, ignorews=opt_ignorews, context=context, showfunc=1)
479 opts = diffopts(git=opt_git, ignorews=opt_ignorews, context=context, showfunc=1)
478
480
479 try:
481 try:
480 diff_iter = patch.diff(
482 diff_iter = patch.diff(
481 repo, node1=commit_id_1, node2=commit_id_2, match=match_filter, opts=opts)
483 repo, node1=commit_id_1, node2=commit_id_2, match=match_filter, opts=opts)
482 return b"".join(diff_iter)
484 return b"".join(diff_iter)
483 except RepoLookupError as e:
485 except RepoLookupError as e:
484 raise exceptions.LookupException(e)()
486 raise exceptions.LookupException(e)()
485
487
486 @reraise_safe_exceptions
488 @reraise_safe_exceptions
487 def node_history(self, wire, revision, path, limit):
489 def node_history(self, wire, revision, path, limit):
488 cache_on, context_uid, repo_id = self._cache_on(wire)
490 cache_on, context_uid, repo_id = self._cache_on(wire)
489 region = self._region(wire)
491 region = self._region(wire)
490
492
491 @region.conditional_cache_on_arguments(condition=cache_on)
493 @region.conditional_cache_on_arguments(condition=cache_on)
492 def _node_history(_context_uid, _repo_id, _revision, _path, _limit):
494 def _node_history(_context_uid, _repo_id, _revision, _path, _limit):
493 repo = self._factory.repo(wire)
495 repo = self._factory.repo(wire)
494
496
495 ctx = self._get_ctx(repo, revision)
497 ctx = self._get_ctx(repo, revision)
496 fctx = ctx.filectx(safe_bytes(path))
498 fctx = ctx.filectx(safe_bytes(path))
497
499
498 def history_iter():
500 def history_iter():
499 limit_rev = fctx.rev()
501 limit_rev = fctx.rev()
500 for obj in reversed(list(fctx.filelog())):
502 for obj in reversed(list(fctx.filelog())):
501 obj = fctx.filectx(obj)
503 obj = fctx.filectx(obj)
502 ctx = obj.changectx()
504 ctx = obj.changectx()
503 if ctx.hidden() or ctx.obsolete():
505 if ctx.hidden() or ctx.obsolete():
504 continue
506 continue
505
507
506 if limit_rev >= obj.rev():
508 if limit_rev >= obj.rev():
507 yield obj
509 yield obj
508
510
509 history = []
511 history = []
510 for cnt, obj in enumerate(history_iter()):
512 for cnt, obj in enumerate(history_iter()):
511 if limit and cnt >= limit:
513 if limit and cnt >= limit:
512 break
514 break
513 history.append(hex(obj.node()))
515 history.append(hex(obj.node()))
514
516
515 return [x for x in history]
517 return [x for x in history]
516 return _node_history(context_uid, repo_id, revision, path, limit)
518 return _node_history(context_uid, repo_id, revision, path, limit)
517
519
518 @reraise_safe_exceptions
520 @reraise_safe_exceptions
519 def node_history_untill(self, wire, revision, path, limit):
521 def node_history_untill(self, wire, revision, path, limit):
520 cache_on, context_uid, repo_id = self._cache_on(wire)
522 cache_on, context_uid, repo_id = self._cache_on(wire)
521 region = self._region(wire)
523 region = self._region(wire)
522
524
523 @region.conditional_cache_on_arguments(condition=cache_on)
525 @region.conditional_cache_on_arguments(condition=cache_on)
524 def _node_history_until(_context_uid, _repo_id):
526 def _node_history_until(_context_uid, _repo_id):
525 repo = self._factory.repo(wire)
527 repo = self._factory.repo(wire)
526 ctx = self._get_ctx(repo, revision)
528 ctx = self._get_ctx(repo, revision)
527 fctx = ctx.filectx(safe_bytes(path))
529 fctx = ctx.filectx(safe_bytes(path))
528
530
529 file_log = list(fctx.filelog())
531 file_log = list(fctx.filelog())
530 if limit:
532 if limit:
531 # Limit to the last n items
533 # Limit to the last n items
532 file_log = file_log[-limit:]
534 file_log = file_log[-limit:]
533
535
534 return [hex(fctx.filectx(cs).node()) for cs in reversed(file_log)]
536 return [hex(fctx.filectx(cs).node()) for cs in reversed(file_log)]
535 return _node_history_until(context_uid, repo_id, revision, path, limit)
537 return _node_history_until(context_uid, repo_id, revision, path, limit)
536
538
537 @reraise_safe_exceptions
539 @reraise_safe_exceptions
538 def fctx_annotate(self, wire, revision, path):
540 def fctx_annotate(self, wire, revision, path):
539 repo = self._factory.repo(wire)
541 repo = self._factory.repo(wire)
540 ctx = self._get_ctx(repo, revision)
542 ctx = self._get_ctx(repo, revision)
541 fctx = ctx.filectx(safe_bytes(path))
543 fctx = ctx.filectx(safe_bytes(path))
542
544
543 result = []
545 result = []
544 for i, annotate_obj in enumerate(fctx.annotate(), 1):
546 for i, annotate_obj in enumerate(fctx.annotate(), 1):
545 ln_no = i
547 ln_no = i
546 sha = hex(annotate_obj.fctx.node())
548 sha = hex(annotate_obj.fctx.node())
547 content = annotate_obj.text
549 content = annotate_obj.text
548 result.append((ln_no, sha, content))
550 result.append((ln_no, sha, content))
549 return result
551 return result
550
552
551 @reraise_safe_exceptions
553 @reraise_safe_exceptions
552 def fctx_node_data(self, wire, revision, path):
554 def fctx_node_data(self, wire, revision, path):
553 repo = self._factory.repo(wire)
555 repo = self._factory.repo(wire)
554 ctx = self._get_ctx(repo, revision)
556 ctx = self._get_ctx(repo, revision)
555 fctx = ctx.filectx(safe_bytes(path))
557 fctx = ctx.filectx(safe_bytes(path))
556 return fctx.data()
558 return fctx.data()
557
559
558 @reraise_safe_exceptions
560 @reraise_safe_exceptions
559 def fctx_flags(self, wire, commit_id, path):
561 def fctx_flags(self, wire, commit_id, path):
560 cache_on, context_uid, repo_id = self._cache_on(wire)
562 cache_on, context_uid, repo_id = self._cache_on(wire)
561 region = self._region(wire)
563 region = self._region(wire)
562
564
563 @region.conditional_cache_on_arguments(condition=cache_on)
565 @region.conditional_cache_on_arguments(condition=cache_on)
564 def _fctx_flags(_repo_id, _commit_id, _path):
566 def _fctx_flags(_repo_id, _commit_id, _path):
565 repo = self._factory.repo(wire)
567 repo = self._factory.repo(wire)
566 ctx = self._get_ctx(repo, commit_id)
568 ctx = self._get_ctx(repo, commit_id)
567 fctx = ctx.filectx(safe_bytes(path))
569 fctx = ctx.filectx(safe_bytes(path))
568 return fctx.flags()
570 return fctx.flags()
569
571
570 return _fctx_flags(repo_id, commit_id, path)
572 return _fctx_flags(repo_id, commit_id, path)
571
573
572 @reraise_safe_exceptions
574 @reraise_safe_exceptions
573 def fctx_size(self, wire, commit_id, path):
575 def fctx_size(self, wire, commit_id, path):
574 cache_on, context_uid, repo_id = self._cache_on(wire)
576 cache_on, context_uid, repo_id = self._cache_on(wire)
575 region = self._region(wire)
577 region = self._region(wire)
576
578
577 @region.conditional_cache_on_arguments(condition=cache_on)
579 @region.conditional_cache_on_arguments(condition=cache_on)
578 def _fctx_size(_repo_id, _revision, _path):
580 def _fctx_size(_repo_id, _revision, _path):
579 repo = self._factory.repo(wire)
581 repo = self._factory.repo(wire)
580 ctx = self._get_ctx(repo, commit_id)
582 ctx = self._get_ctx(repo, commit_id)
581 fctx = ctx.filectx(safe_bytes(path))
583 fctx = ctx.filectx(safe_bytes(path))
582 return fctx.size()
584 return fctx.size()
583 return _fctx_size(repo_id, commit_id, path)
585 return _fctx_size(repo_id, commit_id, path)
584
586
585 @reraise_safe_exceptions
587 @reraise_safe_exceptions
586 def get_all_commit_ids(self, wire, name):
588 def get_all_commit_ids(self, wire, name):
587 cache_on, context_uid, repo_id = self._cache_on(wire)
589 cache_on, context_uid, repo_id = self._cache_on(wire)
588 region = self._region(wire)
590 region = self._region(wire)
589
591
590 @region.conditional_cache_on_arguments(condition=cache_on)
592 @region.conditional_cache_on_arguments(condition=cache_on)
591 def _get_all_commit_ids(_context_uid, _repo_id, _name):
593 def _get_all_commit_ids(_context_uid, _repo_id, _name):
592 repo = self._factory.repo(wire)
594 repo = self._factory.repo(wire)
593 revs = [ascii_str(repo[x].hex()) for x in repo.filtered(b'visible').changelog.revs()]
595 revs = [ascii_str(repo[x].hex()) for x in repo.filtered(b'visible').changelog.revs()]
594 return revs
596 return revs
595 return _get_all_commit_ids(context_uid, repo_id, name)
597 return _get_all_commit_ids(context_uid, repo_id, name)
596
598
597 @reraise_safe_exceptions
599 @reraise_safe_exceptions
598 def get_config_value(self, wire, section, name, untrusted=False):
600 def get_config_value(self, wire, section, name, untrusted=False):
599 repo = self._factory.repo(wire)
601 repo = self._factory.repo(wire)
600 return repo.ui.config(ascii_bytes(section), ascii_bytes(name), untrusted=untrusted)
602 return repo.ui.config(ascii_bytes(section), ascii_bytes(name), untrusted=untrusted)
601
603
602 @reraise_safe_exceptions
604 @reraise_safe_exceptions
603 def is_large_file(self, wire, commit_id, path):
605 def is_large_file(self, wire, commit_id, path):
604 cache_on, context_uid, repo_id = self._cache_on(wire)
606 cache_on, context_uid, repo_id = self._cache_on(wire)
605 region = self._region(wire)
607 region = self._region(wire)
606
608
607 @region.conditional_cache_on_arguments(condition=cache_on)
609 @region.conditional_cache_on_arguments(condition=cache_on)
608 def _is_large_file(_context_uid, _repo_id, _commit_id, _path):
610 def _is_large_file(_context_uid, _repo_id, _commit_id, _path):
609 return largefiles.lfutil.isstandin(safe_bytes(path))
611 return largefiles.lfutil.isstandin(safe_bytes(path))
610
612
611 return _is_large_file(context_uid, repo_id, commit_id, path)
613 return _is_large_file(context_uid, repo_id, commit_id, path)
612
614
613 @reraise_safe_exceptions
615 @reraise_safe_exceptions
614 def is_binary(self, wire, revision, path):
616 def is_binary(self, wire, revision, path):
615 cache_on, context_uid, repo_id = self._cache_on(wire)
617 cache_on, context_uid, repo_id = self._cache_on(wire)
616 region = self._region(wire)
618 region = self._region(wire)
617
619
618 @region.conditional_cache_on_arguments(condition=cache_on)
620 @region.conditional_cache_on_arguments(condition=cache_on)
619 def _is_binary(_repo_id, _sha, _path):
621 def _is_binary(_repo_id, _sha, _path):
620 repo = self._factory.repo(wire)
622 repo = self._factory.repo(wire)
621 ctx = self._get_ctx(repo, revision)
623 ctx = self._get_ctx(repo, revision)
622 fctx = ctx.filectx(safe_bytes(path))
624 fctx = ctx.filectx(safe_bytes(path))
623 return fctx.isbinary()
625 return fctx.isbinary()
624
626
625 return _is_binary(repo_id, revision, path)
627 return _is_binary(repo_id, revision, path)
626
628
627 @reraise_safe_exceptions
629 @reraise_safe_exceptions
628 def md5_hash(self, wire, revision, path):
630 def md5_hash(self, wire, revision, path):
629 cache_on, context_uid, repo_id = self._cache_on(wire)
631 cache_on, context_uid, repo_id = self._cache_on(wire)
630 region = self._region(wire)
632 region = self._region(wire)
631
633
632 @region.conditional_cache_on_arguments(condition=cache_on)
634 @region.conditional_cache_on_arguments(condition=cache_on)
633 def _md5_hash(_repo_id, _sha, _path):
635 def _md5_hash(_repo_id, _sha, _path):
634 repo = self._factory.repo(wire)
636 repo = self._factory.repo(wire)
635 ctx = self._get_ctx(repo, revision)
637 ctx = self._get_ctx(repo, revision)
636 fctx = ctx.filectx(safe_bytes(path))
638 fctx = ctx.filectx(safe_bytes(path))
637 return hashlib.md5(fctx.data()).hexdigest()
639 return hashlib.md5(fctx.data()).hexdigest()
638
640
639 return _md5_hash(repo_id, revision, path)
641 return _md5_hash(repo_id, revision, path)
640
642
641 @reraise_safe_exceptions
643 @reraise_safe_exceptions
642 def in_largefiles_store(self, wire, sha):
644 def in_largefiles_store(self, wire, sha):
643 repo = self._factory.repo(wire)
645 repo = self._factory.repo(wire)
644 return largefiles.lfutil.instore(repo, sha)
646 return largefiles.lfutil.instore(repo, sha)
645
647
646 @reraise_safe_exceptions
648 @reraise_safe_exceptions
647 def in_user_cache(self, wire, sha):
649 def in_user_cache(self, wire, sha):
648 repo = self._factory.repo(wire)
650 repo = self._factory.repo(wire)
649 return largefiles.lfutil.inusercache(repo.ui, sha)
651 return largefiles.lfutil.inusercache(repo.ui, sha)
650
652
651 @reraise_safe_exceptions
653 @reraise_safe_exceptions
652 def store_path(self, wire, sha):
654 def store_path(self, wire, sha):
653 repo = self._factory.repo(wire)
655 repo = self._factory.repo(wire)
654 return largefiles.lfutil.storepath(repo, sha)
656 return largefiles.lfutil.storepath(repo, sha)
655
657
656 @reraise_safe_exceptions
658 @reraise_safe_exceptions
657 def link(self, wire, sha, path):
659 def link(self, wire, sha, path):
658 repo = self._factory.repo(wire)
660 repo = self._factory.repo(wire)
659 largefiles.lfutil.link(
661 largefiles.lfutil.link(
660 largefiles.lfutil.usercachepath(repo.ui, sha), path)
662 largefiles.lfutil.usercachepath(repo.ui, sha), path)
661
663
662 @reraise_safe_exceptions
664 @reraise_safe_exceptions
663 def localrepository(self, wire, create=False):
665 def localrepository(self, wire, create=False):
664 self._factory.repo(wire, create=create)
666 self._factory.repo(wire, create=create)
665
667
666 @reraise_safe_exceptions
668 @reraise_safe_exceptions
667 def lookup(self, wire, revision, both):
669 def lookup(self, wire, revision, both):
668 cache_on, context_uid, repo_id = self._cache_on(wire)
670 cache_on, context_uid, repo_id = self._cache_on(wire)
669 region = self._region(wire)
671 region = self._region(wire)
670
672
671 @region.conditional_cache_on_arguments(condition=cache_on)
673 @region.conditional_cache_on_arguments(condition=cache_on)
672 def _lookup(_context_uid, _repo_id, _revision, _both):
674 def _lookup(_context_uid, _repo_id, _revision, _both):
673
675
674 repo = self._factory.repo(wire)
676 repo = self._factory.repo(wire)
675 rev = _revision
677 rev = _revision
676 if isinstance(rev, int):
678 if isinstance(rev, int):
677 # NOTE(marcink):
679 # NOTE(marcink):
678 # since Mercurial doesn't support negative indexes properly
680 # since Mercurial doesn't support negative indexes properly
679 # we need to shift accordingly by one to get proper index, e.g
681 # we need to shift accordingly by one to get proper index, e.g
680 # repo[-1] => repo[-2]
682 # repo[-1] => repo[-2]
681 # repo[0] => repo[-1]
683 # repo[0] => repo[-1]
682 if rev <= 0:
684 if rev <= 0:
683 rev = rev + -1
685 rev = rev + -1
684 try:
686 try:
685 ctx = self._get_ctx(repo, rev)
687 ctx = self._get_ctx(repo, rev)
686 except (TypeError, RepoLookupError) as e:
688 except (TypeError, RepoLookupError, binascii.Error) as e:
687 e._org_exc_tb = traceback.format_exc()
689 e._org_exc_tb = traceback.format_exc()
688 raise exceptions.LookupException(e)(rev)
690 raise exceptions.LookupException(e)(rev)
689 except LookupError as e:
691 except LookupError as e:
690 e._org_exc_tb = traceback.format_exc()
692 e._org_exc_tb = traceback.format_exc()
691 raise exceptions.LookupException(e)(e.name)
693 raise exceptions.LookupException(e)(e.name)
692
694
693 if not both:
695 if not both:
694 return ctx.hex()
696 return ctx.hex()
695
697
696 ctx = repo[ctx.hex()]
698 ctx = repo[ctx.hex()]
697 return ctx.hex(), ctx.rev()
699 return ctx.hex(), ctx.rev()
698
700
699 return _lookup(context_uid, repo_id, revision, both)
701 return _lookup(context_uid, repo_id, revision, both)
700
702
701 @reraise_safe_exceptions
703 @reraise_safe_exceptions
702 def sync_push(self, wire, url):
704 def sync_push(self, wire, url):
703 if not self.check_url(url, wire['config']):
705 if not self.check_url(url, wire['config']):
704 return
706 return
705
707
706 repo = self._factory.repo(wire)
708 repo = self._factory.repo(wire)
707
709
708 # Disable any prompts for this repo
710 # Disable any prompts for this repo
709 repo.ui.setconfig(b'ui', b'interactive', b'off', b'-y')
711 repo.ui.setconfig(b'ui', b'interactive', b'off', b'-y')
710
712
711 bookmarks = list(dict(repo._bookmarks).keys())
713 bookmarks = list(dict(repo._bookmarks).keys())
712 remote = peer(repo, {}, safe_bytes(url))
714 remote = peer(repo, {}, safe_bytes(url))
713 # Disable any prompts for this remote
715 # Disable any prompts for this remote
714 remote.ui.setconfig(b'ui', b'interactive', b'off', b'-y')
716 remote.ui.setconfig(b'ui', b'interactive', b'off', b'-y')
715
717
716 return exchange.push(
718 return exchange.push(
717 repo, remote, newbranch=True, bookmarks=bookmarks).cgresult
719 repo, remote, newbranch=True, bookmarks=bookmarks).cgresult
718
720
719 @reraise_safe_exceptions
721 @reraise_safe_exceptions
720 def revision(self, wire, rev):
722 def revision(self, wire, rev):
721 repo = self._factory.repo(wire)
723 repo = self._factory.repo(wire)
722 ctx = self._get_ctx(repo, rev)
724 ctx = self._get_ctx(repo, rev)
723 return ctx.rev()
725 return ctx.rev()
724
726
725 @reraise_safe_exceptions
727 @reraise_safe_exceptions
726 def rev_range(self, wire, commit_filter):
728 def rev_range(self, wire, commit_filter):
727 cache_on, context_uid, repo_id = self._cache_on(wire)
729 cache_on, context_uid, repo_id = self._cache_on(wire)
728 region = self._region(wire)
730 region = self._region(wire)
729
731
730 @region.conditional_cache_on_arguments(condition=cache_on)
732 @region.conditional_cache_on_arguments(condition=cache_on)
731 def _rev_range(_context_uid, _repo_id, _filter):
733 def _rev_range(_context_uid, _repo_id, _filter):
732 repo = self._factory.repo(wire)
734 repo = self._factory.repo(wire)
733 revisions = [
735 revisions = [
734 ascii_str(repo[rev].hex())
736 ascii_str(repo[rev].hex())
735 for rev in revrange(repo, list(map(ascii_bytes, commit_filter)))
737 for rev in revrange(repo, list(map(ascii_bytes, commit_filter)))
736 ]
738 ]
737 return revisions
739 return revisions
738
740
739 return _rev_range(context_uid, repo_id, sorted(commit_filter))
741 return _rev_range(context_uid, repo_id, sorted(commit_filter))
740
742
741 @reraise_safe_exceptions
743 @reraise_safe_exceptions
742 def rev_range_hash(self, wire, node):
744 def rev_range_hash(self, wire, node):
743 repo = self._factory.repo(wire)
745 repo = self._factory.repo(wire)
744
746
745 def get_revs(repo, rev_opt):
747 def get_revs(repo, rev_opt):
746 if rev_opt:
748 if rev_opt:
747 revs = revrange(repo, rev_opt)
749 revs = revrange(repo, rev_opt)
748 if len(revs) == 0:
750 if len(revs) == 0:
749 return (nullrev, nullrev)
751 return (nullrev, nullrev)
750 return max(revs), min(revs)
752 return max(revs), min(revs)
751 else:
753 else:
752 return len(repo) - 1, 0
754 return len(repo) - 1, 0
753
755
754 stop, start = get_revs(repo, [node + ':'])
756 stop, start = get_revs(repo, [node + ':'])
755 revs = [ascii_str(repo[r].hex()) for r in range(start, stop + 1)]
757 revs = [ascii_str(repo[r].hex()) for r in range(start, stop + 1)]
756 return revs
758 return revs
757
759
758 @reraise_safe_exceptions
760 @reraise_safe_exceptions
759 def revs_from_revspec(self, wire, rev_spec, *args, **kwargs):
761 def revs_from_revspec(self, wire, rev_spec, *args, **kwargs):
760 org_path = safe_bytes(wire["path"])
762 org_path = safe_bytes(wire["path"])
761 other_path = safe_bytes(kwargs.pop('other_path', ''))
763 other_path = safe_bytes(kwargs.pop('other_path', ''))
762
764
763 # case when we want to compare two independent repositories
765 # case when we want to compare two independent repositories
764 if other_path and other_path != wire["path"]:
766 if other_path and other_path != wire["path"]:
765 baseui = self._factory._create_config(wire["config"])
767 baseui = self._factory._create_config(wire["config"])
766 repo = unionrepo.makeunionrepository(baseui, other_path, org_path)
768 repo = unionrepo.makeunionrepository(baseui, other_path, org_path)
767 else:
769 else:
768 repo = self._factory.repo(wire)
770 repo = self._factory.repo(wire)
769 return list(repo.revs(rev_spec, *args))
771 return list(repo.revs(rev_spec, *args))
770
772
771 @reraise_safe_exceptions
773 @reraise_safe_exceptions
772 def verify(self, wire,):
774 def verify(self, wire,):
773 repo = self._factory.repo(wire)
775 repo = self._factory.repo(wire)
774 baseui = self._factory._create_config(wire['config'])
776 baseui = self._factory._create_config(wire['config'])
775
777
776 baseui, output = patch_ui_message_output(baseui)
778 baseui, output = patch_ui_message_output(baseui)
777
779
778 repo.ui = baseui
780 repo.ui = baseui
779 verify.verify(repo)
781 verify.verify(repo)
780 return output.getvalue()
782 return output.getvalue()
781
783
782 @reraise_safe_exceptions
784 @reraise_safe_exceptions
783 def hg_update_cache(self, wire,):
785 def hg_update_cache(self, wire,):
784 repo = self._factory.repo(wire)
786 repo = self._factory.repo(wire)
785 baseui = self._factory._create_config(wire['config'])
787 baseui = self._factory._create_config(wire['config'])
786 baseui, output = patch_ui_message_output(baseui)
788 baseui, output = patch_ui_message_output(baseui)
787
789
788 repo.ui = baseui
790 repo.ui = baseui
789 with repo.wlock(), repo.lock():
791 with repo.wlock(), repo.lock():
790 repo.updatecaches(full=True)
792 repo.updatecaches(full=True)
791
793
792 return output.getvalue()
794 return output.getvalue()
793
795
794 @reraise_safe_exceptions
796 @reraise_safe_exceptions
795 def hg_rebuild_fn_cache(self, wire,):
797 def hg_rebuild_fn_cache(self, wire,):
796 repo = self._factory.repo(wire)
798 repo = self._factory.repo(wire)
797 baseui = self._factory._create_config(wire['config'])
799 baseui = self._factory._create_config(wire['config'])
798 baseui, output = patch_ui_message_output(baseui)
800 baseui, output = patch_ui_message_output(baseui)
799
801
800 repo.ui = baseui
802 repo.ui = baseui
801
803
802 repair.rebuildfncache(baseui, repo)
804 repair.rebuildfncache(baseui, repo)
803
805
804 return output.getvalue()
806 return output.getvalue()
805
807
806 @reraise_safe_exceptions
808 @reraise_safe_exceptions
807 def tags(self, wire):
809 def tags(self, wire):
808 cache_on, context_uid, repo_id = self._cache_on(wire)
810 cache_on, context_uid, repo_id = self._cache_on(wire)
809 region = self._region(wire)
811 region = self._region(wire)
810
812
811 @region.conditional_cache_on_arguments(condition=cache_on)
813 @region.conditional_cache_on_arguments(condition=cache_on)
812 def _tags(_context_uid, _repo_id):
814 def _tags(_context_uid, _repo_id):
813 repo = self._factory.repo(wire)
815 repo = self._factory.repo(wire)
814 return {safe_str(name): ascii_str(hex(sha)) for name, sha in repo.tags().items()}
816 return {safe_str(name): ascii_str(hex(sha)) for name, sha in repo.tags().items()}
815
817
816 return _tags(context_uid, repo_id)
818 return _tags(context_uid, repo_id)
817
819
818 @reraise_safe_exceptions
820 @reraise_safe_exceptions
819 def update(self, wire, node='', clean=False):
821 def update(self, wire, node='', clean=False):
820 repo = self._factory.repo(wire)
822 repo = self._factory.repo(wire)
821 baseui = self._factory._create_config(wire['config'])
823 baseui = self._factory._create_config(wire['config'])
822 node = safe_bytes(node)
824 node = safe_bytes(node)
823
825
824 commands.update(baseui, repo, node=node, clean=clean)
826 commands.update(baseui, repo, node=node, clean=clean)
825
827
826 @reraise_safe_exceptions
828 @reraise_safe_exceptions
827 def identify(self, wire):
829 def identify(self, wire):
828 repo = self._factory.repo(wire)
830 repo = self._factory.repo(wire)
829 baseui = self._factory._create_config(wire['config'])
831 baseui = self._factory._create_config(wire['config'])
830 output = io.BytesIO()
832 output = io.BytesIO()
831 baseui.write = output.write
833 baseui.write = output.write
832 # This is required to get a full node id
834 # This is required to get a full node id
833 baseui.debugflag = True
835 baseui.debugflag = True
834 commands.identify(baseui, repo, id=True)
836 commands.identify(baseui, repo, id=True)
835
837
836 return output.getvalue()
838 return output.getvalue()
837
839
838 @reraise_safe_exceptions
840 @reraise_safe_exceptions
839 def heads(self, wire, branch=None):
841 def heads(self, wire, branch=None):
840 repo = self._factory.repo(wire)
842 repo = self._factory.repo(wire)
841 baseui = self._factory._create_config(wire['config'])
843 baseui = self._factory._create_config(wire['config'])
842 output = io.BytesIO()
844 output = io.BytesIO()
843
845
844 def write(data, **unused_kwargs):
846 def write(data, **unused_kwargs):
845 output.write(data)
847 output.write(data)
846
848
847 baseui.write = write
849 baseui.write = write
848 if branch:
850 if branch:
849 args = [safe_bytes(branch)]
851 args = [safe_bytes(branch)]
850 else:
852 else:
851 args = []
853 args = []
852 commands.heads(baseui, repo, template=b'{node} ', *args)
854 commands.heads(baseui, repo, template=b'{node} ', *args)
853
855
854 return output.getvalue()
856 return output.getvalue()
855
857
856 @reraise_safe_exceptions
858 @reraise_safe_exceptions
857 def ancestor(self, wire, revision1, revision2):
859 def ancestor(self, wire, revision1, revision2):
858 repo = self._factory.repo(wire)
860 repo = self._factory.repo(wire)
859 changelog = repo.changelog
861 changelog = repo.changelog
860 lookup = repo.lookup
862 lookup = repo.lookup
861 a = changelog.ancestor(lookup(safe_bytes(revision1)), lookup(safe_bytes(revision2)))
863 a = changelog.ancestor(lookup(safe_bytes(revision1)), lookup(safe_bytes(revision2)))
862 return hex(a)
864 return hex(a)
863
865
864 @reraise_safe_exceptions
866 @reraise_safe_exceptions
865 def clone(self, wire, source, dest, update_after_clone=False, hooks=True):
867 def clone(self, wire, source, dest, update_after_clone=False, hooks=True):
866 baseui = self._factory._create_config(wire["config"], hooks=hooks)
868 baseui = self._factory._create_config(wire["config"], hooks=hooks)
867 clone(baseui, safe_bytes(source), safe_bytes(dest), noupdate=not update_after_clone)
869 clone(baseui, safe_bytes(source), safe_bytes(dest), noupdate=not update_after_clone)
868
870
869 @reraise_safe_exceptions
871 @reraise_safe_exceptions
870 def commitctx(self, wire, message, parents, commit_time, commit_timezone, user, files, extra, removed, updated):
872 def commitctx(self, wire, message, parents, commit_time, commit_timezone, user, files, extra, removed, updated):
871
873
872 repo = self._factory.repo(wire)
874 repo = self._factory.repo(wire)
873 baseui = self._factory._create_config(wire['config'])
875 baseui = self._factory._create_config(wire['config'])
874 publishing = baseui.configbool(b'phases', b'publish')
876 publishing = baseui.configbool(b'phases', b'publish')
875
877
876 def _filectxfn(_repo, ctx, path: bytes):
878 def _filectxfn(_repo, ctx, path: bytes):
877 """
879 """
878 Marks given path as added/changed/removed in a given _repo. This is
880 Marks given path as added/changed/removed in a given _repo. This is
879 for internal mercurial commit function.
881 for internal mercurial commit function.
880 """
882 """
881
883
882 # check if this path is removed
884 # check if this path is removed
883 if safe_str(path) in removed:
885 if safe_str(path) in removed:
884 # returning None is a way to mark node for removal
886 # returning None is a way to mark node for removal
885 return None
887 return None
886
888
887 # check if this path is added
889 # check if this path is added
888 for node in updated:
890 for node in updated:
889 if safe_bytes(node['path']) == path:
891 if safe_bytes(node['path']) == path:
890 return memfilectx(
892 return memfilectx(
891 _repo,
893 _repo,
892 changectx=ctx,
894 changectx=ctx,
893 path=safe_bytes(node['path']),
895 path=safe_bytes(node['path']),
894 data=safe_bytes(node['content']),
896 data=safe_bytes(node['content']),
895 islink=False,
897 islink=False,
896 isexec=bool(node['mode'] & stat.S_IXUSR),
898 isexec=bool(node['mode'] & stat.S_IXUSR),
897 copysource=False)
899 copysource=False)
898 abort_exc = exceptions.AbortException()
900 abort_exc = exceptions.AbortException()
899 raise abort_exc(f"Given path haven't been marked as added, changed or removed ({path})")
901 raise abort_exc(f"Given path haven't been marked as added, changed or removed ({path})")
900
902
901 if publishing:
903 if publishing:
902 new_commit_phase = b'public'
904 new_commit_phase = b'public'
903 else:
905 else:
904 new_commit_phase = b'draft'
906 new_commit_phase = b'draft'
905 with repo.ui.configoverride({(b'phases', b'new-commit'): new_commit_phase}):
907 with repo.ui.configoverride({(b'phases', b'new-commit'): new_commit_phase}):
906 kwargs = {safe_bytes(k): safe_bytes(v) for k, v in extra.items()}
908 kwargs = {safe_bytes(k): safe_bytes(v) for k, v in extra.items()}
907 commit_ctx = memctx(
909 commit_ctx = memctx(
908 repo=repo,
910 repo=repo,
909 parents=parents,
911 parents=parents,
910 text=safe_bytes(message),
912 text=safe_bytes(message),
911 files=[safe_bytes(x) for x in files],
913 files=[safe_bytes(x) for x in files],
912 filectxfn=_filectxfn,
914 filectxfn=_filectxfn,
913 user=safe_bytes(user),
915 user=safe_bytes(user),
914 date=(commit_time, commit_timezone),
916 date=(commit_time, commit_timezone),
915 extra=kwargs)
917 extra=kwargs)
916
918
917 n = repo.commitctx(commit_ctx)
919 n = repo.commitctx(commit_ctx)
918 new_id = hex(n)
920 new_id = hex(n)
919
921
920 return new_id
922 return new_id
921
923
922 @reraise_safe_exceptions
924 @reraise_safe_exceptions
923 def pull(self, wire, url, commit_ids=None):
925 def pull(self, wire, url, commit_ids=None):
924 repo = self._factory.repo(wire)
926 repo = self._factory.repo(wire)
925 # Disable any prompts for this repo
927 # Disable any prompts for this repo
926 repo.ui.setconfig(b'ui', b'interactive', b'off', b'-y')
928 repo.ui.setconfig(b'ui', b'interactive', b'off', b'-y')
927
929
928 remote = peer(repo, {}, safe_bytes(url))
930 remote = peer(repo, {}, safe_bytes(url))
929 # Disable any prompts for this remote
931 # Disable any prompts for this remote
930 remote.ui.setconfig(b'ui', b'interactive', b'off', b'-y')
932 remote.ui.setconfig(b'ui', b'interactive', b'off', b'-y')
931
933
932 if commit_ids:
934 if commit_ids:
933 commit_ids = [bin(commit_id) for commit_id in commit_ids]
935 commit_ids = [bin(commit_id) for commit_id in commit_ids]
934
936
935 return exchange.pull(
937 return exchange.pull(
936 repo, remote, heads=commit_ids, force=None).cgresult
938 repo, remote, heads=commit_ids, force=None).cgresult
937
939
938 @reraise_safe_exceptions
940 @reraise_safe_exceptions
939 def pull_cmd(self, wire, source, bookmark='', branch='', revision='', hooks=True):
941 def pull_cmd(self, wire, source, bookmark='', branch='', revision='', hooks=True):
940 repo = self._factory.repo(wire)
942 repo = self._factory.repo(wire)
941 baseui = self._factory._create_config(wire['config'], hooks=hooks)
943 baseui = self._factory._create_config(wire['config'], hooks=hooks)
942
944
943 source = safe_bytes(source)
945 source = safe_bytes(source)
944
946
945 # Mercurial internally has a lot of logic that checks ONLY if
947 # Mercurial internally has a lot of logic that checks ONLY if
946 # option is defined, we just pass those if they are defined then
948 # option is defined, we just pass those if they are defined then
947 opts = {}
949 opts = {}
948 if bookmark:
950 if bookmark:
949 if isinstance(branch, list):
951 if isinstance(branch, list):
950 bookmark = [safe_bytes(x) for x in bookmark]
952 bookmark = [safe_bytes(x) for x in bookmark]
951 else:
953 else:
952 bookmark = safe_bytes(bookmark)
954 bookmark = safe_bytes(bookmark)
953 opts['bookmark'] = bookmark
955 opts['bookmark'] = bookmark
954 if branch:
956 if branch:
955 if isinstance(branch, list):
957 if isinstance(branch, list):
956 branch = [safe_bytes(x) for x in branch]
958 branch = [safe_bytes(x) for x in branch]
957 else:
959 else:
958 branch = safe_bytes(branch)
960 branch = safe_bytes(branch)
959 opts['branch'] = branch
961 opts['branch'] = branch
960 if revision:
962 if revision:
961 opts['rev'] = safe_bytes(revision)
963 opts['rev'] = safe_bytes(revision)
962
964
963 commands.pull(baseui, repo, source, **opts)
965 commands.pull(baseui, repo, source, **opts)
964
966
965 @reraise_safe_exceptions
967 @reraise_safe_exceptions
966 def push(self, wire, revisions, dest_path, hooks=True, push_branches=False):
968 def push(self, wire, revisions, dest_path, hooks=True, push_branches=False):
967 repo = self._factory.repo(wire)
969 repo = self._factory.repo(wire)
968 baseui = self._factory._create_config(wire['config'], hooks=hooks)
970 baseui = self._factory._create_config(wire['config'], hooks=hooks)
969 commands.push(baseui, repo, dest=dest_path, rev=revisions,
971 commands.push(baseui, repo, dest=dest_path, rev=revisions,
970 new_branch=push_branches)
972 new_branch=push_branches)
971
973
972 @reraise_safe_exceptions
974 @reraise_safe_exceptions
973 def strip(self, wire, revision, update, backup):
975 def strip(self, wire, revision, update, backup):
974 repo = self._factory.repo(wire)
976 repo = self._factory.repo(wire)
975 ctx = self._get_ctx(repo, revision)
977 ctx = self._get_ctx(repo, revision)
976 hgext_strip(
978 hgext_strip(
977 repo.baseui, repo, ctx.node(), update=update, backup=backup)
979 repo.baseui, repo, ctx.node(), update=update, backup=backup)
978
980
979 @reraise_safe_exceptions
981 @reraise_safe_exceptions
980 def get_unresolved_files(self, wire):
982 def get_unresolved_files(self, wire):
981 repo = self._factory.repo(wire)
983 repo = self._factory.repo(wire)
982
984
983 log.debug('Calculating unresolved files for repo: %s', repo)
985 log.debug('Calculating unresolved files for repo: %s', repo)
984 output = io.BytesIO()
986 output = io.BytesIO()
985
987
986 def write(data, **unused_kwargs):
988 def write(data, **unused_kwargs):
987 output.write(data)
989 output.write(data)
988
990
989 baseui = self._factory._create_config(wire['config'])
991 baseui = self._factory._create_config(wire['config'])
990 baseui.write = write
992 baseui.write = write
991
993
992 commands.resolve(baseui, repo, list=True)
994 commands.resolve(baseui, repo, list=True)
993 unresolved = output.getvalue().splitlines(0)
995 unresolved = output.getvalue().splitlines(0)
994 return unresolved
996 return unresolved
995
997
996 @reraise_safe_exceptions
998 @reraise_safe_exceptions
997 def merge(self, wire, revision):
999 def merge(self, wire, revision):
998 repo = self._factory.repo(wire)
1000 repo = self._factory.repo(wire)
999 baseui = self._factory._create_config(wire['config'])
1001 baseui = self._factory._create_config(wire['config'])
1000 repo.ui.setconfig(b'ui', b'merge', b'internal:dump')
1002 repo.ui.setconfig(b'ui', b'merge', b'internal:dump')
1001
1003
1002 # In case of sub repositories are used mercurial prompts the user in
1004 # In case of sub repositories are used mercurial prompts the user in
1003 # case of merge conflicts or different sub repository sources. By
1005 # case of merge conflicts or different sub repository sources. By
1004 # setting the interactive flag to `False` mercurial doesn't prompt the
1006 # setting the interactive flag to `False` mercurial doesn't prompt the
1005 # used but instead uses a default value.
1007 # used but instead uses a default value.
1006 repo.ui.setconfig(b'ui', b'interactive', False)
1008 repo.ui.setconfig(b'ui', b'interactive', False)
1007 commands.merge(baseui, repo, rev=revision)
1009 commands.merge(baseui, repo, rev=revision)
1008
1010
1009 @reraise_safe_exceptions
1011 @reraise_safe_exceptions
1010 def merge_state(self, wire):
1012 def merge_state(self, wire):
1011 repo = self._factory.repo(wire)
1013 repo = self._factory.repo(wire)
1012 repo.ui.setconfig(b'ui', b'merge', b'internal:dump')
1014 repo.ui.setconfig(b'ui', b'merge', b'internal:dump')
1013
1015
1014 # In case of sub repositories are used mercurial prompts the user in
1016 # In case of sub repositories are used mercurial prompts the user in
1015 # case of merge conflicts or different sub repository sources. By
1017 # case of merge conflicts or different sub repository sources. By
1016 # setting the interactive flag to `False` mercurial doesn't prompt the
1018 # setting the interactive flag to `False` mercurial doesn't prompt the
1017 # used but instead uses a default value.
1019 # used but instead uses a default value.
1018 repo.ui.setconfig(b'ui', b'interactive', False)
1020 repo.ui.setconfig(b'ui', b'interactive', False)
1019 ms = hg_merge.mergestate(repo)
1021 ms = hg_merge.mergestate(repo)
1020 return [x for x in ms.unresolved()]
1022 return [x for x in ms.unresolved()]
1021
1023
1022 @reraise_safe_exceptions
1024 @reraise_safe_exceptions
1023 def commit(self, wire, message, username, close_branch=False):
1025 def commit(self, wire, message, username, close_branch=False):
1024 repo = self._factory.repo(wire)
1026 repo = self._factory.repo(wire)
1025 baseui = self._factory._create_config(wire['config'])
1027 baseui = self._factory._create_config(wire['config'])
1026 repo.ui.setconfig(b'ui', b'username', username)
1028 repo.ui.setconfig(b'ui', b'username', username)
1027 commands.commit(baseui, repo, message=message, close_branch=close_branch)
1029 commands.commit(baseui, repo, message=message, close_branch=close_branch)
1028
1030
1029 @reraise_safe_exceptions
1031 @reraise_safe_exceptions
1030 def rebase(self, wire, source=None, dest=None, abort=False):
1032 def rebase(self, wire, source=None, dest=None, abort=False):
1031 repo = self._factory.repo(wire)
1033 repo = self._factory.repo(wire)
1032 baseui = self._factory._create_config(wire['config'])
1034 baseui = self._factory._create_config(wire['config'])
1033 repo.ui.setconfig(b'ui', b'merge', b'internal:dump')
1035 repo.ui.setconfig(b'ui', b'merge', b'internal:dump')
1034 # In case of sub repositories are used mercurial prompts the user in
1036 # In case of sub repositories are used mercurial prompts the user in
1035 # case of merge conflicts or different sub repository sources. By
1037 # case of merge conflicts or different sub repository sources. By
1036 # setting the interactive flag to `False` mercurial doesn't prompt the
1038 # setting the interactive flag to `False` mercurial doesn't prompt the
1037 # used but instead uses a default value.
1039 # used but instead uses a default value.
1038 repo.ui.setconfig(b'ui', b'interactive', False)
1040 repo.ui.setconfig(b'ui', b'interactive', False)
1039 rebase.rebase(baseui, repo, base=source, dest=dest, abort=abort, keep=not abort)
1041 rebase.rebase(baseui, repo, base=source, dest=dest, abort=abort, keep=not abort)
1040
1042
1041 @reraise_safe_exceptions
1043 @reraise_safe_exceptions
1042 def tag(self, wire, name, revision, message, local, user, tag_time, tag_timezone):
1044 def tag(self, wire, name, revision, message, local, user, tag_time, tag_timezone):
1043 repo = self._factory.repo(wire)
1045 repo = self._factory.repo(wire)
1044 ctx = self._get_ctx(repo, revision)
1046 ctx = self._get_ctx(repo, revision)
1045 node = ctx.node()
1047 node = ctx.node()
1046
1048
1047 date = (tag_time, tag_timezone)
1049 date = (tag_time, tag_timezone)
1048 try:
1050 try:
1049 hg_tag.tag(repo, name, node, message, local, user, date)
1051 hg_tag.tag(repo, name, node, message, local, user, date)
1050 except Abort as e:
1052 except Abort as e:
1051 log.exception("Tag operation aborted")
1053 log.exception("Tag operation aborted")
1052 # Exception can contain unicode which we convert
1054 # Exception can contain unicode which we convert
1053 raise exceptions.AbortException(e)(repr(e))
1055 raise exceptions.AbortException(e)(repr(e))
1054
1056
1055 @reraise_safe_exceptions
1057 @reraise_safe_exceptions
1056 def bookmark(self, wire, bookmark, revision=''):
1058 def bookmark(self, wire, bookmark, revision=''):
1057 repo = self._factory.repo(wire)
1059 repo = self._factory.repo(wire)
1058 baseui = self._factory._create_config(wire['config'])
1060 baseui = self._factory._create_config(wire['config'])
1059 commands.bookmark(baseui, repo, safe_bytes(bookmark), rev=safe_bytes(revision), force=True)
1061 commands.bookmark(baseui, repo, safe_bytes(bookmark), rev=safe_bytes(revision), force=True)
1060
1062
1061 @reraise_safe_exceptions
1063 @reraise_safe_exceptions
1062 def install_hooks(self, wire, force=False):
1064 def install_hooks(self, wire, force=False):
1063 # we don't need any special hooks for Mercurial
1065 # we don't need any special hooks for Mercurial
1064 pass
1066 pass
1065
1067
1066 @reraise_safe_exceptions
1068 @reraise_safe_exceptions
1067 def get_hooks_info(self, wire):
1069 def get_hooks_info(self, wire):
1068 return {
1070 return {
1069 'pre_version': vcsserver.__version__,
1071 'pre_version': vcsserver.__version__,
1070 'post_version': vcsserver.__version__,
1072 'post_version': vcsserver.__version__,
1071 }
1073 }
1072
1074
1073 @reraise_safe_exceptions
1075 @reraise_safe_exceptions
1074 def set_head_ref(self, wire, head_name):
1076 def set_head_ref(self, wire, head_name):
1075 pass
1077 pass
1076
1078
1077 @reraise_safe_exceptions
1079 @reraise_safe_exceptions
1078 def archive_repo(self, wire, archive_dest_path, kind, mtime, archive_at_path,
1080 def archive_repo(self, wire, archive_dest_path, kind, mtime, archive_at_path,
1079 archive_dir_name, commit_id):
1081 archive_dir_name, commit_id):
1080
1082
1081 def file_walker(_commit_id, path):
1083 def file_walker(_commit_id, path):
1082 repo = self._factory.repo(wire)
1084 repo = self._factory.repo(wire)
1083 ctx = repo[_commit_id]
1085 ctx = repo[_commit_id]
1084 is_root = path in ['', '/']
1086 is_root = path in ['', '/']
1085 if is_root:
1087 if is_root:
1086 matcher = alwaysmatcher(badfn=None)
1088 matcher = alwaysmatcher(badfn=None)
1087 else:
1089 else:
1088 matcher = patternmatcher('', [(b'glob', path+'/**', b'')], badfn=None)
1090 matcher = patternmatcher('', [(b'glob', path+'/**', b'')], badfn=None)
1089 file_iter = ctx.manifest().walk(matcher)
1091 file_iter = ctx.manifest().walk(matcher)
1090
1092
1091 for fn in file_iter:
1093 for fn in file_iter:
1092 file_path = fn
1094 file_path = fn
1093 flags = ctx.flags(fn)
1095 flags = ctx.flags(fn)
1094 mode = b'x' in flags and 0o755 or 0o644
1096 mode = b'x' in flags and 0o755 or 0o644
1095 is_link = b'l' in flags
1097 is_link = b'l' in flags
1096
1098
1097 yield ArchiveNode(file_path, mode, is_link, ctx[fn].data)
1099 yield ArchiveNode(file_path, mode, is_link, ctx[fn].data)
1098
1100
1099 return archive_repo(file_walker, archive_dest_path, kind, mtime, archive_at_path,
1101 return archive_repo(file_walker, archive_dest_path, kind, mtime, archive_at_path,
1100 archive_dir_name, commit_id)
1102 archive_dir_name, commit_id)
1101
1103
@@ -1,879 +1,888 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-2020 RhodeCode GmbH
2 # Copyright (C) 2014-2020 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
18
19 import os
19 import os
20 import subprocess
20 import subprocess
21 from urllib.error import URLError
21 from urllib.error import URLError
22 import urllib.parse
22 import urllib.parse
23 import logging
23 import logging
24 import posixpath as vcspath
24 import posixpath as vcspath
25 import io
25 import io
26 import urllib.request
26 import urllib.request
27 import urllib.parse
27 import urllib.parse
28 import urllib.error
28 import urllib.error
29 import traceback
29 import traceback
30
30
31 import svn.client
31 import svn.client
32 import svn.core
32 import svn.core
33 import svn.delta
33 import svn.delta
34 import svn.diff
34 import svn.diff
35 import svn.fs
35 import svn.fs
36 import svn.repos
36 import svn.repos
37
37
38 from vcsserver import svn_diff, exceptions, subprocessio, settings
38 from vcsserver import svn_diff, exceptions, subprocessio, settings
39 from vcsserver.base import RepoFactory, raise_from_original, ArchiveNode, archive_repo
39 from vcsserver.base import RepoFactory, raise_from_original, ArchiveNode, archive_repo
40 from vcsserver.exceptions import NoContentException
40 from vcsserver.exceptions import NoContentException
41 from vcsserver.str_utils import safe_str, safe_bytes
41 from vcsserver.str_utils import safe_str, safe_bytes
42 from vcsserver.vcs_base import RemoteBase
42 from vcsserver.vcs_base import RemoteBase
43 from vcsserver.lib.svnremoterepo import svnremoterepo
43 from vcsserver.lib.svnremoterepo import svnremoterepo
44 log = logging.getLogger(__name__)
44 log = logging.getLogger(__name__)
45
45
46
46
47 svn_compatible_versions_map = {
47 svn_compatible_versions_map = {
48 'pre-1.4-compatible': '1.3',
48 'pre-1.4-compatible': '1.3',
49 'pre-1.5-compatible': '1.4',
49 'pre-1.5-compatible': '1.4',
50 'pre-1.6-compatible': '1.5',
50 'pre-1.6-compatible': '1.5',
51 'pre-1.8-compatible': '1.7',
51 'pre-1.8-compatible': '1.7',
52 'pre-1.9-compatible': '1.8',
52 'pre-1.9-compatible': '1.8',
53 }
53 }
54
54
55 current_compatible_version = '1.14'
55 current_compatible_version = '1.14'
56
56
57
57
58 def reraise_safe_exceptions(func):
58 def reraise_safe_exceptions(func):
59 """Decorator for converting svn exceptions to something neutral."""
59 """Decorator for converting svn exceptions to something neutral."""
60 def wrapper(*args, **kwargs):
60 def wrapper(*args, **kwargs):
61 try:
61 try:
62 return func(*args, **kwargs)
62 return func(*args, **kwargs)
63 except Exception as e:
63 except Exception as e:
64 if not hasattr(e, '_vcs_kind'):
64 if not hasattr(e, '_vcs_kind'):
65 log.exception("Unhandled exception in svn remote call")
65 log.exception("Unhandled exception in svn remote call")
66 raise_from_original(exceptions.UnhandledException(e), e)
66 raise_from_original(exceptions.UnhandledException(e), e)
67 raise
67 raise
68 return wrapper
68 return wrapper
69
69
70
70
71 class SubversionFactory(RepoFactory):
71 class SubversionFactory(RepoFactory):
72 repo_type = 'svn'
72 repo_type = 'svn'
73
73
74 def _create_repo(self, wire, create, compatible_version):
74 def _create_repo(self, wire, create, compatible_version):
75 path = svn.core.svn_path_canonicalize(wire['path'])
75 path = svn.core.svn_path_canonicalize(wire['path'])
76 if create:
76 if create:
77 fs_config = {'compatible-version': current_compatible_version}
77 fs_config = {'compatible-version': current_compatible_version}
78 if compatible_version:
78 if compatible_version:
79
79
80 compatible_version_string = \
80 compatible_version_string = \
81 svn_compatible_versions_map.get(compatible_version) \
81 svn_compatible_versions_map.get(compatible_version) \
82 or compatible_version
82 or compatible_version
83 fs_config['compatible-version'] = compatible_version_string
83 fs_config['compatible-version'] = compatible_version_string
84
84
85 log.debug('Create SVN repo with config "%s"', fs_config)
85 log.debug('Create SVN repo with config "%s"', fs_config)
86 repo = svn.repos.create(path, "", "", None, fs_config)
86 repo = svn.repos.create(path, "", "", None, fs_config)
87 else:
87 else:
88 repo = svn.repos.open(path)
88 repo = svn.repos.open(path)
89
89
90 log.debug('Got SVN object: %s', repo)
90 log.debug('Got SVN object: %s', repo)
91 return repo
91 return repo
92
92
93 def repo(self, wire, create=False, compatible_version=None):
93 def repo(self, wire, create=False, compatible_version=None):
94 """
94 """
95 Get a repository instance for the given path.
95 Get a repository instance for the given path.
96 """
96 """
97 return self._create_repo(wire, create, compatible_version)
97 return self._create_repo(wire, create, compatible_version)
98
98
99
99
100 NODE_TYPE_MAPPING = {
100 NODE_TYPE_MAPPING = {
101 svn.core.svn_node_file: 'file',
101 svn.core.svn_node_file: 'file',
102 svn.core.svn_node_dir: 'dir',
102 svn.core.svn_node_dir: 'dir',
103 }
103 }
104
104
105
105
106 class SvnRemote(RemoteBase):
106 class SvnRemote(RemoteBase):
107
107
108 def __init__(self, factory, hg_factory=None):
108 def __init__(self, factory, hg_factory=None):
109 self._factory = factory
109 self._factory = factory
110
110
111 @reraise_safe_exceptions
111 @reraise_safe_exceptions
112 def discover_svn_version(self):
112 def discover_svn_version(self):
113 try:
113 try:
114 import svn.core
114 import svn.core
115 svn_ver = svn.core.SVN_VERSION
115 svn_ver = svn.core.SVN_VERSION
116 except ImportError:
116 except ImportError:
117 svn_ver = None
117 svn_ver = None
118 return safe_str(svn_ver)
118 return safe_str(svn_ver)
119
119
120 @reraise_safe_exceptions
120 @reraise_safe_exceptions
121 def is_empty(self, wire):
121 def is_empty(self, wire):
122
122
123 try:
123 try:
124 return self.lookup(wire, -1) == 0
124 return self.lookup(wire, -1) == 0
125 except Exception:
125 except Exception:
126 log.exception("failed to read object_store")
126 log.exception("failed to read object_store")
127 return False
127 return False
128
128
129 def check_url(self, url):
129 def check_url(self, url):
130
130
131 # uuid function get's only valid UUID from proper repo, else
131 # uuid function get's only valid UUID from proper repo, else
132 # throws exception
132 # throws exception
133 username, password, src_url = self.get_url_and_credentials(url)
133 username, password, src_url = self.get_url_and_credentials(url)
134 try:
134 try:
135 svnremoterepo(username, password, src_url).svn().uuid
135 svnremoterepo(username, password, src_url).svn().uuid
136 except Exception:
136 except Exception:
137 tb = traceback.format_exc()
137 tb = traceback.format_exc()
138 log.debug("Invalid Subversion url: `%s`, tb: %s", url, tb)
138 log.debug("Invalid Subversion url: `%s`, tb: %s", url, tb)
139 raise URLError(
139 raise URLError(
140 '"%s" is not a valid Subversion source url.' % (url, ))
140 '"%s" is not a valid Subversion source url.' % (url, ))
141 return True
141 return True
142
142
143 def is_path_valid_repository(self, wire, path):
143 def is_path_valid_repository(self, wire, path):
144
144
145 # NOTE(marcink): short circuit the check for SVN repo
145 # NOTE(marcink): short circuit the check for SVN repo
146 # the repos.open might be expensive to check, but we have one cheap
146 # the repos.open might be expensive to check, but we have one cheap
147 # pre condition that we can use, to check for 'format' file
147 # pre condition that we can use, to check for 'format' file
148
148
149 if not os.path.isfile(os.path.join(path, 'format')):
149 if not os.path.isfile(os.path.join(path, 'format')):
150 return False
150 return False
151
151
152 try:
152 try:
153 svn.repos.open(path)
153 svn.repos.open(path)
154 except svn.core.SubversionException:
154 except svn.core.SubversionException:
155 tb = traceback.format_exc()
155 tb = traceback.format_exc()
156 log.debug("Invalid Subversion path `%s`, tb: %s", path, tb)
156 log.debug("Invalid Subversion path `%s`, tb: %s", path, tb)
157 return False
157 return False
158 return True
158 return True
159
159
160 @reraise_safe_exceptions
160 @reraise_safe_exceptions
161 def verify(self, wire,):
161 def verify(self, wire,):
162 repo_path = wire['path']
162 repo_path = wire['path']
163 if not self.is_path_valid_repository(wire, repo_path):
163 if not self.is_path_valid_repository(wire, repo_path):
164 raise Exception(
164 raise Exception(
165 "Path %s is not a valid Subversion repository." % repo_path)
165 "Path %s is not a valid Subversion repository." % repo_path)
166
166
167 cmd = ['svnadmin', 'info', repo_path]
167 cmd = ['svnadmin', 'info', repo_path]
168 stdout, stderr = subprocessio.run_command(cmd)
168 stdout, stderr = subprocessio.run_command(cmd)
169 return stdout
169 return stdout
170
170
171 def lookup(self, wire, revision):
171 def lookup(self, wire, revision):
172 if revision not in [-1, None, 'HEAD']:
172 if revision not in [-1, None, 'HEAD']:
173 raise NotImplementedError
173 raise NotImplementedError
174 repo = self._factory.repo(wire)
174 repo = self._factory.repo(wire)
175 fs_ptr = svn.repos.fs(repo)
175 fs_ptr = svn.repos.fs(repo)
176 head = svn.fs.youngest_rev(fs_ptr)
176 head = svn.fs.youngest_rev(fs_ptr)
177 return head
177 return head
178
178
179 def lookup_interval(self, wire, start_ts, end_ts):
179 def lookup_interval(self, wire, start_ts, end_ts):
180 repo = self._factory.repo(wire)
180 repo = self._factory.repo(wire)
181 fsobj = svn.repos.fs(repo)
181 fsobj = svn.repos.fs(repo)
182 start_rev = None
182 start_rev = None
183 end_rev = None
183 end_rev = None
184 if start_ts:
184 if start_ts:
185 start_ts_svn = apr_time_t(start_ts)
185 start_ts_svn = apr_time_t(start_ts)
186 start_rev = svn.repos.dated_revision(repo, start_ts_svn) + 1
186 start_rev = svn.repos.dated_revision(repo, start_ts_svn) + 1
187 else:
187 else:
188 start_rev = 1
188 start_rev = 1
189 if end_ts:
189 if end_ts:
190 end_ts_svn = apr_time_t(end_ts)
190 end_ts_svn = apr_time_t(end_ts)
191 end_rev = svn.repos.dated_revision(repo, end_ts_svn)
191 end_rev = svn.repos.dated_revision(repo, end_ts_svn)
192 else:
192 else:
193 end_rev = svn.fs.youngest_rev(fsobj)
193 end_rev = svn.fs.youngest_rev(fsobj)
194 return start_rev, end_rev
194 return start_rev, end_rev
195
195
196 def revision_properties(self, wire, revision):
196 def revision_properties(self, wire, revision):
197
197
198 cache_on, context_uid, repo_id = self._cache_on(wire)
198 cache_on, context_uid, repo_id = self._cache_on(wire)
199 region = self._region(wire)
199 region = self._region(wire)
200 @region.conditional_cache_on_arguments(condition=cache_on)
200 @region.conditional_cache_on_arguments(condition=cache_on)
201 def _revision_properties(_repo_id, _revision):
201 def _revision_properties(_repo_id, _revision):
202 repo = self._factory.repo(wire)
202 repo = self._factory.repo(wire)
203 fs_ptr = svn.repos.fs(repo)
203 fs_ptr = svn.repos.fs(repo)
204 return svn.fs.revision_proplist(fs_ptr, revision)
204 return svn.fs.revision_proplist(fs_ptr, revision)
205 return _revision_properties(repo_id, revision)
205 return _revision_properties(repo_id, revision)
206
206
207 def revision_changes(self, wire, revision):
207 def revision_changes(self, wire, revision):
208
208
209 repo = self._factory.repo(wire)
209 repo = self._factory.repo(wire)
210 fsobj = svn.repos.fs(repo)
210 fsobj = svn.repos.fs(repo)
211 rev_root = svn.fs.revision_root(fsobj, revision)
211 rev_root = svn.fs.revision_root(fsobj, revision)
212
212
213 editor = svn.repos.ChangeCollector(fsobj, rev_root)
213 editor = svn.repos.ChangeCollector(fsobj, rev_root)
214 editor_ptr, editor_baton = svn.delta.make_editor(editor)
214 editor_ptr, editor_baton = svn.delta.make_editor(editor)
215 base_dir = ""
215 base_dir = ""
216 send_deltas = False
216 send_deltas = False
217 svn.repos.replay2(
217 svn.repos.replay2(
218 rev_root, base_dir, svn.core.SVN_INVALID_REVNUM, send_deltas,
218 rev_root, base_dir, svn.core.SVN_INVALID_REVNUM, send_deltas,
219 editor_ptr, editor_baton, None)
219 editor_ptr, editor_baton, None)
220
220
221 added = []
221 added = []
222 changed = []
222 changed = []
223 removed = []
223 removed = []
224
224
225 # TODO: CHANGE_ACTION_REPLACE: Figure out where it belongs
225 # TODO: CHANGE_ACTION_REPLACE: Figure out where it belongs
226 for path, change in editor.changes.items():
226 for path, change in editor.changes.items():
227 # TODO: Decide what to do with directory nodes. Subversion can add
227 # TODO: Decide what to do with directory nodes. Subversion can add
228 # empty directories.
228 # empty directories.
229
229
230 if change.item_kind == svn.core.svn_node_dir:
230 if change.item_kind == svn.core.svn_node_dir:
231 continue
231 continue
232 if change.action in [svn.repos.CHANGE_ACTION_ADD]:
232 if change.action in [svn.repos.CHANGE_ACTION_ADD]:
233 added.append(path)
233 added.append(path)
234 elif change.action in [svn.repos.CHANGE_ACTION_MODIFY,
234 elif change.action in [svn.repos.CHANGE_ACTION_MODIFY,
235 svn.repos.CHANGE_ACTION_REPLACE]:
235 svn.repos.CHANGE_ACTION_REPLACE]:
236 changed.append(path)
236 changed.append(path)
237 elif change.action in [svn.repos.CHANGE_ACTION_DELETE]:
237 elif change.action in [svn.repos.CHANGE_ACTION_DELETE]:
238 removed.append(path)
238 removed.append(path)
239 else:
239 else:
240 raise NotImplementedError(
240 raise NotImplementedError(
241 "Action %s not supported on path %s" % (
241 "Action %s not supported on path %s" % (
242 change.action, path))
242 change.action, path))
243
243
244 changes = {
244 changes = {
245 'added': added,
245 'added': added,
246 'changed': changed,
246 'changed': changed,
247 'removed': removed,
247 'removed': removed,
248 }
248 }
249 return changes
249 return changes
250
250
251 @reraise_safe_exceptions
251 @reraise_safe_exceptions
252 def node_history(self, wire, path, revision, limit):
252 def node_history(self, wire, path, revision, limit):
253 cache_on, context_uid, repo_id = self._cache_on(wire)
253 cache_on, context_uid, repo_id = self._cache_on(wire)
254 region = self._region(wire)
254 region = self._region(wire)
255 @region.conditional_cache_on_arguments(condition=cache_on)
255 @region.conditional_cache_on_arguments(condition=cache_on)
256 def _assert_correct_path(_context_uid, _repo_id, _path, _revision, _limit):
256 def _assert_correct_path(_context_uid, _repo_id, _path, _revision, _limit):
257 cross_copies = False
257 cross_copies = False
258 repo = self._factory.repo(wire)
258 repo = self._factory.repo(wire)
259 fsobj = svn.repos.fs(repo)
259 fsobj = svn.repos.fs(repo)
260 rev_root = svn.fs.revision_root(fsobj, revision)
260 rev_root = svn.fs.revision_root(fsobj, revision)
261
261
262 history_revisions = []
262 history_revisions = []
263 history = svn.fs.node_history(rev_root, path)
263 history = svn.fs.node_history(rev_root, path)
264 history = svn.fs.history_prev(history, cross_copies)
264 history = svn.fs.history_prev(history, cross_copies)
265 while history:
265 while history:
266 __, node_revision = svn.fs.history_location(history)
266 __, node_revision = svn.fs.history_location(history)
267 history_revisions.append(node_revision)
267 history_revisions.append(node_revision)
268 if limit and len(history_revisions) >= limit:
268 if limit and len(history_revisions) >= limit:
269 break
269 break
270 history = svn.fs.history_prev(history, cross_copies)
270 history = svn.fs.history_prev(history, cross_copies)
271 return history_revisions
271 return history_revisions
272 return _assert_correct_path(context_uid, repo_id, path, revision, limit)
272 return _assert_correct_path(context_uid, repo_id, path, revision, limit)
273
273
274 def node_properties(self, wire, path, revision):
274 def node_properties(self, wire, path, revision):
275 cache_on, context_uid, repo_id = self._cache_on(wire)
275 cache_on, context_uid, repo_id = self._cache_on(wire)
276 region = self._region(wire)
276 region = self._region(wire)
277 @region.conditional_cache_on_arguments(condition=cache_on)
277 @region.conditional_cache_on_arguments(condition=cache_on)
278 def _node_properties(_repo_id, _path, _revision):
278 def _node_properties(_repo_id, _path, _revision):
279 repo = self._factory.repo(wire)
279 repo = self._factory.repo(wire)
280 fsobj = svn.repos.fs(repo)
280 fsobj = svn.repos.fs(repo)
281 rev_root = svn.fs.revision_root(fsobj, revision)
281 rev_root = svn.fs.revision_root(fsobj, revision)
282 return svn.fs.node_proplist(rev_root, path)
282 return svn.fs.node_proplist(rev_root, path)
283 return _node_properties(repo_id, path, revision)
283 return _node_properties(repo_id, path, revision)
284
284
285 def file_annotate(self, wire, path, revision):
285 def file_annotate(self, wire, path, revision):
286 abs_path = 'file://' + urllib.request.pathname2url(
286 abs_path = 'file://' + urllib.request.pathname2url(
287 vcspath.join(wire['path'], path))
287 vcspath.join(wire['path'], path))
288 file_uri = svn.core.svn_path_canonicalize(abs_path)
288 file_uri = svn.core.svn_path_canonicalize(abs_path)
289
289
290 start_rev = svn_opt_revision_value_t(0)
290 start_rev = svn_opt_revision_value_t(0)
291 peg_rev = svn_opt_revision_value_t(revision)
291 peg_rev = svn_opt_revision_value_t(revision)
292 end_rev = peg_rev
292 end_rev = peg_rev
293
293
294 annotations = []
294 annotations = []
295
295
296 def receiver(line_no, revision, author, date, line, pool):
296 def receiver(line_no, revision, author, date, line, pool):
297 annotations.append((line_no, revision, line))
297 annotations.append((line_no, revision, line))
298
298
299 # TODO: Cannot use blame5, missing typemap function in the swig code
299 # TODO: Cannot use blame5, missing typemap function in the swig code
300 try:
300 try:
301 svn.client.blame2(
301 svn.client.blame2(
302 file_uri, peg_rev, start_rev, end_rev,
302 file_uri, peg_rev, start_rev, end_rev,
303 receiver, svn.client.create_context())
303 receiver, svn.client.create_context())
304 except svn.core.SubversionException as exc:
304 except svn.core.SubversionException as exc:
305 log.exception("Error during blame operation.")
305 log.exception("Error during blame operation.")
306 raise Exception(
306 raise Exception(
307 "Blame not supported or file does not exist at path %s. "
307 "Blame not supported or file does not exist at path %s. "
308 "Error %s." % (path, exc))
308 "Error %s." % (path, exc))
309
309
310 return annotations
310 return annotations
311
311
312 def get_node_type(self, wire, path, revision=None):
312 def get_node_type(self, wire, path, revision=None):
313
313
314 cache_on, context_uid, repo_id = self._cache_on(wire)
314 cache_on, context_uid, repo_id = self._cache_on(wire)
315 region = self._region(wire)
315 region = self._region(wire)
316 @region.conditional_cache_on_arguments(condition=cache_on)
316 @region.conditional_cache_on_arguments(condition=cache_on)
317 def _get_node_type(_repo_id, _path, _revision):
317 def _get_node_type(_repo_id, _path, _revision):
318 repo = self._factory.repo(wire)
318 repo = self._factory.repo(wire)
319 fs_ptr = svn.repos.fs(repo)
319 fs_ptr = svn.repos.fs(repo)
320 if _revision is None:
320 if _revision is None:
321 _revision = svn.fs.youngest_rev(fs_ptr)
321 _revision = svn.fs.youngest_rev(fs_ptr)
322 root = svn.fs.revision_root(fs_ptr, _revision)
322 root = svn.fs.revision_root(fs_ptr, _revision)
323 node = svn.fs.check_path(root, path)
323 node = svn.fs.check_path(root, path)
324 return NODE_TYPE_MAPPING.get(node, None)
324 return NODE_TYPE_MAPPING.get(node, None)
325 return _get_node_type(repo_id, path, revision)
325 return _get_node_type(repo_id, path, revision)
326
326
327 def get_nodes(self, wire, path, revision=None):
327 def get_nodes(self, wire, path, revision=None):
328
328
329 cache_on, context_uid, repo_id = self._cache_on(wire)
329 cache_on, context_uid, repo_id = self._cache_on(wire)
330 region = self._region(wire)
330 region = self._region(wire)
331
331
332 @region.conditional_cache_on_arguments(condition=cache_on)
332 @region.conditional_cache_on_arguments(condition=cache_on)
333 def _get_nodes(_repo_id, _path, _revision):
333 def _get_nodes(_repo_id, _path, _revision):
334 repo = self._factory.repo(wire)
334 repo = self._factory.repo(wire)
335 fsobj = svn.repos.fs(repo)
335 fsobj = svn.repos.fs(repo)
336 if _revision is None:
336 if _revision is None:
337 _revision = svn.fs.youngest_rev(fsobj)
337 _revision = svn.fs.youngest_rev(fsobj)
338 root = svn.fs.revision_root(fsobj, _revision)
338 root = svn.fs.revision_root(fsobj, _revision)
339 entries = svn.fs.dir_entries(root, path)
339 entries = svn.fs.dir_entries(root, path)
340 result = []
340 result = []
341 for entry_path, entry_info in entries.items():
341 for entry_path, entry_info in entries.items():
342 result.append(
342 result.append(
343 (entry_path, NODE_TYPE_MAPPING.get(entry_info.kind, None)))
343 (entry_path, NODE_TYPE_MAPPING.get(entry_info.kind, None)))
344 return result
344 return result
345 return _get_nodes(repo_id, path, revision)
345 return _get_nodes(repo_id, path, revision)
346
346
347 def get_file_content(self, wire, path, rev=None):
347 def get_file_content(self, wire, path, rev=None):
348 repo = self._factory.repo(wire)
348 repo = self._factory.repo(wire)
349 fsobj = svn.repos.fs(repo)
349 fsobj = svn.repos.fs(repo)
350 if rev is None:
350 if rev is None:
351 rev = svn.fs.youngest_revision(fsobj)
351 rev = svn.fs.youngest_revision(fsobj)
352 root = svn.fs.revision_root(fsobj, rev)
352 root = svn.fs.revision_root(fsobj, rev)
353 content = svn.core.Stream(svn.fs.file_contents(root, path))
353 content = svn.core.Stream(svn.fs.file_contents(root, path))
354 return content.read()
354 return content.read()
355
355
356 def get_file_size(self, wire, path, revision=None):
356 def get_file_size(self, wire, path, revision=None):
357
357
358 cache_on, context_uid, repo_id = self._cache_on(wire)
358 cache_on, context_uid, repo_id = self._cache_on(wire)
359 region = self._region(wire)
359 region = self._region(wire)
360
360
361 @region.conditional_cache_on_arguments(condition=cache_on)
361 @region.conditional_cache_on_arguments(condition=cache_on)
362 def _get_file_size(_repo_id, _path, _revision):
362 def _get_file_size(_repo_id, _path, _revision):
363 repo = self._factory.repo(wire)
363 repo = self._factory.repo(wire)
364 fsobj = svn.repos.fs(repo)
364 fsobj = svn.repos.fs(repo)
365 if _revision is None:
365 if _revision is None:
366 _revision = svn.fs.youngest_revision(fsobj)
366 _revision = svn.fs.youngest_revision(fsobj)
367 root = svn.fs.revision_root(fsobj, _revision)
367 root = svn.fs.revision_root(fsobj, _revision)
368 size = svn.fs.file_length(root, path)
368 size = svn.fs.file_length(root, path)
369 return size
369 return size
370 return _get_file_size(repo_id, path, revision)
370 return _get_file_size(repo_id, path, revision)
371
371
372 def create_repository(self, wire, compatible_version=None):
372 def create_repository(self, wire, compatible_version=None):
373 log.info('Creating Subversion repository in path "%s"', wire['path'])
373 log.info('Creating Subversion repository in path "%s"', wire['path'])
374 self._factory.repo(wire, create=True,
374 self._factory.repo(wire, create=True,
375 compatible_version=compatible_version)
375 compatible_version=compatible_version)
376
376
377 def get_url_and_credentials(self, src_url):
377 def get_url_and_credentials(self, src_url):
378 obj = urllib.parse.urlparse(src_url)
378 obj = urllib.parse.urlparse(src_url)
379 username = obj.username or None
379 username = obj.username or None
380 password = obj.password or None
380 password = obj.password or None
381 return username, password, src_url
381 return username, password, src_url
382
382
383 def import_remote_repository(self, wire, src_url):
383 def import_remote_repository(self, wire, src_url):
384 repo_path = wire['path']
384 repo_path = wire['path']
385 if not self.is_path_valid_repository(wire, repo_path):
385 if not self.is_path_valid_repository(wire, repo_path):
386 raise Exception(
386 raise Exception(
387 "Path %s is not a valid Subversion repository." % repo_path)
387 "Path %s is not a valid Subversion repository." % repo_path)
388
388
389 username, password, src_url = self.get_url_and_credentials(src_url)
389 username, password, src_url = self.get_url_and_credentials(src_url)
390 rdump_cmd = ['svnrdump', 'dump', '--non-interactive',
390 rdump_cmd = ['svnrdump', 'dump', '--non-interactive',
391 '--trust-server-cert-failures=unknown-ca']
391 '--trust-server-cert-failures=unknown-ca']
392 if username and password:
392 if username and password:
393 rdump_cmd += ['--username', username, '--password', password]
393 rdump_cmd += ['--username', username, '--password', password]
394 rdump_cmd += [src_url]
394 rdump_cmd += [src_url]
395
395
396 rdump = subprocess.Popen(
396 rdump = subprocess.Popen(
397 rdump_cmd,
397 rdump_cmd,
398 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
398 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
399 load = subprocess.Popen(
399 load = subprocess.Popen(
400 ['svnadmin', 'load', repo_path], stdin=rdump.stdout)
400 ['svnadmin', 'load', repo_path], stdin=rdump.stdout)
401
401
402 # TODO: johbo: This can be a very long operation, might be better
402 # TODO: johbo: This can be a very long operation, might be better
403 # to track some kind of status and provide an api to check if the
403 # to track some kind of status and provide an api to check if the
404 # import is done.
404 # import is done.
405 rdump.wait()
405 rdump.wait()
406 load.wait()
406 load.wait()
407
407
408 log.debug('Return process ended with code: %s', rdump.returncode)
408 log.debug('Return process ended with code: %s', rdump.returncode)
409 if rdump.returncode != 0:
409 if rdump.returncode != 0:
410 errors = rdump.stderr.read()
410 errors = rdump.stderr.read()
411 log.error('svnrdump dump failed: statuscode %s: message: %s', rdump.returncode, errors)
411 log.error('svnrdump dump failed: statuscode %s: message: %s', rdump.returncode, errors)
412
412
413 reason = 'UNKNOWN'
413 reason = 'UNKNOWN'
414 if b'svnrdump: E230001:' in errors:
414 if b'svnrdump: E230001:' in errors:
415 reason = 'INVALID_CERTIFICATE'
415 reason = 'INVALID_CERTIFICATE'
416
416
417 if reason == 'UNKNOWN':
417 if reason == 'UNKNOWN':
418 reason = 'UNKNOWN:{}'.format(safe_str(errors))
418 reason = 'UNKNOWN:{}'.format(safe_str(errors))
419
419
420 raise Exception(
420 raise Exception(
421 'Failed to dump the remote repository from %s. Reason:%s' % (
421 'Failed to dump the remote repository from %s. Reason:%s' % (
422 src_url, reason))
422 src_url, reason))
423 if load.returncode != 0:
423 if load.returncode != 0:
424 raise Exception(
424 raise Exception(
425 'Failed to load the dump of remote repository from %s.' %
425 'Failed to load the dump of remote repository from %s.' %
426 (src_url, ))
426 (src_url, ))
427
427
428 def commit(self, wire, message, author, timestamp, updated, removed):
428 def commit(self, wire, message, author, timestamp, updated, removed):
429
429
430 updated = [{k: safe_bytes(v) for k, v in x.items() if isinstance(v, str)} for x in updated]
430 updated = [{k: safe_bytes(v) for k, v in x.items() if isinstance(v, str)} for x in updated]
431
431
432 message = safe_bytes(message)
432 message = safe_bytes(message)
433 author = safe_bytes(author)
433 author = safe_bytes(author)
434
434
435 repo = self._factory.repo(wire)
435 repo = self._factory.repo(wire)
436 fsobj = svn.repos.fs(repo)
436 fsobj = svn.repos.fs(repo)
437
437
438 rev = svn.fs.youngest_rev(fsobj)
438 rev = svn.fs.youngest_rev(fsobj)
439 txn = svn.repos.fs_begin_txn_for_commit(repo, rev, author, message)
439 txn = svn.repos.fs_begin_txn_for_commit(repo, rev, author, message)
440 txn_root = svn.fs.txn_root(txn)
440 txn_root = svn.fs.txn_root(txn)
441
441
442 for node in updated:
442 for node in updated:
443 TxnNodeProcessor(node, txn_root).update()
443 TxnNodeProcessor(node, txn_root).update()
444 for node in removed:
444 for node in removed:
445 TxnNodeProcessor(node, txn_root).remove()
445 TxnNodeProcessor(node, txn_root).remove()
446
446
447 commit_id = svn.repos.fs_commit_txn(repo, txn)
447 commit_id = svn.repos.fs_commit_txn(repo, txn)
448
448
449 if timestamp:
449 if timestamp:
450 apr_time = apr_time_t(timestamp)
450 apr_time = apr_time_t(timestamp)
451 ts_formatted = svn.core.svn_time_to_cstring(apr_time)
451 ts_formatted = svn.core.svn_time_to_cstring(apr_time)
452 svn.fs.change_rev_prop(fsobj, commit_id, 'svn:date', ts_formatted)
452 svn.fs.change_rev_prop(fsobj, commit_id, 'svn:date', ts_formatted)
453
453
454 log.debug('Committed revision "%s" to "%s".', commit_id, wire['path'])
454 log.debug('Committed revision "%s" to "%s".', commit_id, wire['path'])
455 return commit_id
455 return commit_id
456
456
457 def diff(self, wire, rev1, rev2, path1=None, path2=None,
457 def diff(self, wire, rev1, rev2, path1=None, path2=None,
458 ignore_whitespace=False, context=3):
458 ignore_whitespace=False, context=3):
459
459
460 wire.update(cache=False)
460 wire.update(cache=False)
461 repo = self._factory.repo(wire)
461 repo = self._factory.repo(wire)
462 diff_creator = SvnDiffer(
462 diff_creator = SvnDiffer(
463 repo, rev1, path1, rev2, path2, ignore_whitespace, context)
463 repo, rev1, path1, rev2, path2, ignore_whitespace, context)
464 try:
464 try:
465 return diff_creator.generate_diff()
465 return diff_creator.generate_diff()
466 except svn.core.SubversionException as e:
466 except svn.core.SubversionException as e:
467 log.exception(
467 log.exception(
468 "Error during diff operation operation. "
468 "Error during diff operation operation. "
469 "Path might not exist %s, %s" % (path1, path2))
469 "Path might not exist %s, %s" % (path1, path2))
470 return ""
470 return ""
471
471
472 @reraise_safe_exceptions
472 @reraise_safe_exceptions
473 def is_large_file(self, wire, path):
473 def is_large_file(self, wire, path):
474 return False
474 return False
475
475
476 @reraise_safe_exceptions
476 @reraise_safe_exceptions
477 def is_binary(self, wire, rev, path):
477 def is_binary(self, wire, rev, path):
478 cache_on, context_uid, repo_id = self._cache_on(wire)
478 cache_on, context_uid, repo_id = self._cache_on(wire)
479 region = self._region(wire)
479 region = self._region(wire)
480
480
481 @region.conditional_cache_on_arguments(condition=cache_on)
481 @region.conditional_cache_on_arguments(condition=cache_on)
482 def _is_binary(_repo_id, _rev, _path):
482 def _is_binary(_repo_id, _rev, _path):
483 raw_bytes = self.get_file_content(wire, path, rev)
483 raw_bytes = self.get_file_content(wire, path, rev)
484 return raw_bytes and b'\0' in raw_bytes
484 return raw_bytes and b'\0' in raw_bytes
485
485
486 return _is_binary(repo_id, rev, path)
486 return _is_binary(repo_id, rev, path)
487
487
488 @reraise_safe_exceptions
488 @reraise_safe_exceptions
489 def md5_hash(self, wire, rev, path):
489 def md5_hash(self, wire, rev, path):
490 cache_on, context_uid, repo_id = self._cache_on(wire)
490 cache_on, context_uid, repo_id = self._cache_on(wire)
491 region = self._region(wire)
491 region = self._region(wire)
492
492
493 @region.conditional_cache_on_arguments(condition=cache_on)
493 @region.conditional_cache_on_arguments(condition=cache_on)
494 def _md5_hash(_repo_id, _rev, _path):
494 def _md5_hash(_repo_id, _rev, _path):
495 return ''
495 return ''
496
496
497 return _md5_hash(repo_id, rev, path)
497 return _md5_hash(repo_id, rev, path)
498
498
499 @reraise_safe_exceptions
499 @reraise_safe_exceptions
500 def run_svn_command(self, wire, cmd, **opts):
500 def run_svn_command(self, wire, cmd, **opts):
501 path = wire.get('path', None)
501 path = wire.get('path', None)
502
502
503 if path and os.path.isdir(path):
503 if path and os.path.isdir(path):
504 opts['cwd'] = path
504 opts['cwd'] = path
505
505
506 safe_call = opts.pop('_safe', False)
506 safe_call = opts.pop('_safe', False)
507
507
508 svnenv = os.environ.copy()
508 svnenv = os.environ.copy()
509 svnenv.update(opts.pop('extra_env', {}))
509 svnenv.update(opts.pop('extra_env', {}))
510
510
511 _opts = {'env': svnenv, 'shell': False}
511 _opts = {'env': svnenv, 'shell': False}
512
512
513 try:
513 try:
514 _opts.update(opts)
514 _opts.update(opts)
515 proc = subprocessio.SubprocessIOChunker(cmd, **_opts)
515 proc = subprocessio.SubprocessIOChunker(cmd, **_opts)
516
516
517 return b''.join(proc), b''.join(proc.stderr)
517 return b''.join(proc), b''.join(proc.stderr)
518 except OSError as err:
518 except OSError as err:
519 if safe_call:
519 if safe_call:
520 return '', safe_str(err).strip()
520 return '', safe_str(err).strip()
521 else:
521 else:
522 cmd = ' '.join(map(safe_str, cmd)) # human friendly CMD
522 cmd = ' '.join(map(safe_str, cmd)) # human friendly CMD
523 tb_err = ("Couldn't run svn command (%s).\n"
523 tb_err = ("Couldn't run svn command (%s).\n"
524 "Original error was:%s\n"
524 "Original error was:%s\n"
525 "Call options:%s\n"
525 "Call options:%s\n"
526 % (cmd, err, _opts))
526 % (cmd, err, _opts))
527 log.exception(tb_err)
527 log.exception(tb_err)
528 raise exceptions.VcsException()(tb_err)
528 raise exceptions.VcsException()(tb_err)
529
529
530 @reraise_safe_exceptions
530 @reraise_safe_exceptions
531 def install_hooks(self, wire, force=False):
531 def install_hooks(self, wire, force=False):
532 from vcsserver.hook_utils import install_svn_hooks
532 from vcsserver.hook_utils import install_svn_hooks
533 repo_path = wire['path']
533 repo_path = wire['path']
534 binary_dir = settings.BINARY_DIR
534 binary_dir = settings.BINARY_DIR
535 executable = None
535 executable = None
536 if binary_dir:
536 if binary_dir:
537 executable = os.path.join(binary_dir, 'python3')
537 executable = os.path.join(binary_dir, 'python3')
538 return install_svn_hooks(repo_path, force_create=force)
538 return install_svn_hooks(repo_path, force_create=force)
539
539
540 @reraise_safe_exceptions
540 @reraise_safe_exceptions
541 def get_hooks_info(self, wire):
541 def get_hooks_info(self, wire):
542 from vcsserver.hook_utils import (
542 from vcsserver.hook_utils import (
543 get_svn_pre_hook_version, get_svn_post_hook_version)
543 get_svn_pre_hook_version, get_svn_post_hook_version)
544 repo_path = wire['path']
544 repo_path = wire['path']
545 return {
545 return {
546 'pre_version': get_svn_pre_hook_version(repo_path),
546 'pre_version': get_svn_pre_hook_version(repo_path),
547 'post_version': get_svn_post_hook_version(repo_path),
547 'post_version': get_svn_post_hook_version(repo_path),
548 }
548 }
549
549
550 @reraise_safe_exceptions
550 @reraise_safe_exceptions
551 def set_head_ref(self, wire, head_name):
551 def set_head_ref(self, wire, head_name):
552 pass
552 pass
553
553
554 @reraise_safe_exceptions
554 @reraise_safe_exceptions
555 def archive_repo(self, wire, archive_dest_path, kind, mtime, archive_at_path,
555 def archive_repo(self, wire, archive_dest_path, kind, mtime, archive_at_path,
556 archive_dir_name, commit_id):
556 archive_dir_name, commit_id):
557
557
558 def walk_tree(root, root_dir, _commit_id):
558 def walk_tree(root, root_dir, _commit_id):
559 """
559 """
560 Special recursive svn repo walker
560 Special recursive svn repo walker
561 """
561 """
562 root_dir = safe_bytes(root_dir)
562
563
563 filemode_default = 0o100644
564 filemode_default = 0o100644
564 filemode_executable = 0o100755
565 filemode_executable = 0o100755
565
566
566 file_iter = svn.fs.dir_entries(root, root_dir)
567 file_iter = svn.fs.dir_entries(root, root_dir)
567 for f_name in file_iter:
568 for f_name in file_iter:
568 f_type = NODE_TYPE_MAPPING.get(file_iter[f_name].kind, None)
569 f_type = NODE_TYPE_MAPPING.get(file_iter[f_name].kind, None)
569
570
570 if f_type == 'dir':
571 if f_type == 'dir':
571 # return only DIR, and then all entries in that dir
572 # return only DIR, and then all entries in that dir
572 yield os.path.join(root_dir, f_name), {'mode': filemode_default}, f_type
573 yield os.path.join(root_dir, f_name), {'mode': filemode_default}, f_type
573 new_root = os.path.join(root_dir, f_name)
574 new_root = os.path.join(root_dir, f_name)
574 for _f_name, _f_data, _f_type in walk_tree(root, new_root, _commit_id):
575 for _f_name, _f_data, _f_type in walk_tree(root, new_root, _commit_id):
575 yield _f_name, _f_data, _f_type
576 yield _f_name, _f_data, _f_type
576 else:
577 else:
577 f_path = os.path.join(root_dir, f_name).rstrip('/')
578
579 f_path = os.path.join(root_dir, f_name).rstrip(b'/')
578 prop_list = svn.fs.node_proplist(root, f_path)
580 prop_list = svn.fs.node_proplist(root, f_path)
579
581
580 f_mode = filemode_default
582 f_mode = filemode_default
581 if prop_list.get('svn:executable'):
583 if prop_list.get('svn:executable'):
582 f_mode = filemode_executable
584 f_mode = filemode_executable
583
585
584 f_is_link = False
586 f_is_link = False
585 if prop_list.get('svn:special'):
587 if prop_list.get('svn:special'):
586 f_is_link = True
588 f_is_link = True
587
589
588 data = {
590 data = {
589 'is_link': f_is_link,
591 'is_link': f_is_link,
590 'mode': f_mode,
592 'mode': f_mode,
591 'content_stream': svn.core.Stream(svn.fs.file_contents(root, f_path)).read
593 'content_stream': svn.core.Stream(svn.fs.file_contents(root, f_path)).read
592 }
594 }
593
595
594 yield f_path, data, f_type
596 yield f_path, data, f_type
595
597
596 def file_walker(_commit_id, path):
598 def file_walker(_commit_id, path):
597 repo = self._factory.repo(wire)
599 repo = self._factory.repo(wire)
598 root = svn.fs.revision_root(svn.repos.fs(repo), int(commit_id))
600 root = svn.fs.revision_root(svn.repos.fs(repo), int(commit_id))
599
601
600 def no_content():
602 def no_content():
601 raise NoContentException()
603 raise NoContentException()
602
604
603 for f_name, f_data, f_type in walk_tree(root, path, _commit_id):
605 for f_name, f_data, f_type in walk_tree(root, path, _commit_id):
604 file_path = f_name
606 file_path = f_name
605
607
606 if f_type == 'dir':
608 if f_type == 'dir':
607 mode = f_data['mode']
609 mode = f_data['mode']
608 yield ArchiveNode(file_path, mode, False, no_content)
610 yield ArchiveNode(file_path, mode, False, no_content)
609 else:
611 else:
610 mode = f_data['mode']
612 mode = f_data['mode']
611 is_link = f_data['is_link']
613 is_link = f_data['is_link']
612 data_stream = f_data['content_stream']
614 data_stream = f_data['content_stream']
613 yield ArchiveNode(file_path, mode, is_link, data_stream)
615 yield ArchiveNode(file_path, mode, is_link, data_stream)
614
616
615 return archive_repo(file_walker, archive_dest_path, kind, mtime, archive_at_path,
617 return archive_repo(file_walker, archive_dest_path, kind, mtime, archive_at_path,
616 archive_dir_name, commit_id)
618 archive_dir_name, commit_id)
617
619
618
620
619 class SvnDiffer(object):
621 class SvnDiffer(object):
620 """
622 """
621 Utility to create diffs based on difflib and the Subversion api
623 Utility to create diffs based on difflib and the Subversion api
622 """
624 """
623
625
624 binary_content = False
626 binary_content = False
625
627
626 def __init__(
628 def __init__(
627 self, repo, src_rev, src_path, tgt_rev, tgt_path,
629 self, repo, src_rev, src_path, tgt_rev, tgt_path,
628 ignore_whitespace, context):
630 ignore_whitespace, context):
629 self.repo = repo
631 self.repo = repo
630 self.ignore_whitespace = ignore_whitespace
632 self.ignore_whitespace = ignore_whitespace
631 self.context = context
633 self.context = context
632
634
633 fsobj = svn.repos.fs(repo)
635 fsobj = svn.repos.fs(repo)
634
636
635 self.tgt_rev = tgt_rev
637 self.tgt_rev = tgt_rev
636 self.tgt_path = tgt_path or ''
638 self.tgt_path = tgt_path or ''
637 self.tgt_root = svn.fs.revision_root(fsobj, tgt_rev)
639 self.tgt_root = svn.fs.revision_root(fsobj, tgt_rev)
638 self.tgt_kind = svn.fs.check_path(self.tgt_root, self.tgt_path)
640 self.tgt_kind = svn.fs.check_path(self.tgt_root, self.tgt_path)
639
641
640 self.src_rev = src_rev
642 self.src_rev = src_rev
641 self.src_path = src_path or self.tgt_path
643 self.src_path = src_path or self.tgt_path
642 self.src_root = svn.fs.revision_root(fsobj, src_rev)
644 self.src_root = svn.fs.revision_root(fsobj, src_rev)
643 self.src_kind = svn.fs.check_path(self.src_root, self.src_path)
645 self.src_kind = svn.fs.check_path(self.src_root, self.src_path)
644
646
645 self._validate()
647 self._validate()
646
648
647 def _validate(self):
649 def _validate(self):
648 if (self.tgt_kind != svn.core.svn_node_none and
650 if (self.tgt_kind != svn.core.svn_node_none and
649 self.src_kind != svn.core.svn_node_none and
651 self.src_kind != svn.core.svn_node_none and
650 self.src_kind != self.tgt_kind):
652 self.src_kind != self.tgt_kind):
651 # TODO: johbo: proper error handling
653 # TODO: johbo: proper error handling
652 raise Exception(
654 raise Exception(
653 "Source and target are not compatible for diff generation. "
655 "Source and target are not compatible for diff generation. "
654 "Source type: %s, target type: %s" %
656 "Source type: %s, target type: %s" %
655 (self.src_kind, self.tgt_kind))
657 (self.src_kind, self.tgt_kind))
656
658
657 def generate_diff(self):
659 def generate_diff(self):
658 buf = io.StringIO()
660 buf = io.StringIO()
659 if self.tgt_kind == svn.core.svn_node_dir:
661 if self.tgt_kind == svn.core.svn_node_dir:
660 self._generate_dir_diff(buf)
662 self._generate_dir_diff(buf)
661 else:
663 else:
662 self._generate_file_diff(buf)
664 self._generate_file_diff(buf)
663 return buf.getvalue()
665 return buf.getvalue()
664
666
665 def _generate_dir_diff(self, buf):
667 def _generate_dir_diff(self, buf):
666 editor = DiffChangeEditor()
668 editor = DiffChangeEditor()
667 editor_ptr, editor_baton = svn.delta.make_editor(editor)
669 editor_ptr, editor_baton = svn.delta.make_editor(editor)
668 svn.repos.dir_delta2(
670 svn.repos.dir_delta2(
669 self.src_root,
671 self.src_root,
670 self.src_path,
672 self.src_path,
671 '', # src_entry
673 '', # src_entry
672 self.tgt_root,
674 self.tgt_root,
673 self.tgt_path,
675 self.tgt_path,
674 editor_ptr, editor_baton,
676 editor_ptr, editor_baton,
675 authorization_callback_allow_all,
677 authorization_callback_allow_all,
676 False, # text_deltas
678 False, # text_deltas
677 svn.core.svn_depth_infinity, # depth
679 svn.core.svn_depth_infinity, # depth
678 False, # entry_props
680 False, # entry_props
679 False, # ignore_ancestry
681 False, # ignore_ancestry
680 )
682 )
681
683
682 for path, __, change in sorted(editor.changes):
684 for path, __, change in sorted(editor.changes):
683 self._generate_node_diff(
685 self._generate_node_diff(
684 buf, change, path, self.tgt_path, path, self.src_path)
686 buf, change, path, self.tgt_path, path, self.src_path)
685
687
686 def _generate_file_diff(self, buf):
688 def _generate_file_diff(self, buf):
687 change = None
689 change = None
688 if self.src_kind == svn.core.svn_node_none:
690 if self.src_kind == svn.core.svn_node_none:
689 change = "add"
691 change = "add"
690 elif self.tgt_kind == svn.core.svn_node_none:
692 elif self.tgt_kind == svn.core.svn_node_none:
691 change = "delete"
693 change = "delete"
692 tgt_base, tgt_path = vcspath.split(self.tgt_path)
694 tgt_base, tgt_path = vcspath.split(self.tgt_path)
693 src_base, src_path = vcspath.split(self.src_path)
695 src_base, src_path = vcspath.split(self.src_path)
694 self._generate_node_diff(
696 self._generate_node_diff(
695 buf, change, tgt_path, tgt_base, src_path, src_base)
697 buf, change, tgt_path, tgt_base, src_path, src_base)
696
698
697 def _generate_node_diff(
699 def _generate_node_diff(
698 self, buf, change, tgt_path, tgt_base, src_path, src_base):
700 self, buf, change, tgt_path, tgt_base, src_path, src_base):
699
701
702
703 tgt_path = safe_str(tgt_path)
704 src_path = safe_str(src_path)
705
706
700 if self.src_rev == self.tgt_rev and tgt_base == src_base:
707 if self.src_rev == self.tgt_rev and tgt_base == src_base:
701 # makes consistent behaviour with git/hg to return empty diff if
708 # makes consistent behaviour with git/hg to return empty diff if
702 # we compare same revisions
709 # we compare same revisions
703 return
710 return
704
711
705 tgt_full_path = vcspath.join(tgt_base, tgt_path)
712 tgt_full_path = vcspath.join(tgt_base, tgt_path)
706 src_full_path = vcspath.join(src_base, src_path)
713 src_full_path = vcspath.join(src_base, src_path)
707
714
708 self.binary_content = False
715 self.binary_content = False
709 mime_type = self._get_mime_type(tgt_full_path)
716 mime_type = self._get_mime_type(tgt_full_path)
710
717
711 if mime_type and not mime_type.startswith('text'):
718 if mime_type and not mime_type.startswith('text'):
712 self.binary_content = True
719 self.binary_content = True
713 buf.write("=" * 67 + '\n')
720 buf.write("=" * 67 + '\n')
714 buf.write("Cannot display: file marked as a binary type.\n")
721 buf.write("Cannot display: file marked as a binary type.\n")
715 buf.write("svn:mime-type = %s\n" % mime_type)
722 buf.write("svn:mime-type = %s\n" % mime_type)
716 buf.write("Index: %s\n" % (tgt_path, ))
723 buf.write("Index: %s\n" % (tgt_path, ))
717 buf.write("=" * 67 + '\n')
724 buf.write("=" * 67 + '\n')
718 buf.write("diff --git a/%(tgt_path)s b/%(tgt_path)s\n" % {
725 buf.write("diff --git a/%(tgt_path)s b/%(tgt_path)s\n" % {
719 'tgt_path': tgt_path})
726 'tgt_path': tgt_path})
720
727
721 if change == 'add':
728 if change == 'add':
722 # TODO: johbo: SVN is missing a zero here compared to git
729 # TODO: johbo: SVN is missing a zero here compared to git
723 buf.write("new file mode 10644\n")
730 buf.write("new file mode 10644\n")
724
731
725 #TODO(marcink): intro to binary detection of svn patches
732 #TODO(marcink): intro to binary detection of svn patches
726 # if self.binary_content:
733 # if self.binary_content:
727 # buf.write('GIT binary patch\n')
734 # buf.write('GIT binary patch\n')
728
735
729 buf.write("--- /dev/null\t(revision 0)\n")
736 buf.write("--- /dev/null\t(revision 0)\n")
730 src_lines = []
737 src_lines = []
731 else:
738 else:
732 if change == 'delete':
739 if change == 'delete':
733 buf.write("deleted file mode 10644\n")
740 buf.write("deleted file mode 10644\n")
734
741
735 #TODO(marcink): intro to binary detection of svn patches
742 #TODO(marcink): intro to binary detection of svn patches
736 # if self.binary_content:
743 # if self.binary_content:
737 # buf.write('GIT binary patch\n')
744 # buf.write('GIT binary patch\n')
738
745
739 buf.write("--- a/%s\t(revision %s)\n" % (
746 buf.write("--- a/%s\t(revision %s)\n" % (
740 src_path, self.src_rev))
747 src_path, self.src_rev))
741 src_lines = self._svn_readlines(self.src_root, src_full_path)
748 src_lines = self._svn_readlines(self.src_root, src_full_path)
742
749
743 if change == 'delete':
750 if change == 'delete':
744 buf.write("+++ /dev/null\t(revision %s)\n" % (self.tgt_rev, ))
751 buf.write("+++ /dev/null\t(revision %s)\n" % (self.tgt_rev, ))
745 tgt_lines = []
752 tgt_lines = []
746 else:
753 else:
747 buf.write("+++ b/%s\t(revision %s)\n" % (
754 buf.write("+++ b/%s\t(revision %s)\n" % (
748 tgt_path, self.tgt_rev))
755 tgt_path, self.tgt_rev))
749 tgt_lines = self._svn_readlines(self.tgt_root, tgt_full_path)
756 tgt_lines = self._svn_readlines(self.tgt_root, tgt_full_path)
750
757
751 if not self.binary_content:
758 if not self.binary_content:
752 udiff = svn_diff.unified_diff(
759 udiff = svn_diff.unified_diff(
753 src_lines, tgt_lines, context=self.context,
760 src_lines, tgt_lines, context=self.context,
754 ignore_blank_lines=self.ignore_whitespace,
761 ignore_blank_lines=self.ignore_whitespace,
755 ignore_case=False,
762 ignore_case=False,
756 ignore_space_changes=self.ignore_whitespace)
763 ignore_space_changes=self.ignore_whitespace)
764
757 buf.writelines(udiff)
765 buf.writelines(udiff)
758
766
759 def _get_mime_type(self, path):
767 def _get_mime_type(self, path):
760 try:
768 try:
761 mime_type = svn.fs.node_prop(
769 mime_type = svn.fs.node_prop(
762 self.tgt_root, path, svn.core.SVN_PROP_MIME_TYPE)
770 self.tgt_root, path, svn.core.SVN_PROP_MIME_TYPE)
763 except svn.core.SubversionException:
771 except svn.core.SubversionException:
764 mime_type = svn.fs.node_prop(
772 mime_type = svn.fs.node_prop(
765 self.src_root, path, svn.core.SVN_PROP_MIME_TYPE)
773 self.src_root, path, svn.core.SVN_PROP_MIME_TYPE)
766 return mime_type
774 return mime_type
767
775
768 def _svn_readlines(self, fs_root, node_path):
776 def _svn_readlines(self, fs_root, node_path):
769 if self.binary_content:
777 if self.binary_content:
770 return []
778 return []
771 node_kind = svn.fs.check_path(fs_root, node_path)
779 node_kind = svn.fs.check_path(fs_root, node_path)
772 if node_kind not in (
780 if node_kind not in (
773 svn.core.svn_node_file, svn.core.svn_node_symlink):
781 svn.core.svn_node_file, svn.core.svn_node_symlink):
774 return []
782 return []
775 content = svn.core.Stream(
783 content = svn.core.Stream(
776 svn.fs.file_contents(fs_root, node_path)).read()
784 svn.fs.file_contents(fs_root, node_path)).read()
785
777 return content.splitlines(True)
786 return content.splitlines(True)
778
787
779
788
780 class DiffChangeEditor(svn.delta.Editor):
789 class DiffChangeEditor(svn.delta.Editor):
781 """
790 """
782 Records changes between two given revisions
791 Records changes between two given revisions
783 """
792 """
784
793
785 def __init__(self):
794 def __init__(self):
786 self.changes = []
795 self.changes = []
787
796
788 def delete_entry(self, path, revision, parent_baton, pool=None):
797 def delete_entry(self, path, revision, parent_baton, pool=None):
789 self.changes.append((path, None, 'delete'))
798 self.changes.append((path, None, 'delete'))
790
799
791 def add_file(
800 def add_file(
792 self, path, parent_baton, copyfrom_path, copyfrom_revision,
801 self, path, parent_baton, copyfrom_path, copyfrom_revision,
793 file_pool=None):
802 file_pool=None):
794 self.changes.append((path, 'file', 'add'))
803 self.changes.append((path, 'file', 'add'))
795
804
796 def open_file(self, path, parent_baton, base_revision, file_pool=None):
805 def open_file(self, path, parent_baton, base_revision, file_pool=None):
797 self.changes.append((path, 'file', 'change'))
806 self.changes.append((path, 'file', 'change'))
798
807
799
808
800 def authorization_callback_allow_all(root, path, pool):
809 def authorization_callback_allow_all(root, path, pool):
801 return True
810 return True
802
811
803
812
804 class TxnNodeProcessor(object):
813 class TxnNodeProcessor(object):
805 """
814 """
806 Utility to process the change of one node within a transaction root.
815 Utility to process the change of one node within a transaction root.
807
816
808 It encapsulates the knowledge of how to add, update or remove
817 It encapsulates the knowledge of how to add, update or remove
809 a node for a given transaction root. The purpose is to support the method
818 a node for a given transaction root. The purpose is to support the method
810 `SvnRemote.commit`.
819 `SvnRemote.commit`.
811 """
820 """
812
821
813 def __init__(self, node, txn_root):
822 def __init__(self, node, txn_root):
814 assert isinstance(node['path'], bytes)
823 assert isinstance(node['path'], bytes)
815
824
816 self.node = node
825 self.node = node
817 self.txn_root = txn_root
826 self.txn_root = txn_root
818
827
819 def update(self):
828 def update(self):
820 self._ensure_parent_dirs()
829 self._ensure_parent_dirs()
821 self._add_file_if_node_does_not_exist()
830 self._add_file_if_node_does_not_exist()
822 self._update_file_content()
831 self._update_file_content()
823 self._update_file_properties()
832 self._update_file_properties()
824
833
825 def remove(self):
834 def remove(self):
826 svn.fs.delete(self.txn_root, self.node['path'])
835 svn.fs.delete(self.txn_root, self.node['path'])
827 # TODO: Clean up directory if empty
836 # TODO: Clean up directory if empty
828
837
829 def _ensure_parent_dirs(self):
838 def _ensure_parent_dirs(self):
830 curdir = vcspath.dirname(self.node['path'])
839 curdir = vcspath.dirname(self.node['path'])
831 dirs_to_create = []
840 dirs_to_create = []
832 while not self._svn_path_exists(curdir):
841 while not self._svn_path_exists(curdir):
833 dirs_to_create.append(curdir)
842 dirs_to_create.append(curdir)
834 curdir = vcspath.dirname(curdir)
843 curdir = vcspath.dirname(curdir)
835
844
836 for curdir in reversed(dirs_to_create):
845 for curdir in reversed(dirs_to_create):
837 log.debug('Creating missing directory "%s"', curdir)
846 log.debug('Creating missing directory "%s"', curdir)
838 svn.fs.make_dir(self.txn_root, curdir)
847 svn.fs.make_dir(self.txn_root, curdir)
839
848
840 def _svn_path_exists(self, path):
849 def _svn_path_exists(self, path):
841 path_status = svn.fs.check_path(self.txn_root, path)
850 path_status = svn.fs.check_path(self.txn_root, path)
842 return path_status != svn.core.svn_node_none
851 return path_status != svn.core.svn_node_none
843
852
844 def _add_file_if_node_does_not_exist(self):
853 def _add_file_if_node_does_not_exist(self):
845 kind = svn.fs.check_path(self.txn_root, self.node['path'])
854 kind = svn.fs.check_path(self.txn_root, self.node['path'])
846 if kind == svn.core.svn_node_none:
855 if kind == svn.core.svn_node_none:
847 svn.fs.make_file(self.txn_root, self.node['path'])
856 svn.fs.make_file(self.txn_root, self.node['path'])
848
857
849 def _update_file_content(self):
858 def _update_file_content(self):
850 assert isinstance(self.node['content'], bytes)
859 assert isinstance(self.node['content'], bytes)
851
860
852 handler, baton = svn.fs.apply_textdelta(
861 handler, baton = svn.fs.apply_textdelta(
853 self.txn_root, self.node['path'], None, None)
862 self.txn_root, self.node['path'], None, None)
854 svn.delta.svn_txdelta_send_string(self.node['content'], handler, baton)
863 svn.delta.svn_txdelta_send_string(self.node['content'], handler, baton)
855
864
856 def _update_file_properties(self):
865 def _update_file_properties(self):
857 properties = self.node.get('properties', {})
866 properties = self.node.get('properties', {})
858 for key, value in properties.items():
867 for key, value in properties.items():
859 svn.fs.change_node_prop(
868 svn.fs.change_node_prop(
860 self.txn_root, self.node['path'], key, value)
869 self.txn_root, self.node['path'], key, value)
861
870
862
871
863 def apr_time_t(timestamp):
872 def apr_time_t(timestamp):
864 """
873 """
865 Convert a Python timestamp into APR timestamp type apr_time_t
874 Convert a Python timestamp into APR timestamp type apr_time_t
866 """
875 """
867 return timestamp * 1E6
876 return timestamp * 1E6
868
877
869
878
870 def svn_opt_revision_value_t(num):
879 def svn_opt_revision_value_t(num):
871 """
880 """
872 Put `num` into a `svn_opt_revision_value_t` structure.
881 Put `num` into a `svn_opt_revision_value_t` structure.
873 """
882 """
874 value = svn.core.svn_opt_revision_value_t()
883 value = svn.core.svn_opt_revision_value_t()
875 value.number = num
884 value.number = num
876 revision = svn.core.svn_opt_revision_t()
885 revision = svn.core.svn_opt_revision_t()
877 revision.kind = svn.core.svn_opt_revision_number
886 revision.kind = svn.core.svn_opt_revision_number
878 revision.value = value
887 revision.value = value
879 return revision
888 return revision
@@ -1,209 +1,210 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 #
2 #
3 # Copyright (C) 2004-2009 Edgewall Software
3 # Copyright (C) 2004-2009 Edgewall Software
4 # Copyright (C) 2004-2006 Christopher Lenz <cmlenz@gmx.de>
4 # Copyright (C) 2004-2006 Christopher Lenz <cmlenz@gmx.de>
5 # All rights reserved.
5 # All rights reserved.
6 #
6 #
7 # This software is licensed as described in the file COPYING, which
7 # This software is licensed as described in the file COPYING, which
8 # you should have received as part of this distribution. The terms
8 # you should have received as part of this distribution. The terms
9 # are also available at http://trac.edgewall.org/wiki/TracLicense.
9 # are also available at http://trac.edgewall.org/wiki/TracLicense.
10 #
10 #
11 # This software consists of voluntary contributions made by many
11 # This software consists of voluntary contributions made by many
12 # individuals. For the exact contribution history, see the revision
12 # individuals. For the exact contribution history, see the revision
13 # history and logs, available at http://trac.edgewall.org/log/.
13 # history and logs, available at http://trac.edgewall.org/log/.
14 #
14 #
15 # Author: Christopher Lenz <cmlenz@gmx.de>
15 # Author: Christopher Lenz <cmlenz@gmx.de>
16
16
17 import difflib
17 import difflib
18
18
19
19
20 def get_filtered_hunks(fromlines, tolines, context=None,
20 def get_filtered_hunks(fromlines, tolines, context=None,
21 ignore_blank_lines=False, ignore_case=False,
21 ignore_blank_lines=False, ignore_case=False,
22 ignore_space_changes=False):
22 ignore_space_changes=False):
23 """Retrieve differences in the form of `difflib.SequenceMatcher`
23 """Retrieve differences in the form of `difflib.SequenceMatcher`
24 opcodes, grouped according to the ``context`` and ``ignore_*``
24 opcodes, grouped according to the ``context`` and ``ignore_*``
25 parameters.
25 parameters.
26
26
27 :param fromlines: list of lines corresponding to the old content
27 :param fromlines: list of lines corresponding to the old content
28 :param tolines: list of lines corresponding to the new content
28 :param tolines: list of lines corresponding to the new content
29 :param ignore_blank_lines: differences about empty lines only are ignored
29 :param ignore_blank_lines: differences about empty lines only are ignored
30 :param ignore_case: upper case / lower case only differences are ignored
30 :param ignore_case: upper case / lower case only differences are ignored
31 :param ignore_space_changes: differences in amount of spaces are ignored
31 :param ignore_space_changes: differences in amount of spaces are ignored
32 :param context: the number of "equal" lines kept for representing
32 :param context: the number of "equal" lines kept for representing
33 the context of the change
33 the context of the change
34 :return: generator of grouped `difflib.SequenceMatcher` opcodes
34 :return: generator of grouped `difflib.SequenceMatcher` opcodes
35
35
36 If none of the ``ignore_*`` parameters is `True`, there's nothing
36 If none of the ``ignore_*`` parameters is `True`, there's nothing
37 to filter out the results will come straight from the
37 to filter out the results will come straight from the
38 SequenceMatcher.
38 SequenceMatcher.
39 """
39 """
40 hunks = get_hunks(fromlines, tolines, context)
40 hunks = get_hunks(fromlines, tolines, context)
41 if ignore_space_changes or ignore_case or ignore_blank_lines:
41 if ignore_space_changes or ignore_case or ignore_blank_lines:
42 hunks = filter_ignorable_lines(hunks, fromlines, tolines, context,
42 hunks = filter_ignorable_lines(hunks, fromlines, tolines, context,
43 ignore_blank_lines, ignore_case,
43 ignore_blank_lines, ignore_case,
44 ignore_space_changes)
44 ignore_space_changes)
45 return hunks
45 return hunks
46
46
47
47
48 def get_hunks(fromlines, tolines, context=None):
48 def get_hunks(fromlines, tolines, context=None):
49 """Generator yielding grouped opcodes describing differences .
49 """Generator yielding grouped opcodes describing differences .
50
50
51 See `get_filtered_hunks` for the parameter descriptions.
51 See `get_filtered_hunks` for the parameter descriptions.
52 """
52 """
53 matcher = difflib.SequenceMatcher(None, fromlines, tolines)
53 matcher = difflib.SequenceMatcher(None, fromlines, tolines)
54 if context is None:
54 if context is None:
55 return (hunk for hunk in [matcher.get_opcodes()])
55 return (hunk for hunk in [matcher.get_opcodes()])
56 else:
56 else:
57 return matcher.get_grouped_opcodes(context)
57 return matcher.get_grouped_opcodes(context)
58
58
59
59
60 def filter_ignorable_lines(hunks, fromlines, tolines, context,
60 def filter_ignorable_lines(hunks, fromlines, tolines, context,
61 ignore_blank_lines, ignore_case,
61 ignore_blank_lines, ignore_case,
62 ignore_space_changes):
62 ignore_space_changes):
63 """Detect line changes that should be ignored and emits them as
63 """Detect line changes that should be ignored and emits them as
64 tagged as "equal", possibly joined with the preceding and/or
64 tagged as "equal", possibly joined with the preceding and/or
65 following "equal" block.
65 following "equal" block.
66
66
67 See `get_filtered_hunks` for the parameter descriptions.
67 See `get_filtered_hunks` for the parameter descriptions.
68 """
68 """
69 def is_ignorable(tag, fromlines, tolines):
69 def is_ignorable(tag, fromlines, tolines):
70 if tag == 'delete' and ignore_blank_lines:
70 if tag == 'delete' and ignore_blank_lines:
71 if ''.join(fromlines) == '':
71 if ''.join(fromlines) == '':
72 return True
72 return True
73 elif tag == 'insert' and ignore_blank_lines:
73 elif tag == 'insert' and ignore_blank_lines:
74 if ''.join(tolines) == '':
74 if ''.join(tolines) == '':
75 return True
75 return True
76 elif tag == 'replace' and (ignore_case or ignore_space_changes):
76 elif tag == 'replace' and (ignore_case or ignore_space_changes):
77 if len(fromlines) != len(tolines):
77 if len(fromlines) != len(tolines):
78 return False
78 return False
79
79
80 def f(input_str):
80 def f(input_str):
81 if ignore_case:
81 if ignore_case:
82 input_str = input_str.lower()
82 input_str = input_str.lower()
83 if ignore_space_changes:
83 if ignore_space_changes:
84 input_str = ' '.join(input_str.split())
84 input_str = ' '.join(input_str.split())
85 return input_str
85 return input_str
86
86
87 for i in range(len(fromlines)):
87 for i in range(len(fromlines)):
88 if f(fromlines[i]) != f(tolines[i]):
88 if f(fromlines[i]) != f(tolines[i]):
89 return False
89 return False
90 return True
90 return True
91
91
92 hunks = list(hunks)
92 hunks = list(hunks)
93 opcodes = []
93 opcodes = []
94 ignored_lines = False
94 ignored_lines = False
95 prev = None
95 prev = None
96 for hunk in hunks:
96 for hunk in hunks:
97 for tag, i1, i2, j1, j2 in hunk:
97 for tag, i1, i2, j1, j2 in hunk:
98 if tag == 'equal':
98 if tag == 'equal':
99 if prev:
99 if prev:
100 prev = (tag, prev[1], i2, prev[3], j2)
100 prev = (tag, prev[1], i2, prev[3], j2)
101 else:
101 else:
102 prev = (tag, i1, i2, j1, j2)
102 prev = (tag, i1, i2, j1, j2)
103 else:
103 else:
104 if is_ignorable(tag, fromlines[i1:i2], tolines[j1:j2]):
104 if is_ignorable(tag, fromlines[i1:i2], tolines[j1:j2]):
105 ignored_lines = True
105 ignored_lines = True
106 if prev:
106 if prev:
107 prev = 'equal', prev[1], i2, prev[3], j2
107 prev = 'equal', prev[1], i2, prev[3], j2
108 else:
108 else:
109 prev = 'equal', i1, i2, j1, j2
109 prev = 'equal', i1, i2, j1, j2
110 continue
110 continue
111 if prev:
111 if prev:
112 opcodes.append(prev)
112 opcodes.append(prev)
113 opcodes.append((tag, i1, i2, j1, j2))
113 opcodes.append((tag, i1, i2, j1, j2))
114 prev = None
114 prev = None
115 if prev:
115 if prev:
116 opcodes.append(prev)
116 opcodes.append(prev)
117
117
118 if ignored_lines:
118 if ignored_lines:
119 if context is None:
119 if context is None:
120 yield opcodes
120 yield opcodes
121 else:
121 else:
122 # we leave at most n lines with the tag 'equal' before and after
122 # we leave at most n lines with the tag 'equal' before and after
123 # every change
123 # every change
124 n = context
124 n = context
125 nn = n + n
125 nn = n + n
126
126
127 group = []
127 group = []
128 def all_equal():
128 def all_equal():
129 all(op[0] == 'equal' for op in group)
129 all(op[0] == 'equal' for op in group)
130 for idx, (tag, i1, i2, j1, j2) in enumerate(opcodes):
130 for idx, (tag, i1, i2, j1, j2) in enumerate(opcodes):
131 if idx == 0 and tag == 'equal': # Fixup leading unchanged block
131 if idx == 0 and tag == 'equal': # Fixup leading unchanged block
132 i1, j1 = max(i1, i2 - n), max(j1, j2 - n)
132 i1, j1 = max(i1, i2 - n), max(j1, j2 - n)
133 elif tag == 'equal' and i2 - i1 > nn:
133 elif tag == 'equal' and i2 - i1 > nn:
134 group.append((tag, i1, min(i2, i1 + n), j1,
134 group.append((tag, i1, min(i2, i1 + n), j1,
135 min(j2, j1 + n)))
135 min(j2, j1 + n)))
136 if not all_equal():
136 if not all_equal():
137 yield group
137 yield group
138 group = []
138 group = []
139 i1, j1 = max(i1, i2 - n), max(j1, j2 - n)
139 i1, j1 = max(i1, i2 - n), max(j1, j2 - n)
140 group.append((tag, i1, i2, j1, j2))
140 group.append((tag, i1, i2, j1, j2))
141
141
142 if group and not (len(group) == 1 and group[0][0] == 'equal'):
142 if group and not (len(group) == 1 and group[0][0] == 'equal'):
143 if group[-1][0] == 'equal': # Fixup trailing unchanged block
143 if group[-1][0] == 'equal': # Fixup trailing unchanged block
144 tag, i1, i2, j1, j2 = group[-1]
144 tag, i1, i2, j1, j2 = group[-1]
145 group[-1] = tag, i1, min(i2, i1 + n), j1, min(j2, j1 + n)
145 group[-1] = tag, i1, min(i2, i1 + n), j1, min(j2, j1 + n)
146 if not all_equal():
146 if not all_equal():
147 yield group
147 yield group
148 else:
148 else:
149 for hunk in hunks:
149 for hunk in hunks:
150 yield hunk
150 yield hunk
151
151
152
152
153 NO_NEWLINE_AT_END = '\\ No newline at end of file'
153 NO_NEWLINE_AT_END = '\\ No newline at end of file'
154
154
155
155
156 def unified_diff(fromlines, tolines, context=None, ignore_blank_lines=0,
156 def unified_diff(fromlines, tolines, context=None, ignore_blank_lines=0,
157 ignore_case=0, ignore_space_changes=0, lineterm='\n'):
157 ignore_case=0, ignore_space_changes=0, lineterm='\n'):
158 """
158 """
159 Generator producing lines corresponding to a textual diff.
159 Generator producing lines corresponding to a textual diff.
160
160
161 See `get_filtered_hunks` for the parameter descriptions.
161 See `get_filtered_hunks` for the parameter descriptions.
162 """
162 """
163 # TODO: johbo: Check if this can be nicely integrated into the matching
163 # TODO: johbo: Check if this can be nicely integrated into the matching
164
164 if ignore_space_changes:
165 if ignore_space_changes:
165 fromlines = [l.strip() for l in fromlines]
166 fromlines = [l.strip() for l in fromlines]
166 tolines = [l.strip() for l in tolines]
167 tolines = [l.strip() for l in tolines]
167
168
168 for group in get_filtered_hunks(fromlines, tolines, context,
169 for group in get_filtered_hunks(fromlines, tolines, context,
169 ignore_blank_lines, ignore_case,
170 ignore_blank_lines, ignore_case,
170 ignore_space_changes):
171 ignore_space_changes):
171 i1, i2, j1, j2 = group[0][1], group[-1][2], group[0][3], group[-1][4]
172 i1, i2, j1, j2 = group[0][1], group[-1][2], group[0][3], group[-1][4]
172 if i1 == 0 and i2 == 0:
173 if i1 == 0 and i2 == 0:
173 i1, i2 = -1, -1 # support for Add changes
174 i1, i2 = -1, -1 # support for Add changes
174 if j1 == 0 and j2 == 0:
175 if j1 == 0 and j2 == 0:
175 j1, j2 = -1, -1 # support for Delete changes
176 j1, j2 = -1, -1 # support for Delete changes
176 yield '@@ -%s +%s @@%s' % (
177 yield '@@ -%s +%s @@%s' % (
177 _hunk_range(i1 + 1, i2 - i1),
178 _hunk_range(i1 + 1, i2 - i1),
178 _hunk_range(j1 + 1, j2 - j1),
179 _hunk_range(j1 + 1, j2 - j1),
179 lineterm)
180 lineterm)
180 for tag, i1, i2, j1, j2 in group:
181 for tag, i1, i2, j1, j2 in group:
181 if tag == 'equal':
182 if tag == 'equal':
182 for line in fromlines[i1:i2]:
183 for line in fromlines[i1:i2]:
183 if not line.endswith(lineterm):
184 if not line.endswith(lineterm):
184 yield ' ' + line + lineterm
185 yield ' ' + line + lineterm
185 yield NO_NEWLINE_AT_END + lineterm
186 yield NO_NEWLINE_AT_END + lineterm
186 else:
187 else:
187 yield ' ' + line
188 yield ' ' + line
188 else:
189 else:
189 if tag in ('replace', 'delete'):
190 if tag in ('replace', 'delete'):
190 for line in fromlines[i1:i2]:
191 for line in fromlines[i1:i2]:
191 if not line.endswith(lineterm):
192 if not line.endswith(lineterm):
192 yield '-' + line + lineterm
193 yield '-' + line + lineterm
193 yield NO_NEWLINE_AT_END + lineterm
194 yield NO_NEWLINE_AT_END + lineterm
194 else:
195 else:
195 yield '-' + line
196 yield '-' + line
196 if tag in ('replace', 'insert'):
197 if tag in ('replace', 'insert'):
197 for line in tolines[j1:j2]:
198 for line in tolines[j1:j2]:
198 if not line.endswith(lineterm):
199 if not line.endswith(lineterm):
199 yield '+' + line + lineterm
200 yield '+' + line + lineterm
200 yield NO_NEWLINE_AT_END + lineterm
201 yield NO_NEWLINE_AT_END + lineterm
201 else:
202 else:
202 yield '+' + line
203 yield '+' + line
203
204
204
205
205 def _hunk_range(start, length):
206 def _hunk_range(start, length):
206 if length != 1:
207 if length != 1:
207 return '%d,%d' % (start, length)
208 return '%d,%d' % (start, length)
208 else:
209 else:
209 return '%d' % (start, )
210 return '%d' % (start, )
General Comments 0
You need to be logged in to leave comments. Login now