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