##// END OF EJS Templates
git: use the SSL dir exposed from wire for remote GIT commands.
marcink -
r607:7f39296b default
parent child Browse files
Show More
@@ -1,719 +1,728 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-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2018 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 collections
17 import collections
18 import logging
18 import logging
19 import os
19 import os
20 import posixpath as vcspath
20 import posixpath as vcspath
21 import re
21 import re
22 import stat
22 import stat
23 import traceback
23 import traceback
24 import urllib
24 import urllib
25 import urllib2
25 import urllib2
26 from functools import wraps
26 from functools import wraps
27
27
28 from dulwich import index, objects
28 from dulwich import index, objects
29 from dulwich.client import HttpGitClient, LocalGitClient
29 from dulwich.client import HttpGitClient, LocalGitClient
30 from dulwich.errors import (
30 from dulwich.errors import (
31 NotGitRepository, ChecksumMismatch, WrongObjectException,
31 NotGitRepository, ChecksumMismatch, WrongObjectException,
32 MissingCommitError, ObjectMissing, HangupException,
32 MissingCommitError, ObjectMissing, HangupException,
33 UnexpectedCommandError)
33 UnexpectedCommandError)
34 from dulwich.repo import Repo as DulwichRepo, Tag
34 from dulwich.repo import Repo as DulwichRepo, Tag
35 from dulwich.server import update_server_info
35 from dulwich.server import update_server_info
36
36
37 from vcsserver import exceptions, settings, subprocessio
37 from vcsserver import exceptions, settings, subprocessio
38 from vcsserver.utils import safe_str
38 from vcsserver.utils import safe_str
39 from vcsserver.base import RepoFactory, obfuscate_qs, raise_from_original
39 from vcsserver.base import RepoFactory, obfuscate_qs, raise_from_original
40 from vcsserver.hgcompat import (
40 from vcsserver.hgcompat import (
41 hg_url as url_parser, httpbasicauthhandler, httpdigestauthhandler)
41 hg_url as url_parser, httpbasicauthhandler, httpdigestauthhandler)
42 from vcsserver.git_lfs.lib import LFSOidStore
42 from vcsserver.git_lfs.lib import LFSOidStore
43
43
44 DIR_STAT = stat.S_IFDIR
44 DIR_STAT = stat.S_IFDIR
45 FILE_MODE = stat.S_IFMT
45 FILE_MODE = stat.S_IFMT
46 GIT_LINK = objects.S_IFGITLINK
46 GIT_LINK = objects.S_IFGITLINK
47
47
48 log = logging.getLogger(__name__)
48 log = logging.getLogger(__name__)
49
49
50
50
51 def reraise_safe_exceptions(func):
51 def reraise_safe_exceptions(func):
52 """Converts Dulwich exceptions to something neutral."""
52 """Converts Dulwich exceptions to something neutral."""
53 @wraps(func)
53 @wraps(func)
54 def wrapper(*args, **kwargs):
54 def wrapper(*args, **kwargs):
55 try:
55 try:
56 return func(*args, **kwargs)
56 return func(*args, **kwargs)
57 except (ChecksumMismatch, WrongObjectException, MissingCommitError,
57 except (ChecksumMismatch, WrongObjectException, MissingCommitError,
58 ObjectMissing) as e:
58 ObjectMissing) as e:
59 exc = exceptions.LookupException(e)
59 exc = exceptions.LookupException(e)
60 raise exc(e)
60 raise exc(e)
61 except (HangupException, UnexpectedCommandError) as e:
61 except (HangupException, UnexpectedCommandError) as e:
62 exc = exceptions.VcsException(e)
62 exc = exceptions.VcsException(e)
63 raise exc(e)
63 raise exc(e)
64 except Exception as e:
64 except Exception as e:
65 # NOTE(marcink): becuase of how dulwich handles some exceptions
65 # NOTE(marcink): becuase of how dulwich handles some exceptions
66 # (KeyError on empty repos), we cannot track this and catch all
66 # (KeyError on empty repos), we cannot track this and catch all
67 # exceptions, it's an exceptions from other handlers
67 # exceptions, it's an exceptions from other handlers
68 #if not hasattr(e, '_vcs_kind'):
68 #if not hasattr(e, '_vcs_kind'):
69 #log.exception("Unhandled exception in git remote call")
69 #log.exception("Unhandled exception in git remote call")
70 #raise_from_original(exceptions.UnhandledException)
70 #raise_from_original(exceptions.UnhandledException)
71 raise
71 raise
72 return wrapper
72 return wrapper
73
73
74
74
75 class Repo(DulwichRepo):
75 class Repo(DulwichRepo):
76 """
76 """
77 A wrapper for dulwich Repo class.
77 A wrapper for dulwich Repo class.
78
78
79 Since dulwich is sometimes keeping .idx file descriptors open, it leads to
79 Since dulwich is sometimes keeping .idx file descriptors open, it leads to
80 "Too many open files" error. We need to close all opened file descriptors
80 "Too many open files" error. We need to close all opened file descriptors
81 once the repo object is destroyed.
81 once the repo object is destroyed.
82
82
83 TODO: mikhail: please check if we need this wrapper after updating dulwich
83 TODO: mikhail: please check if we need this wrapper after updating dulwich
84 to 0.12.0 +
84 to 0.12.0 +
85 """
85 """
86 def __del__(self):
86 def __del__(self):
87 if hasattr(self, 'object_store'):
87 if hasattr(self, 'object_store'):
88 self.close()
88 self.close()
89
89
90
90
91 class GitFactory(RepoFactory):
91 class GitFactory(RepoFactory):
92 repo_type = 'git'
92 repo_type = 'git'
93
93
94 def _create_repo(self, wire, create):
94 def _create_repo(self, wire, create):
95 repo_path = str_to_dulwich(wire['path'])
95 repo_path = str_to_dulwich(wire['path'])
96 return Repo(repo_path)
96 return Repo(repo_path)
97
97
98
98
99 class GitRemote(object):
99 class GitRemote(object):
100
100
101 def __init__(self, factory):
101 def __init__(self, factory):
102 self._factory = factory
102 self._factory = factory
103 self.peeled_ref_marker = '^{}'
103 self.peeled_ref_marker = '^{}'
104 self._bulk_methods = {
104 self._bulk_methods = {
105 "author": self.commit_attribute,
105 "author": self.commit_attribute,
106 "date": self.get_object_attrs,
106 "date": self.get_object_attrs,
107 "message": self.commit_attribute,
107 "message": self.commit_attribute,
108 "parents": self.commit_attribute,
108 "parents": self.commit_attribute,
109 "_commit": self.revision,
109 "_commit": self.revision,
110 }
110 }
111
111
112 def _wire_to_config(self, wire):
112 def _wire_to_config(self, wire):
113 if 'config' in wire:
113 if 'config' in wire:
114 return dict([(x[0] + '_' + x[1], x[2]) for x in wire['config']])
114 return dict([(x[0] + '_' + x[1], x[2]) for x in wire['config']])
115 return {}
115 return {}
116
116
117 def _assign_ref(self, wire, ref, commit_id):
117 def _assign_ref(self, wire, ref, commit_id):
118 repo = self._factory.repo(wire)
118 repo = self._factory.repo(wire)
119 repo[ref] = commit_id
119 repo[ref] = commit_id
120
120
121 def _remote_conf(self, config):
122 params = [
123 '-c', 'core.askpass=""',
124 ]
125 ssl_cert_dir = config.get('vcs_ssl_dir')
126 if ssl_cert_dir:
127 params.extend(['-c', 'http.sslCAinfo={}'.format(ssl_cert_dir)])
128 return params
129
121 @reraise_safe_exceptions
130 @reraise_safe_exceptions
122 def add_object(self, wire, content):
131 def add_object(self, wire, content):
123 repo = self._factory.repo(wire)
132 repo = self._factory.repo(wire)
124 blob = objects.Blob()
133 blob = objects.Blob()
125 blob.set_raw_string(content)
134 blob.set_raw_string(content)
126 repo.object_store.add_object(blob)
135 repo.object_store.add_object(blob)
127 return blob.id
136 return blob.id
128
137
129 @reraise_safe_exceptions
138 @reraise_safe_exceptions
130 def assert_correct_path(self, wire):
139 def assert_correct_path(self, wire):
131 path = wire.get('path')
140 path = wire.get('path')
132 try:
141 try:
133 self._factory.repo(wire)
142 self._factory.repo(wire)
134 except NotGitRepository as e:
143 except NotGitRepository as e:
135 tb = traceback.format_exc()
144 tb = traceback.format_exc()
136 log.debug("Invalid Git path `%s`, tb: %s", path, tb)
145 log.debug("Invalid Git path `%s`, tb: %s", path, tb)
137 return False
146 return False
138
147
139 return True
148 return True
140
149
141 @reraise_safe_exceptions
150 @reraise_safe_exceptions
142 def bare(self, wire):
151 def bare(self, wire):
143 repo = self._factory.repo(wire)
152 repo = self._factory.repo(wire)
144 return repo.bare
153 return repo.bare
145
154
146 @reraise_safe_exceptions
155 @reraise_safe_exceptions
147 def blob_as_pretty_string(self, wire, sha):
156 def blob_as_pretty_string(self, wire, sha):
148 repo = self._factory.repo(wire)
157 repo = self._factory.repo(wire)
149 return repo[sha].as_pretty_string()
158 return repo[sha].as_pretty_string()
150
159
151 @reraise_safe_exceptions
160 @reraise_safe_exceptions
152 def blob_raw_length(self, wire, sha):
161 def blob_raw_length(self, wire, sha):
153 repo = self._factory.repo(wire)
162 repo = self._factory.repo(wire)
154 blob = repo[sha]
163 blob = repo[sha]
155 return blob.raw_length()
164 return blob.raw_length()
156
165
157 def _parse_lfs_pointer(self, raw_content):
166 def _parse_lfs_pointer(self, raw_content):
158
167
159 spec_string = 'version https://git-lfs.github.com/spec'
168 spec_string = 'version https://git-lfs.github.com/spec'
160 if raw_content and raw_content.startswith(spec_string):
169 if raw_content and raw_content.startswith(spec_string):
161 pattern = re.compile(r"""
170 pattern = re.compile(r"""
162 (?:\n)?
171 (?:\n)?
163 ^version[ ]https://git-lfs\.github\.com/spec/(?P<spec_ver>v\d+)\n
172 ^version[ ]https://git-lfs\.github\.com/spec/(?P<spec_ver>v\d+)\n
164 ^oid[ ] sha256:(?P<oid_hash>[0-9a-f]{64})\n
173 ^oid[ ] sha256:(?P<oid_hash>[0-9a-f]{64})\n
165 ^size[ ](?P<oid_size>[0-9]+)\n
174 ^size[ ](?P<oid_size>[0-9]+)\n
166 (?:\n)?
175 (?:\n)?
167 """, re.VERBOSE | re.MULTILINE)
176 """, re.VERBOSE | re.MULTILINE)
168 match = pattern.match(raw_content)
177 match = pattern.match(raw_content)
169 if match:
178 if match:
170 return match.groupdict()
179 return match.groupdict()
171
180
172 return {}
181 return {}
173
182
174 @reraise_safe_exceptions
183 @reraise_safe_exceptions
175 def is_large_file(self, wire, sha):
184 def is_large_file(self, wire, sha):
176 repo = self._factory.repo(wire)
185 repo = self._factory.repo(wire)
177 blob = repo[sha]
186 blob = repo[sha]
178 return self._parse_lfs_pointer(blob.as_raw_string())
187 return self._parse_lfs_pointer(blob.as_raw_string())
179
188
180 @reraise_safe_exceptions
189 @reraise_safe_exceptions
181 def in_largefiles_store(self, wire, oid):
190 def in_largefiles_store(self, wire, oid):
182 repo = self._factory.repo(wire)
191 repo = self._factory.repo(wire)
183 conf = self._wire_to_config(wire)
192 conf = self._wire_to_config(wire)
184
193
185 store_location = conf.get('vcs_git_lfs_store_location')
194 store_location = conf.get('vcs_git_lfs_store_location')
186 if store_location:
195 if store_location:
187 repo_name = repo.path
196 repo_name = repo.path
188 store = LFSOidStore(
197 store = LFSOidStore(
189 oid=oid, repo=repo_name, store_location=store_location)
198 oid=oid, repo=repo_name, store_location=store_location)
190 return store.has_oid()
199 return store.has_oid()
191
200
192 return False
201 return False
193
202
194 @reraise_safe_exceptions
203 @reraise_safe_exceptions
195 def store_path(self, wire, oid):
204 def store_path(self, wire, oid):
196 repo = self._factory.repo(wire)
205 repo = self._factory.repo(wire)
197 conf = self._wire_to_config(wire)
206 conf = self._wire_to_config(wire)
198
207
199 store_location = conf.get('vcs_git_lfs_store_location')
208 store_location = conf.get('vcs_git_lfs_store_location')
200 if store_location:
209 if store_location:
201 repo_name = repo.path
210 repo_name = repo.path
202 store = LFSOidStore(
211 store = LFSOidStore(
203 oid=oid, repo=repo_name, store_location=store_location)
212 oid=oid, repo=repo_name, store_location=store_location)
204 return store.oid_path
213 return store.oid_path
205 raise ValueError('Unable to fetch oid with path {}'.format(oid))
214 raise ValueError('Unable to fetch oid with path {}'.format(oid))
206
215
207 @reraise_safe_exceptions
216 @reraise_safe_exceptions
208 def bulk_request(self, wire, rev, pre_load):
217 def bulk_request(self, wire, rev, pre_load):
209 result = {}
218 result = {}
210 for attr in pre_load:
219 for attr in pre_load:
211 try:
220 try:
212 method = self._bulk_methods[attr]
221 method = self._bulk_methods[attr]
213 args = [wire, rev]
222 args = [wire, rev]
214 if attr == "date":
223 if attr == "date":
215 args.extend(["commit_time", "commit_timezone"])
224 args.extend(["commit_time", "commit_timezone"])
216 elif attr in ["author", "message", "parents"]:
225 elif attr in ["author", "message", "parents"]:
217 args.append(attr)
226 args.append(attr)
218 result[attr] = method(*args)
227 result[attr] = method(*args)
219 except KeyError as e:
228 except KeyError as e:
220 raise exceptions.VcsException(e)(
229 raise exceptions.VcsException(e)(
221 "Unknown bulk attribute: %s" % attr)
230 "Unknown bulk attribute: %s" % attr)
222 return result
231 return result
223
232
224 def _build_opener(self, url):
233 def _build_opener(self, url):
225 handlers = []
234 handlers = []
226 url_obj = url_parser(url)
235 url_obj = url_parser(url)
227 _, authinfo = url_obj.authinfo()
236 _, authinfo = url_obj.authinfo()
228
237
229 if authinfo:
238 if authinfo:
230 # create a password manager
239 # create a password manager
231 passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
240 passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
232 passmgr.add_password(*authinfo)
241 passmgr.add_password(*authinfo)
233
242
234 handlers.extend((httpbasicauthhandler(passmgr),
243 handlers.extend((httpbasicauthhandler(passmgr),
235 httpdigestauthhandler(passmgr)))
244 httpdigestauthhandler(passmgr)))
236
245
237 return urllib2.build_opener(*handlers)
246 return urllib2.build_opener(*handlers)
238
247
239 @reraise_safe_exceptions
248 @reraise_safe_exceptions
240 def check_url(self, url, config):
249 def check_url(self, url, config):
241 url_obj = url_parser(url)
250 url_obj = url_parser(url)
242 test_uri, _ = url_obj.authinfo()
251 test_uri, _ = url_obj.authinfo()
243 url_obj.passwd = '*****' if url_obj.passwd else url_obj.passwd
252 url_obj.passwd = '*****' if url_obj.passwd else url_obj.passwd
244 url_obj.query = obfuscate_qs(url_obj.query)
253 url_obj.query = obfuscate_qs(url_obj.query)
245 cleaned_uri = str(url_obj)
254 cleaned_uri = str(url_obj)
246 log.info("Checking URL for remote cloning/import: %s", cleaned_uri)
255 log.info("Checking URL for remote cloning/import: %s", cleaned_uri)
247
256
248 if not test_uri.endswith('info/refs'):
257 if not test_uri.endswith('info/refs'):
249 test_uri = test_uri.rstrip('/') + '/info/refs'
258 test_uri = test_uri.rstrip('/') + '/info/refs'
250
259
251 o = self._build_opener(url)
260 o = self._build_opener(url)
252 o.addheaders = [('User-Agent', 'git/1.7.8.0')] # fake some git
261 o.addheaders = [('User-Agent', 'git/1.7.8.0')] # fake some git
253
262
254 q = {"service": 'git-upload-pack'}
263 q = {"service": 'git-upload-pack'}
255 qs = '?%s' % urllib.urlencode(q)
264 qs = '?%s' % urllib.urlencode(q)
256 cu = "%s%s" % (test_uri, qs)
265 cu = "%s%s" % (test_uri, qs)
257 req = urllib2.Request(cu, None, {})
266 req = urllib2.Request(cu, None, {})
258
267
259 try:
268 try:
260 log.debug("Trying to open URL %s", cleaned_uri)
269 log.debug("Trying to open URL %s", cleaned_uri)
261 resp = o.open(req)
270 resp = o.open(req)
262 if resp.code != 200:
271 if resp.code != 200:
263 raise exceptions.URLError()('Return Code is not 200')
272 raise exceptions.URLError()('Return Code is not 200')
264 except Exception as e:
273 except Exception as e:
265 log.warning("URL cannot be opened: %s", cleaned_uri, exc_info=True)
274 log.warning("URL cannot be opened: %s", cleaned_uri, exc_info=True)
266 # means it cannot be cloned
275 # means it cannot be cloned
267 raise exceptions.URLError(e)("[%s] org_exc: %s" % (cleaned_uri, e))
276 raise exceptions.URLError(e)("[%s] org_exc: %s" % (cleaned_uri, e))
268
277
269 # now detect if it's proper git repo
278 # now detect if it's proper git repo
270 gitdata = resp.read()
279 gitdata = resp.read()
271 if 'service=git-upload-pack' in gitdata:
280 if 'service=git-upload-pack' in gitdata:
272 pass
281 pass
273 elif re.findall(r'[0-9a-fA-F]{40}\s+refs', gitdata):
282 elif re.findall(r'[0-9a-fA-F]{40}\s+refs', gitdata):
274 # old style git can return some other format !
283 # old style git can return some other format !
275 pass
284 pass
276 else:
285 else:
277 raise exceptions.URLError()(
286 raise exceptions.URLError()(
278 "url [%s] does not look like an git" % (cleaned_uri,))
287 "url [%s] does not look like an git" % (cleaned_uri,))
279
288
280 return True
289 return True
281
290
282 @reraise_safe_exceptions
291 @reraise_safe_exceptions
283 def clone(self, wire, url, deferred, valid_refs, update_after_clone):
292 def clone(self, wire, url, deferred, valid_refs, update_after_clone):
284 # TODO(marcink): deprecate this method. Last i checked we don't use it anymore
293 # TODO(marcink): deprecate this method. Last i checked we don't use it anymore
285 remote_refs = self.pull(wire, url, apply_refs=False)
294 remote_refs = self.pull(wire, url, apply_refs=False)
286 repo = self._factory.repo(wire)
295 repo = self._factory.repo(wire)
287 if isinstance(valid_refs, list):
296 if isinstance(valid_refs, list):
288 valid_refs = tuple(valid_refs)
297 valid_refs = tuple(valid_refs)
289
298
290 for k in remote_refs:
299 for k in remote_refs:
291 # only parse heads/tags and skip so called deferred tags
300 # only parse heads/tags and skip so called deferred tags
292 if k.startswith(valid_refs) and not k.endswith(deferred):
301 if k.startswith(valid_refs) and not k.endswith(deferred):
293 repo[k] = remote_refs[k]
302 repo[k] = remote_refs[k]
294
303
295 if update_after_clone:
304 if update_after_clone:
296 # we want to checkout HEAD
305 # we want to checkout HEAD
297 repo["HEAD"] = remote_refs["HEAD"]
306 repo["HEAD"] = remote_refs["HEAD"]
298 index.build_index_from_tree(repo.path, repo.index_path(),
307 index.build_index_from_tree(repo.path, repo.index_path(),
299 repo.object_store, repo["HEAD"].tree)
308 repo.object_store, repo["HEAD"].tree)
300
309
301 # TODO: this is quite complex, check if that can be simplified
310 # TODO: this is quite complex, check if that can be simplified
302 @reraise_safe_exceptions
311 @reraise_safe_exceptions
303 def commit(self, wire, commit_data, branch, commit_tree, updated, removed):
312 def commit(self, wire, commit_data, branch, commit_tree, updated, removed):
304 repo = self._factory.repo(wire)
313 repo = self._factory.repo(wire)
305 object_store = repo.object_store
314 object_store = repo.object_store
306
315
307 # Create tree and populates it with blobs
316 # Create tree and populates it with blobs
308 commit_tree = commit_tree and repo[commit_tree] or objects.Tree()
317 commit_tree = commit_tree and repo[commit_tree] or objects.Tree()
309
318
310 for node in updated:
319 for node in updated:
311 # Compute subdirs if needed
320 # Compute subdirs if needed
312 dirpath, nodename = vcspath.split(node['path'])
321 dirpath, nodename = vcspath.split(node['path'])
313 dirnames = map(safe_str, dirpath and dirpath.split('/') or [])
322 dirnames = map(safe_str, dirpath and dirpath.split('/') or [])
314 parent = commit_tree
323 parent = commit_tree
315 ancestors = [('', parent)]
324 ancestors = [('', parent)]
316
325
317 # Tries to dig for the deepest existing tree
326 # Tries to dig for the deepest existing tree
318 while dirnames:
327 while dirnames:
319 curdir = dirnames.pop(0)
328 curdir = dirnames.pop(0)
320 try:
329 try:
321 dir_id = parent[curdir][1]
330 dir_id = parent[curdir][1]
322 except KeyError:
331 except KeyError:
323 # put curdir back into dirnames and stops
332 # put curdir back into dirnames and stops
324 dirnames.insert(0, curdir)
333 dirnames.insert(0, curdir)
325 break
334 break
326 else:
335 else:
327 # If found, updates parent
336 # If found, updates parent
328 parent = repo[dir_id]
337 parent = repo[dir_id]
329 ancestors.append((curdir, parent))
338 ancestors.append((curdir, parent))
330 # Now parent is deepest existing tree and we need to create
339 # Now parent is deepest existing tree and we need to create
331 # subtrees for dirnames (in reverse order)
340 # subtrees for dirnames (in reverse order)
332 # [this only applies for nodes from added]
341 # [this only applies for nodes from added]
333 new_trees = []
342 new_trees = []
334
343
335 blob = objects.Blob.from_string(node['content'])
344 blob = objects.Blob.from_string(node['content'])
336
345
337 if dirnames:
346 if dirnames:
338 # If there are trees which should be created we need to build
347 # If there are trees which should be created we need to build
339 # them now (in reverse order)
348 # them now (in reverse order)
340 reversed_dirnames = list(reversed(dirnames))
349 reversed_dirnames = list(reversed(dirnames))
341 curtree = objects.Tree()
350 curtree = objects.Tree()
342 curtree[node['node_path']] = node['mode'], blob.id
351 curtree[node['node_path']] = node['mode'], blob.id
343 new_trees.append(curtree)
352 new_trees.append(curtree)
344 for dirname in reversed_dirnames[:-1]:
353 for dirname in reversed_dirnames[:-1]:
345 newtree = objects.Tree()
354 newtree = objects.Tree()
346 newtree[dirname] = (DIR_STAT, curtree.id)
355 newtree[dirname] = (DIR_STAT, curtree.id)
347 new_trees.append(newtree)
356 new_trees.append(newtree)
348 curtree = newtree
357 curtree = newtree
349 parent[reversed_dirnames[-1]] = (DIR_STAT, curtree.id)
358 parent[reversed_dirnames[-1]] = (DIR_STAT, curtree.id)
350 else:
359 else:
351 parent.add(
360 parent.add(
352 name=node['node_path'], mode=node['mode'], hexsha=blob.id)
361 name=node['node_path'], mode=node['mode'], hexsha=blob.id)
353
362
354 new_trees.append(parent)
363 new_trees.append(parent)
355 # Update ancestors
364 # Update ancestors
356 reversed_ancestors = reversed(
365 reversed_ancestors = reversed(
357 [(a[1], b[1], b[0]) for a, b in zip(ancestors, ancestors[1:])])
366 [(a[1], b[1], b[0]) for a, b in zip(ancestors, ancestors[1:])])
358 for parent, tree, path in reversed_ancestors:
367 for parent, tree, path in reversed_ancestors:
359 parent[path] = (DIR_STAT, tree.id)
368 parent[path] = (DIR_STAT, tree.id)
360 object_store.add_object(tree)
369 object_store.add_object(tree)
361
370
362 object_store.add_object(blob)
371 object_store.add_object(blob)
363 for tree in new_trees:
372 for tree in new_trees:
364 object_store.add_object(tree)
373 object_store.add_object(tree)
365
374
366 for node_path in removed:
375 for node_path in removed:
367 paths = node_path.split('/')
376 paths = node_path.split('/')
368 tree = commit_tree
377 tree = commit_tree
369 trees = [tree]
378 trees = [tree]
370 # Traverse deep into the forest...
379 # Traverse deep into the forest...
371 for path in paths:
380 for path in paths:
372 try:
381 try:
373 obj = repo[tree[path][1]]
382 obj = repo[tree[path][1]]
374 if isinstance(obj, objects.Tree):
383 if isinstance(obj, objects.Tree):
375 trees.append(obj)
384 trees.append(obj)
376 tree = obj
385 tree = obj
377 except KeyError:
386 except KeyError:
378 break
387 break
379 # Cut down the blob and all rotten trees on the way back...
388 # Cut down the blob and all rotten trees on the way back...
380 for path, tree in reversed(zip(paths, trees)):
389 for path, tree in reversed(zip(paths, trees)):
381 del tree[path]
390 del tree[path]
382 if tree:
391 if tree:
383 # This tree still has elements - don't remove it or any
392 # This tree still has elements - don't remove it or any
384 # of it's parents
393 # of it's parents
385 break
394 break
386
395
387 object_store.add_object(commit_tree)
396 object_store.add_object(commit_tree)
388
397
389 # Create commit
398 # Create commit
390 commit = objects.Commit()
399 commit = objects.Commit()
391 commit.tree = commit_tree.id
400 commit.tree = commit_tree.id
392 for k, v in commit_data.iteritems():
401 for k, v in commit_data.iteritems():
393 setattr(commit, k, v)
402 setattr(commit, k, v)
394 object_store.add_object(commit)
403 object_store.add_object(commit)
395
404
396 ref = 'refs/heads/%s' % branch
405 ref = 'refs/heads/%s' % branch
397 repo.refs[ref] = commit.id
406 repo.refs[ref] = commit.id
398
407
399 return commit.id
408 return commit.id
400
409
401 @reraise_safe_exceptions
410 @reraise_safe_exceptions
402 def pull(self, wire, url, apply_refs=True, refs=None, update_after=False):
411 def pull(self, wire, url, apply_refs=True, refs=None, update_after=False):
403 if url != 'default' and '://' not in url:
412 if url != 'default' and '://' not in url:
404 client = LocalGitClient(url)
413 client = LocalGitClient(url)
405 else:
414 else:
406 url_obj = url_parser(url)
415 url_obj = url_parser(url)
407 o = self._build_opener(url)
416 o = self._build_opener(url)
408 url, _ = url_obj.authinfo()
417 url, _ = url_obj.authinfo()
409 client = HttpGitClient(base_url=url, opener=o)
418 client = HttpGitClient(base_url=url, opener=o)
410 repo = self._factory.repo(wire)
419 repo = self._factory.repo(wire)
411
420
412 determine_wants = repo.object_store.determine_wants_all
421 determine_wants = repo.object_store.determine_wants_all
413 if refs:
422 if refs:
414 def determine_wants_requested(references):
423 def determine_wants_requested(references):
415 return [references[r] for r in references if r in refs]
424 return [references[r] for r in references if r in refs]
416 determine_wants = determine_wants_requested
425 determine_wants = determine_wants_requested
417
426
418 try:
427 try:
419 remote_refs = client.fetch(
428 remote_refs = client.fetch(
420 path=url, target=repo, determine_wants=determine_wants)
429 path=url, target=repo, determine_wants=determine_wants)
421 except NotGitRepository as e:
430 except NotGitRepository as e:
422 log.warning(
431 log.warning(
423 'Trying to fetch from "%s" failed, not a Git repository.', url)
432 'Trying to fetch from "%s" failed, not a Git repository.', url)
424 # Exception can contain unicode which we convert
433 # Exception can contain unicode which we convert
425 raise exceptions.AbortException(e)(repr(e))
434 raise exceptions.AbortException(e)(repr(e))
426
435
427 # mikhail: client.fetch() returns all the remote refs, but fetches only
436 # mikhail: client.fetch() returns all the remote refs, but fetches only
428 # refs filtered by `determine_wants` function. We need to filter result
437 # refs filtered by `determine_wants` function. We need to filter result
429 # as well
438 # as well
430 if refs:
439 if refs:
431 remote_refs = {k: remote_refs[k] for k in remote_refs if k in refs}
440 remote_refs = {k: remote_refs[k] for k in remote_refs if k in refs}
432
441
433 if apply_refs:
442 if apply_refs:
434 # TODO: johbo: Needs proper test coverage with a git repository
443 # TODO: johbo: Needs proper test coverage with a git repository
435 # that contains a tag object, so that we would end up with
444 # that contains a tag object, so that we would end up with
436 # a peeled ref at this point.
445 # a peeled ref at this point.
437 for k in remote_refs:
446 for k in remote_refs:
438 if k.endswith(self.peeled_ref_marker):
447 if k.endswith(self.peeled_ref_marker):
439 log.debug("Skipping peeled reference %s", k)
448 log.debug("Skipping peeled reference %s", k)
440 continue
449 continue
441 repo[k] = remote_refs[k]
450 repo[k] = remote_refs[k]
442
451
443 if refs and not update_after:
452 if refs and not update_after:
444 # mikhail: explicitly set the head to the last ref.
453 # mikhail: explicitly set the head to the last ref.
445 repo['HEAD'] = remote_refs[refs[-1]]
454 repo['HEAD'] = remote_refs[refs[-1]]
446
455
447 if update_after:
456 if update_after:
448 # we want to checkout HEAD
457 # we want to checkout HEAD
449 repo["HEAD"] = remote_refs["HEAD"]
458 repo["HEAD"] = remote_refs["HEAD"]
450 index.build_index_from_tree(repo.path, repo.index_path(),
459 index.build_index_from_tree(repo.path, repo.index_path(),
451 repo.object_store, repo["HEAD"].tree)
460 repo.object_store, repo["HEAD"].tree)
452 return remote_refs
461 return remote_refs
453
462
454 @reraise_safe_exceptions
463 @reraise_safe_exceptions
455 def sync_fetch(self, wire, url, refs=None):
464 def sync_fetch(self, wire, url, refs=None):
456 repo = self._factory.repo(wire)
465 repo = self._factory.repo(wire)
457 if refs and not isinstance(refs, (list, tuple)):
466 if refs and not isinstance(refs, (list, tuple)):
458 refs = [refs]
467 refs = [refs]
459
468 config = self._wire_to_config(wire)
460 # get all remote refs we'll use to fetch later
469 # get all remote refs we'll use to fetch later
461 output, __ = self.run_git_command(
470 output, __ = self.run_git_command(
462 wire, ['ls-remote', url], fail_on_stderr=False,
471 wire, ['ls-remote', url], fail_on_stderr=False,
463 _copts=['-c', 'core.askpass=""'],
472 _copts=self._remote_conf(config),
464 extra_env={'GIT_TERMINAL_PROMPT': '0'})
473 extra_env={'GIT_TERMINAL_PROMPT': '0'})
465
474
466 remote_refs = collections.OrderedDict()
475 remote_refs = collections.OrderedDict()
467 fetch_refs = []
476 fetch_refs = []
468
477
469 for ref_line in output.splitlines():
478 for ref_line in output.splitlines():
470 sha, ref = ref_line.split('\t')
479 sha, ref = ref_line.split('\t')
471 sha = sha.strip()
480 sha = sha.strip()
472 if ref in remote_refs:
481 if ref in remote_refs:
473 # duplicate, skip
482 # duplicate, skip
474 continue
483 continue
475 if ref.endswith(self.peeled_ref_marker):
484 if ref.endswith(self.peeled_ref_marker):
476 log.debug("Skipping peeled reference %s", ref)
485 log.debug("Skipping peeled reference %s", ref)
477 continue
486 continue
478 # don't sync HEAD
487 # don't sync HEAD
479 if ref in ['HEAD']:
488 if ref in ['HEAD']:
480 continue
489 continue
481
490
482 remote_refs[ref] = sha
491 remote_refs[ref] = sha
483
492
484 if refs and sha in refs:
493 if refs and sha in refs:
485 # we filter fetch using our specified refs
494 # we filter fetch using our specified refs
486 fetch_refs.append('{}:{}'.format(ref, ref))
495 fetch_refs.append('{}:{}'.format(ref, ref))
487 elif not refs:
496 elif not refs:
488 fetch_refs.append('{}:{}'.format(ref, ref))
497 fetch_refs.append('{}:{}'.format(ref, ref))
489
498
490 if fetch_refs:
499 if fetch_refs:
491 _out, _err = self.run_git_command(
500 _out, _err = self.run_git_command(
492 wire, ['fetch', url, '--force', '--prune', '--'] + fetch_refs,
501 wire, ['fetch', url, '--force', '--prune', '--'] + fetch_refs,
493 fail_on_stderr=False,
502 fail_on_stderr=False,
494 _copts=['-c', 'core.askpass=""'],
503 _copts=self._remote_conf(config),
495 extra_env={'GIT_TERMINAL_PROMPT': '0'})
504 extra_env={'GIT_TERMINAL_PROMPT': '0'})
496
505
497 return remote_refs
506 return remote_refs
498
507
499 @reraise_safe_exceptions
508 @reraise_safe_exceptions
500 def sync_push(self, wire, url, refs=None):
509 def sync_push(self, wire, url, refs=None):
501 if not self.check_url(url, wire):
510 if not self.check_url(url, wire):
502 return
511 return
503
512 config = self._wire_to_config(wire)
504 repo = self._factory.repo(wire)
513 repo = self._factory.repo(wire)
505 self.run_git_command(
514 self.run_git_command(
506 wire, ['push', url, '--mirror'], fail_on_stderr=False,
515 wire, ['push', url, '--mirror'], fail_on_stderr=False,
507 _copts=['-c', 'core.askpass=""'],
516 _copts=self._remote_conf(config),
508 extra_env={'GIT_TERMINAL_PROMPT': '0'})
517 extra_env={'GIT_TERMINAL_PROMPT': '0'})
509
518
510 @reraise_safe_exceptions
519 @reraise_safe_exceptions
511 def get_remote_refs(self, wire, url):
520 def get_remote_refs(self, wire, url):
512 repo = Repo(url)
521 repo = Repo(url)
513 return repo.get_refs()
522 return repo.get_refs()
514
523
515 @reraise_safe_exceptions
524 @reraise_safe_exceptions
516 def get_description(self, wire):
525 def get_description(self, wire):
517 repo = self._factory.repo(wire)
526 repo = self._factory.repo(wire)
518 return repo.get_description()
527 return repo.get_description()
519
528
520 @reraise_safe_exceptions
529 @reraise_safe_exceptions
521 def get_missing_revs(self, wire, rev1, rev2, path2):
530 def get_missing_revs(self, wire, rev1, rev2, path2):
522 repo = self._factory.repo(wire)
531 repo = self._factory.repo(wire)
523 LocalGitClient(thin_packs=False).fetch(path2, repo)
532 LocalGitClient(thin_packs=False).fetch(path2, repo)
524
533
525 wire_remote = wire.copy()
534 wire_remote = wire.copy()
526 wire_remote['path'] = path2
535 wire_remote['path'] = path2
527 repo_remote = self._factory.repo(wire_remote)
536 repo_remote = self._factory.repo(wire_remote)
528 LocalGitClient(thin_packs=False).fetch(wire["path"], repo_remote)
537 LocalGitClient(thin_packs=False).fetch(wire["path"], repo_remote)
529
538
530 revs = [
539 revs = [
531 x.commit.id
540 x.commit.id
532 for x in repo_remote.get_walker(include=[rev2], exclude=[rev1])]
541 for x in repo_remote.get_walker(include=[rev2], exclude=[rev1])]
533 return revs
542 return revs
534
543
535 @reraise_safe_exceptions
544 @reraise_safe_exceptions
536 def get_object(self, wire, sha):
545 def get_object(self, wire, sha):
537 repo = self._factory.repo(wire)
546 repo = self._factory.repo(wire)
538 obj = repo.get_object(sha)
547 obj = repo.get_object(sha)
539 commit_id = obj.id
548 commit_id = obj.id
540
549
541 if isinstance(obj, Tag):
550 if isinstance(obj, Tag):
542 commit_id = obj.object[1]
551 commit_id = obj.object[1]
543
552
544 return {
553 return {
545 'id': obj.id,
554 'id': obj.id,
546 'type': obj.type_name,
555 'type': obj.type_name,
547 'commit_id': commit_id
556 'commit_id': commit_id
548 }
557 }
549
558
550 @reraise_safe_exceptions
559 @reraise_safe_exceptions
551 def get_object_attrs(self, wire, sha, *attrs):
560 def get_object_attrs(self, wire, sha, *attrs):
552 repo = self._factory.repo(wire)
561 repo = self._factory.repo(wire)
553 obj = repo.get_object(sha)
562 obj = repo.get_object(sha)
554 return list(getattr(obj, a) for a in attrs)
563 return list(getattr(obj, a) for a in attrs)
555
564
556 @reraise_safe_exceptions
565 @reraise_safe_exceptions
557 def get_refs(self, wire):
566 def get_refs(self, wire):
558 repo = self._factory.repo(wire)
567 repo = self._factory.repo(wire)
559 result = {}
568 result = {}
560 for ref, sha in repo.refs.as_dict().items():
569 for ref, sha in repo.refs.as_dict().items():
561 peeled_sha = repo.get_peeled(ref)
570 peeled_sha = repo.get_peeled(ref)
562 result[ref] = peeled_sha
571 result[ref] = peeled_sha
563 return result
572 return result
564
573
565 @reraise_safe_exceptions
574 @reraise_safe_exceptions
566 def get_refs_path(self, wire):
575 def get_refs_path(self, wire):
567 repo = self._factory.repo(wire)
576 repo = self._factory.repo(wire)
568 return repo.refs.path
577 return repo.refs.path
569
578
570 @reraise_safe_exceptions
579 @reraise_safe_exceptions
571 def head(self, wire, show_exc=True):
580 def head(self, wire, show_exc=True):
572 repo = self._factory.repo(wire)
581 repo = self._factory.repo(wire)
573 try:
582 try:
574 return repo.head()
583 return repo.head()
575 except Exception:
584 except Exception:
576 if show_exc:
585 if show_exc:
577 raise
586 raise
578
587
579 @reraise_safe_exceptions
588 @reraise_safe_exceptions
580 def init(self, wire):
589 def init(self, wire):
581 repo_path = str_to_dulwich(wire['path'])
590 repo_path = str_to_dulwich(wire['path'])
582 self.repo = Repo.init(repo_path)
591 self.repo = Repo.init(repo_path)
583
592
584 @reraise_safe_exceptions
593 @reraise_safe_exceptions
585 def init_bare(self, wire):
594 def init_bare(self, wire):
586 repo_path = str_to_dulwich(wire['path'])
595 repo_path = str_to_dulwich(wire['path'])
587 self.repo = Repo.init_bare(repo_path)
596 self.repo = Repo.init_bare(repo_path)
588
597
589 @reraise_safe_exceptions
598 @reraise_safe_exceptions
590 def revision(self, wire, rev):
599 def revision(self, wire, rev):
591 repo = self._factory.repo(wire)
600 repo = self._factory.repo(wire)
592 obj = repo[rev]
601 obj = repo[rev]
593 obj_data = {
602 obj_data = {
594 'id': obj.id,
603 'id': obj.id,
595 }
604 }
596 try:
605 try:
597 obj_data['tree'] = obj.tree
606 obj_data['tree'] = obj.tree
598 except AttributeError:
607 except AttributeError:
599 pass
608 pass
600 return obj_data
609 return obj_data
601
610
602 @reraise_safe_exceptions
611 @reraise_safe_exceptions
603 def commit_attribute(self, wire, rev, attr):
612 def commit_attribute(self, wire, rev, attr):
604 repo = self._factory.repo(wire)
613 repo = self._factory.repo(wire)
605 obj = repo[rev]
614 obj = repo[rev]
606 return getattr(obj, attr)
615 return getattr(obj, attr)
607
616
608 @reraise_safe_exceptions
617 @reraise_safe_exceptions
609 def set_refs(self, wire, key, value):
618 def set_refs(self, wire, key, value):
610 repo = self._factory.repo(wire)
619 repo = self._factory.repo(wire)
611 repo.refs[key] = value
620 repo.refs[key] = value
612
621
613 @reraise_safe_exceptions
622 @reraise_safe_exceptions
614 def remove_ref(self, wire, key):
623 def remove_ref(self, wire, key):
615 repo = self._factory.repo(wire)
624 repo = self._factory.repo(wire)
616 del repo.refs[key]
625 del repo.refs[key]
617
626
618 @reraise_safe_exceptions
627 @reraise_safe_exceptions
619 def tree_changes(self, wire, source_id, target_id):
628 def tree_changes(self, wire, source_id, target_id):
620 repo = self._factory.repo(wire)
629 repo = self._factory.repo(wire)
621 source = repo[source_id].tree if source_id else None
630 source = repo[source_id].tree if source_id else None
622 target = repo[target_id].tree
631 target = repo[target_id].tree
623 result = repo.object_store.tree_changes(source, target)
632 result = repo.object_store.tree_changes(source, target)
624 return list(result)
633 return list(result)
625
634
626 @reraise_safe_exceptions
635 @reraise_safe_exceptions
627 def tree_items(self, wire, tree_id):
636 def tree_items(self, wire, tree_id):
628 repo = self._factory.repo(wire)
637 repo = self._factory.repo(wire)
629 tree = repo[tree_id]
638 tree = repo[tree_id]
630
639
631 result = []
640 result = []
632 for item in tree.iteritems():
641 for item in tree.iteritems():
633 item_sha = item.sha
642 item_sha = item.sha
634 item_mode = item.mode
643 item_mode = item.mode
635
644
636 if FILE_MODE(item_mode) == GIT_LINK:
645 if FILE_MODE(item_mode) == GIT_LINK:
637 item_type = "link"
646 item_type = "link"
638 else:
647 else:
639 item_type = repo[item_sha].type_name
648 item_type = repo[item_sha].type_name
640
649
641 result.append((item.path, item_mode, item_sha, item_type))
650 result.append((item.path, item_mode, item_sha, item_type))
642 return result
651 return result
643
652
644 @reraise_safe_exceptions
653 @reraise_safe_exceptions
645 def update_server_info(self, wire):
654 def update_server_info(self, wire):
646 repo = self._factory.repo(wire)
655 repo = self._factory.repo(wire)
647 update_server_info(repo)
656 update_server_info(repo)
648
657
649 @reraise_safe_exceptions
658 @reraise_safe_exceptions
650 def discover_git_version(self):
659 def discover_git_version(self):
651 stdout, _ = self.run_git_command(
660 stdout, _ = self.run_git_command(
652 {}, ['--version'], _bare=True, _safe=True)
661 {}, ['--version'], _bare=True, _safe=True)
653 prefix = 'git version'
662 prefix = 'git version'
654 if stdout.startswith(prefix):
663 if stdout.startswith(prefix):
655 stdout = stdout[len(prefix):]
664 stdout = stdout[len(prefix):]
656 return stdout.strip()
665 return stdout.strip()
657
666
658 @reraise_safe_exceptions
667 @reraise_safe_exceptions
659 def run_git_command(self, wire, cmd, **opts):
668 def run_git_command(self, wire, cmd, **opts):
660 path = wire.get('path', None)
669 path = wire.get('path', None)
661
670
662 if path and os.path.isdir(path):
671 if path and os.path.isdir(path):
663 opts['cwd'] = path
672 opts['cwd'] = path
664
673
665 if '_bare' in opts:
674 if '_bare' in opts:
666 _copts = []
675 _copts = []
667 del opts['_bare']
676 del opts['_bare']
668 else:
677 else:
669 _copts = ['-c', 'core.quotepath=false', ]
678 _copts = ['-c', 'core.quotepath=false', ]
670 safe_call = False
679 safe_call = False
671 if '_safe' in opts:
680 if '_safe' in opts:
672 # no exc on failure
681 # no exc on failure
673 del opts['_safe']
682 del opts['_safe']
674 safe_call = True
683 safe_call = True
675
684
676 if '_copts' in opts:
685 if '_copts' in opts:
677 _copts.extend(opts['_copts'] or [])
686 _copts.extend(opts['_copts'] or [])
678 del opts['_copts']
687 del opts['_copts']
679
688
680 gitenv = os.environ.copy()
689 gitenv = os.environ.copy()
681 gitenv.update(opts.pop('extra_env', {}))
690 gitenv.update(opts.pop('extra_env', {}))
682 # need to clean fix GIT_DIR !
691 # need to clean fix GIT_DIR !
683 if 'GIT_DIR' in gitenv:
692 if 'GIT_DIR' in gitenv:
684 del gitenv['GIT_DIR']
693 del gitenv['GIT_DIR']
685 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
694 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
686 gitenv['GIT_DISCOVERY_ACROSS_FILESYSTEM'] = '1'
695 gitenv['GIT_DISCOVERY_ACROSS_FILESYSTEM'] = '1'
687
696
688 cmd = [settings.GIT_EXECUTABLE] + _copts + cmd
697 cmd = [settings.GIT_EXECUTABLE] + _copts + cmd
689 _opts = {'env': gitenv, 'shell': False}
698 _opts = {'env': gitenv, 'shell': False}
690
699
691 try:
700 try:
692 _opts.update(opts)
701 _opts.update(opts)
693 p = subprocessio.SubprocessIOChunker(cmd, **_opts)
702 p = subprocessio.SubprocessIOChunker(cmd, **_opts)
694
703
695 return ''.join(p), ''.join(p.error)
704 return ''.join(p), ''.join(p.error)
696 except (EnvironmentError, OSError) as err:
705 except (EnvironmentError, OSError) as err:
697 cmd = ' '.join(cmd) # human friendly CMD
706 cmd = ' '.join(cmd) # human friendly CMD
698 tb_err = ("Couldn't run git command (%s).\n"
707 tb_err = ("Couldn't run git command (%s).\n"
699 "Original error was:%s\n"
708 "Original error was:%s\n"
700 "Call options:%s\n"
709 "Call options:%s\n"
701 % (cmd, err, _opts))
710 % (cmd, err, _opts))
702 log.exception(tb_err)
711 log.exception(tb_err)
703 if safe_call:
712 if safe_call:
704 return '', err
713 return '', err
705 else:
714 else:
706 raise exceptions.VcsException()(tb_err)
715 raise exceptions.VcsException()(tb_err)
707
716
708 @reraise_safe_exceptions
717 @reraise_safe_exceptions
709 def install_hooks(self, wire, force=False):
718 def install_hooks(self, wire, force=False):
710 from vcsserver.hook_utils import install_git_hooks
719 from vcsserver.hook_utils import install_git_hooks
711 repo = self._factory.repo(wire)
720 repo = self._factory.repo(wire)
712 return install_git_hooks(repo.path, repo.bare, force_create=force)
721 return install_git_hooks(repo.path, repo.bare, force_create=force)
713
722
714
723
715 def str_to_dulwich(value):
724 def str_to_dulwich(value):
716 """
725 """
717 Dulwich 0.10.1a requires `unicode` objects to be passed in.
726 Dulwich 0.10.1a requires `unicode` objects to be passed in.
718 """
727 """
719 return value.decode(settings.WIRE_ENCODING)
728 return value.decode(settings.WIRE_ENCODING)
General Comments 0
You need to be logged in to leave comments. Login now