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