##// END OF EJS Templates
git: sync_fetch should fetch HEAD ref
marcink -
r558:d806d23f default
parent child Browse files
Show More
@@ -1,722 +1,726 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 # don't sync HEAD
477 if ref in ['HEAD']:
478 continue
479
476 remote_refs[ref] = sha
480 remote_refs[ref] = sha
477
481
478 if refs and sha in refs:
482 if refs and sha in refs:
479 # we filter fetch using our specified refs
483 # we filter fetch using our specified refs
480 fetch_refs.append('{}:{}'.format(ref, ref))
484 fetch_refs.append('{}:{}'.format(ref, ref))
481 elif not refs:
485 elif not refs:
482 fetch_refs.append('{}:{}'.format(ref, ref))
486 fetch_refs.append('{}:{}'.format(ref, ref))
483
487
484 if fetch_refs:
488 if fetch_refs:
485 _out, _err = self.run_git_command(
489 _out, _err = self.run_git_command(
486 wire, ['fetch', url, '--force', '--prune', '--'] + fetch_refs,
490 wire, ['fetch', url, '--force', '--prune', '--'] + fetch_refs,
487 fail_on_stderr=False,
491 fail_on_stderr=False,
488 _copts=['-c', 'core.askpass=""'],
492 _copts=['-c', 'core.askpass=""'],
489 extra_env={'GIT_TERMINAL_PROMPT': '0'})
493 extra_env={'GIT_TERMINAL_PROMPT': '0'})
490
494
491 return remote_refs
495 return remote_refs
492
496
493 @reraise_safe_exceptions
497 @reraise_safe_exceptions
494 def sync_push(self, wire, url, refs=None):
498 def sync_push(self, wire, url, refs=None):
495 if not self.check_url(url, wire):
499 if not self.check_url(url, wire):
496 return
500 return
497
501
498 repo = self._factory.repo(wire)
502 repo = self._factory.repo(wire)
499 self.run_git_command(
503 self.run_git_command(
500 wire, ['push', url, '--mirror'], fail_on_stderr=False,
504 wire, ['push', url, '--mirror'], fail_on_stderr=False,
501 _copts=['-c', 'core.askpass=""'],
505 _copts=['-c', 'core.askpass=""'],
502 extra_env={'GIT_TERMINAL_PROMPT': '0'})
506 extra_env={'GIT_TERMINAL_PROMPT': '0'})
503
507
504 @reraise_safe_exceptions
508 @reraise_safe_exceptions
505 def get_remote_refs(self, wire, url):
509 def get_remote_refs(self, wire, url):
506 repo = Repo(url)
510 repo = Repo(url)
507 return repo.get_refs()
511 return repo.get_refs()
508
512
509 @reraise_safe_exceptions
513 @reraise_safe_exceptions
510 def get_description(self, wire):
514 def get_description(self, wire):
511 repo = self._factory.repo(wire)
515 repo = self._factory.repo(wire)
512 return repo.get_description()
516 return repo.get_description()
513
517
514 @reraise_safe_exceptions
518 @reraise_safe_exceptions
515 def get_file_history(self, wire, file_path, commit_id, limit):
519 def get_file_history(self, wire, file_path, commit_id, limit):
516 repo = self._factory.repo(wire)
520 repo = self._factory.repo(wire)
517 include = [commit_id]
521 include = [commit_id]
518 paths = [file_path]
522 paths = [file_path]
519
523
520 walker = repo.get_walker(include, paths=paths, max_entries=limit)
524 walker = repo.get_walker(include, paths=paths, max_entries=limit)
521 return [x.commit.id for x in walker]
525 return [x.commit.id for x in walker]
522
526
523 @reraise_safe_exceptions
527 @reraise_safe_exceptions
524 def get_missing_revs(self, wire, rev1, rev2, path2):
528 def get_missing_revs(self, wire, rev1, rev2, path2):
525 repo = self._factory.repo(wire)
529 repo = self._factory.repo(wire)
526 LocalGitClient(thin_packs=False).fetch(path2, repo)
530 LocalGitClient(thin_packs=False).fetch(path2, repo)
527
531
528 wire_remote = wire.copy()
532 wire_remote = wire.copy()
529 wire_remote['path'] = path2
533 wire_remote['path'] = path2
530 repo_remote = self._factory.repo(wire_remote)
534 repo_remote = self._factory.repo(wire_remote)
531 LocalGitClient(thin_packs=False).fetch(wire["path"], repo_remote)
535 LocalGitClient(thin_packs=False).fetch(wire["path"], repo_remote)
532
536
533 revs = [
537 revs = [
534 x.commit.id
538 x.commit.id
535 for x in repo_remote.get_walker(include=[rev2], exclude=[rev1])]
539 for x in repo_remote.get_walker(include=[rev2], exclude=[rev1])]
536 return revs
540 return revs
537
541
538 @reraise_safe_exceptions
542 @reraise_safe_exceptions
539 def get_object(self, wire, sha):
543 def get_object(self, wire, sha):
540 repo = self._factory.repo(wire)
544 repo = self._factory.repo(wire)
541 obj = repo.get_object(sha)
545 obj = repo.get_object(sha)
542 commit_id = obj.id
546 commit_id = obj.id
543
547
544 if isinstance(obj, Tag):
548 if isinstance(obj, Tag):
545 commit_id = obj.object[1]
549 commit_id = obj.object[1]
546
550
547 return {
551 return {
548 'id': obj.id,
552 'id': obj.id,
549 'type': obj.type_name,
553 'type': obj.type_name,
550 'commit_id': commit_id
554 'commit_id': commit_id
551 }
555 }
552
556
553 @reraise_safe_exceptions
557 @reraise_safe_exceptions
554 def get_object_attrs(self, wire, sha, *attrs):
558 def get_object_attrs(self, wire, sha, *attrs):
555 repo = self._factory.repo(wire)
559 repo = self._factory.repo(wire)
556 obj = repo.get_object(sha)
560 obj = repo.get_object(sha)
557 return list(getattr(obj, a) for a in attrs)
561 return list(getattr(obj, a) for a in attrs)
558
562
559 @reraise_safe_exceptions
563 @reraise_safe_exceptions
560 def get_refs(self, wire):
564 def get_refs(self, wire):
561 repo = self._factory.repo(wire)
565 repo = self._factory.repo(wire)
562 result = {}
566 result = {}
563 for ref, sha in repo.refs.as_dict().items():
567 for ref, sha in repo.refs.as_dict().items():
564 peeled_sha = repo.get_peeled(ref)
568 peeled_sha = repo.get_peeled(ref)
565 result[ref] = peeled_sha
569 result[ref] = peeled_sha
566 return result
570 return result
567
571
568 @reraise_safe_exceptions
572 @reraise_safe_exceptions
569 def get_refs_path(self, wire):
573 def get_refs_path(self, wire):
570 repo = self._factory.repo(wire)
574 repo = self._factory.repo(wire)
571 return repo.refs.path
575 return repo.refs.path
572
576
573 @reraise_safe_exceptions
577 @reraise_safe_exceptions
574 def head(self, wire, show_exc=True):
578 def head(self, wire, show_exc=True):
575 repo = self._factory.repo(wire)
579 repo = self._factory.repo(wire)
576 try:
580 try:
577 return repo.head()
581 return repo.head()
578 except Exception:
582 except Exception:
579 if show_exc:
583 if show_exc:
580 raise
584 raise
581
585
582 @reraise_safe_exceptions
586 @reraise_safe_exceptions
583 def init(self, wire):
587 def init(self, wire):
584 repo_path = str_to_dulwich(wire['path'])
588 repo_path = str_to_dulwich(wire['path'])
585 self.repo = Repo.init(repo_path)
589 self.repo = Repo.init(repo_path)
586
590
587 @reraise_safe_exceptions
591 @reraise_safe_exceptions
588 def init_bare(self, wire):
592 def init_bare(self, wire):
589 repo_path = str_to_dulwich(wire['path'])
593 repo_path = str_to_dulwich(wire['path'])
590 self.repo = Repo.init_bare(repo_path)
594 self.repo = Repo.init_bare(repo_path)
591
595
592 @reraise_safe_exceptions
596 @reraise_safe_exceptions
593 def revision(self, wire, rev):
597 def revision(self, wire, rev):
594 repo = self._factory.repo(wire)
598 repo = self._factory.repo(wire)
595 obj = repo[rev]
599 obj = repo[rev]
596 obj_data = {
600 obj_data = {
597 'id': obj.id,
601 'id': obj.id,
598 }
602 }
599 try:
603 try:
600 obj_data['tree'] = obj.tree
604 obj_data['tree'] = obj.tree
601 except AttributeError:
605 except AttributeError:
602 pass
606 pass
603 return obj_data
607 return obj_data
604
608
605 @reraise_safe_exceptions
609 @reraise_safe_exceptions
606 def commit_attribute(self, wire, rev, attr):
610 def commit_attribute(self, wire, rev, attr):
607 repo = self._factory.repo(wire)
611 repo = self._factory.repo(wire)
608 obj = repo[rev]
612 obj = repo[rev]
609 return getattr(obj, attr)
613 return getattr(obj, attr)
610
614
611 @reraise_safe_exceptions
615 @reraise_safe_exceptions
612 def set_refs(self, wire, key, value):
616 def set_refs(self, wire, key, value):
613 repo = self._factory.repo(wire)
617 repo = self._factory.repo(wire)
614 repo.refs[key] = value
618 repo.refs[key] = value
615
619
616 @reraise_safe_exceptions
620 @reraise_safe_exceptions
617 def remove_ref(self, wire, key):
621 def remove_ref(self, wire, key):
618 repo = self._factory.repo(wire)
622 repo = self._factory.repo(wire)
619 del repo.refs[key]
623 del repo.refs[key]
620
624
621 @reraise_safe_exceptions
625 @reraise_safe_exceptions
622 def tree_changes(self, wire, source_id, target_id):
626 def tree_changes(self, wire, source_id, target_id):
623 repo = self._factory.repo(wire)
627 repo = self._factory.repo(wire)
624 source = repo[source_id].tree if source_id else None
628 source = repo[source_id].tree if source_id else None
625 target = repo[target_id].tree
629 target = repo[target_id].tree
626 result = repo.object_store.tree_changes(source, target)
630 result = repo.object_store.tree_changes(source, target)
627 return list(result)
631 return list(result)
628
632
629 @reraise_safe_exceptions
633 @reraise_safe_exceptions
630 def tree_items(self, wire, tree_id):
634 def tree_items(self, wire, tree_id):
631 repo = self._factory.repo(wire)
635 repo = self._factory.repo(wire)
632 tree = repo[tree_id]
636 tree = repo[tree_id]
633
637
634 result = []
638 result = []
635 for item in tree.iteritems():
639 for item in tree.iteritems():
636 item_sha = item.sha
640 item_sha = item.sha
637 item_mode = item.mode
641 item_mode = item.mode
638
642
639 if FILE_MODE(item_mode) == GIT_LINK:
643 if FILE_MODE(item_mode) == GIT_LINK:
640 item_type = "link"
644 item_type = "link"
641 else:
645 else:
642 item_type = repo[item_sha].type_name
646 item_type = repo[item_sha].type_name
643
647
644 result.append((item.path, item_mode, item_sha, item_type))
648 result.append((item.path, item_mode, item_sha, item_type))
645 return result
649 return result
646
650
647 @reraise_safe_exceptions
651 @reraise_safe_exceptions
648 def update_server_info(self, wire):
652 def update_server_info(self, wire):
649 repo = self._factory.repo(wire)
653 repo = self._factory.repo(wire)
650 update_server_info(repo)
654 update_server_info(repo)
651
655
652 @reraise_safe_exceptions
656 @reraise_safe_exceptions
653 def discover_git_version(self):
657 def discover_git_version(self):
654 stdout, _ = self.run_git_command(
658 stdout, _ = self.run_git_command(
655 {}, ['--version'], _bare=True, _safe=True)
659 {}, ['--version'], _bare=True, _safe=True)
656 prefix = 'git version'
660 prefix = 'git version'
657 if stdout.startswith(prefix):
661 if stdout.startswith(prefix):
658 stdout = stdout[len(prefix):]
662 stdout = stdout[len(prefix):]
659 return stdout.strip()
663 return stdout.strip()
660
664
661 @reraise_safe_exceptions
665 @reraise_safe_exceptions
662 def run_git_command(self, wire, cmd, **opts):
666 def run_git_command(self, wire, cmd, **opts):
663 path = wire.get('path', None)
667 path = wire.get('path', None)
664
668
665 if path and os.path.isdir(path):
669 if path and os.path.isdir(path):
666 opts['cwd'] = path
670 opts['cwd'] = path
667
671
668 if '_bare' in opts:
672 if '_bare' in opts:
669 _copts = []
673 _copts = []
670 del opts['_bare']
674 del opts['_bare']
671 else:
675 else:
672 _copts = ['-c', 'core.quotepath=false', ]
676 _copts = ['-c', 'core.quotepath=false', ]
673 safe_call = False
677 safe_call = False
674 if '_safe' in opts:
678 if '_safe' in opts:
675 # no exc on failure
679 # no exc on failure
676 del opts['_safe']
680 del opts['_safe']
677 safe_call = True
681 safe_call = True
678
682
679 if '_copts' in opts:
683 if '_copts' in opts:
680 _copts.extend(opts['_copts'] or [])
684 _copts.extend(opts['_copts'] or [])
681 del opts['_copts']
685 del opts['_copts']
682
686
683 gitenv = os.environ.copy()
687 gitenv = os.environ.copy()
684 gitenv.update(opts.pop('extra_env', {}))
688 gitenv.update(opts.pop('extra_env', {}))
685 # need to clean fix GIT_DIR !
689 # need to clean fix GIT_DIR !
686 if 'GIT_DIR' in gitenv:
690 if 'GIT_DIR' in gitenv:
687 del gitenv['GIT_DIR']
691 del gitenv['GIT_DIR']
688 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
692 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
689 gitenv['GIT_DISCOVERY_ACROSS_FILESYSTEM'] = '1'
693 gitenv['GIT_DISCOVERY_ACROSS_FILESYSTEM'] = '1'
690
694
691 cmd = [settings.GIT_EXECUTABLE] + _copts + cmd
695 cmd = [settings.GIT_EXECUTABLE] + _copts + cmd
692 _opts = {'env': gitenv, 'shell': False}
696 _opts = {'env': gitenv, 'shell': False}
693
697
694 try:
698 try:
695 _opts.update(opts)
699 _opts.update(opts)
696 p = subprocessio.SubprocessIOChunker(cmd, **_opts)
700 p = subprocessio.SubprocessIOChunker(cmd, **_opts)
697
701
698 return ''.join(p), ''.join(p.error)
702 return ''.join(p), ''.join(p.error)
699 except (EnvironmentError, OSError) as err:
703 except (EnvironmentError, OSError) as err:
700 cmd = ' '.join(cmd) # human friendly CMD
704 cmd = ' '.join(cmd) # human friendly CMD
701 tb_err = ("Couldn't run git command (%s).\n"
705 tb_err = ("Couldn't run git command (%s).\n"
702 "Original error was:%s\n"
706 "Original error was:%s\n"
703 "Call options:%s\n"
707 "Call options:%s\n"
704 % (cmd, err, _opts))
708 % (cmd, err, _opts))
705 log.exception(tb_err)
709 log.exception(tb_err)
706 if safe_call:
710 if safe_call:
707 return '', err
711 return '', err
708 else:
712 else:
709 raise exceptions.VcsException()(tb_err)
713 raise exceptions.VcsException()(tb_err)
710
714
711 @reraise_safe_exceptions
715 @reraise_safe_exceptions
712 def install_hooks(self, wire, force=False):
716 def install_hooks(self, wire, force=False):
713 from vcsserver.hook_utils import install_git_hooks
717 from vcsserver.hook_utils import install_git_hooks
714 repo = self._factory.repo(wire)
718 repo = self._factory.repo(wire)
715 return install_git_hooks(repo.path, repo.bare, force_create=force)
719 return install_git_hooks(repo.path, repo.bare, force_create=force)
716
720
717
721
718 def str_to_dulwich(value):
722 def str_to_dulwich(value):
719 """
723 """
720 Dulwich 0.10.1a requires `unicode` objects to be passed in.
724 Dulwich 0.10.1a requires `unicode` objects to be passed in.
721 """
725 """
722 return value.decode(settings.WIRE_ENCODING)
726 return value.decode(settings.WIRE_ENCODING)
General Comments 0
You need to be logged in to leave comments. Login now