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