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