##// END OF EJS Templates
git-version: strip the info returned from subcall to remove newline character.
marcink -
r103:230198b7 default
parent child Browse files
Show More
@@ -1,576 +1,576 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 2 # Copyright (C) 2014-2016 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
39 39 from vcsserver.hgcompat import (
40 40 hg_url, httpbasicauthhandler, httpdigestauthhandler)
41 41
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 return wrapper
62 62
63 63
64 64 class Repo(DulwichRepo):
65 65 """
66 66 A wrapper for dulwich Repo class.
67 67
68 68 Since dulwich is sometimes keeping .idx file descriptors open, it leads to
69 69 "Too many open files" error. We need to close all opened file descriptors
70 70 once the repo object is destroyed.
71 71
72 72 TODO: mikhail: please check if we need this wrapper after updating dulwich
73 73 to 0.12.0 +
74 74 """
75 75 def __del__(self):
76 76 if hasattr(self, 'object_store'):
77 77 self.close()
78 78
79 79
80 80 class GitFactory(RepoFactory):
81 81
82 82 def _create_repo(self, wire, create):
83 83 repo_path = str_to_dulwich(wire['path'])
84 84 return Repo(repo_path)
85 85
86 86
87 87 class GitRemote(object):
88 88
89 89 def __init__(self, factory):
90 90 self._factory = factory
91 91
92 92 self._bulk_methods = {
93 93 "author": self.commit_attribute,
94 94 "date": self.get_object_attrs,
95 95 "message": self.commit_attribute,
96 96 "parents": self.commit_attribute,
97 97 "_commit": self.revision,
98 98 }
99 99
100 100 def _assign_ref(self, wire, ref, commit_id):
101 101 repo = self._factory.repo(wire)
102 102 repo[ref] = commit_id
103 103
104 104 @reraise_safe_exceptions
105 105 def add_object(self, wire, content):
106 106 repo = self._factory.repo(wire)
107 107 blob = objects.Blob()
108 108 blob.set_raw_string(content)
109 109 repo.object_store.add_object(blob)
110 110 return blob.id
111 111
112 112 @reraise_safe_exceptions
113 113 def assert_correct_path(self, wire):
114 114 try:
115 115 self._factory.repo(wire)
116 116 except NotGitRepository as e:
117 117 # Exception can contain unicode which we convert
118 118 raise exceptions.AbortException(repr(e))
119 119
120 120 @reraise_safe_exceptions
121 121 def bare(self, wire):
122 122 repo = self._factory.repo(wire)
123 123 return repo.bare
124 124
125 125 @reraise_safe_exceptions
126 126 def blob_as_pretty_string(self, wire, sha):
127 127 repo = self._factory.repo(wire)
128 128 return repo[sha].as_pretty_string()
129 129
130 130 @reraise_safe_exceptions
131 131 def blob_raw_length(self, wire, sha):
132 132 repo = self._factory.repo(wire)
133 133 blob = repo[sha]
134 134 return blob.raw_length()
135 135
136 136 @reraise_safe_exceptions
137 137 def bulk_request(self, wire, rev, pre_load):
138 138 result = {}
139 139 for attr in pre_load:
140 140 try:
141 141 method = self._bulk_methods[attr]
142 142 args = [wire, rev]
143 143 if attr == "date":
144 144 args.extend(["commit_time", "commit_timezone"])
145 145 elif attr in ["author", "message", "parents"]:
146 146 args.append(attr)
147 147 result[attr] = method(*args)
148 148 except KeyError:
149 149 raise exceptions.VcsException(
150 150 "Unknown bulk attribute: %s" % attr)
151 151 return result
152 152
153 153 def _build_opener(self, url):
154 154 handlers = []
155 155 url_obj = hg_url(url)
156 156 _, authinfo = url_obj.authinfo()
157 157
158 158 if authinfo:
159 159 # create a password manager
160 160 passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
161 161 passmgr.add_password(*authinfo)
162 162
163 163 handlers.extend((httpbasicauthhandler(passmgr),
164 164 httpdigestauthhandler(passmgr)))
165 165
166 166 return urllib2.build_opener(*handlers)
167 167
168 168 @reraise_safe_exceptions
169 169 def check_url(self, url, config):
170 170 url_obj = hg_url(url)
171 171 test_uri, _ = url_obj.authinfo()
172 172 url_obj.passwd = '*****'
173 173 cleaned_uri = str(url_obj)
174 174
175 175 if not test_uri.endswith('info/refs'):
176 176 test_uri = test_uri.rstrip('/') + '/info/refs'
177 177
178 178 o = self._build_opener(url)
179 179 o.addheaders = [('User-Agent', 'git/1.7.8.0')] # fake some git
180 180
181 181 q = {"service": 'git-upload-pack'}
182 182 qs = '?%s' % urllib.urlencode(q)
183 183 cu = "%s%s" % (test_uri, qs)
184 184 req = urllib2.Request(cu, None, {})
185 185
186 186 try:
187 187 resp = o.open(req)
188 188 if resp.code != 200:
189 189 raise Exception('Return Code is not 200')
190 190 except Exception as e:
191 191 # means it cannot be cloned
192 192 raise urllib2.URLError("[%s] org_exc: %s" % (cleaned_uri, e))
193 193
194 194 # now detect if it's proper git repo
195 195 gitdata = resp.read()
196 196 if 'service=git-upload-pack' in gitdata:
197 197 pass
198 198 elif re.findall(r'[0-9a-fA-F]{40}\s+refs', gitdata):
199 199 # old style git can return some other format !
200 200 pass
201 201 else:
202 202 raise urllib2.URLError(
203 203 "url [%s] does not look like an git" % (cleaned_uri,))
204 204
205 205 return True
206 206
207 207 @reraise_safe_exceptions
208 208 def clone(self, wire, url, deferred, valid_refs, update_after_clone):
209 209 remote_refs = self.fetch(wire, url, apply_refs=False)
210 210 repo = self._factory.repo(wire)
211 211 if isinstance(valid_refs, list):
212 212 valid_refs = tuple(valid_refs)
213 213
214 214 for k in remote_refs:
215 215 # only parse heads/tags and skip so called deferred tags
216 216 if k.startswith(valid_refs) and not k.endswith(deferred):
217 217 repo[k] = remote_refs[k]
218 218
219 219 if update_after_clone:
220 220 # we want to checkout HEAD
221 221 repo["HEAD"] = remote_refs["HEAD"]
222 222 index.build_index_from_tree(repo.path, repo.index_path(),
223 223 repo.object_store, repo["HEAD"].tree)
224 224
225 225 # TODO: this is quite complex, check if that can be simplified
226 226 @reraise_safe_exceptions
227 227 def commit(self, wire, commit_data, branch, commit_tree, updated, removed):
228 228 repo = self._factory.repo(wire)
229 229 object_store = repo.object_store
230 230
231 231 # Create tree and populates it with blobs
232 232 commit_tree = commit_tree and repo[commit_tree] or objects.Tree()
233 233
234 234 for node in updated:
235 235 # Compute subdirs if needed
236 236 dirpath, nodename = vcspath.split(node['path'])
237 237 dirnames = map(safe_str, dirpath and dirpath.split('/') or [])
238 238 parent = commit_tree
239 239 ancestors = [('', parent)]
240 240
241 241 # Tries to dig for the deepest existing tree
242 242 while dirnames:
243 243 curdir = dirnames.pop(0)
244 244 try:
245 245 dir_id = parent[curdir][1]
246 246 except KeyError:
247 247 # put curdir back into dirnames and stops
248 248 dirnames.insert(0, curdir)
249 249 break
250 250 else:
251 251 # If found, updates parent
252 252 parent = repo[dir_id]
253 253 ancestors.append((curdir, parent))
254 254 # Now parent is deepest existing tree and we need to create
255 255 # subtrees for dirnames (in reverse order)
256 256 # [this only applies for nodes from added]
257 257 new_trees = []
258 258
259 259 blob = objects.Blob.from_string(node['content'])
260 260
261 261 if dirnames:
262 262 # If there are trees which should be created we need to build
263 263 # them now (in reverse order)
264 264 reversed_dirnames = list(reversed(dirnames))
265 265 curtree = objects.Tree()
266 266 curtree[node['node_path']] = node['mode'], blob.id
267 267 new_trees.append(curtree)
268 268 for dirname in reversed_dirnames[:-1]:
269 269 newtree = objects.Tree()
270 270 newtree[dirname] = (DIR_STAT, curtree.id)
271 271 new_trees.append(newtree)
272 272 curtree = newtree
273 273 parent[reversed_dirnames[-1]] = (DIR_STAT, curtree.id)
274 274 else:
275 275 parent.add(
276 276 name=node['node_path'], mode=node['mode'], hexsha=blob.id)
277 277
278 278 new_trees.append(parent)
279 279 # Update ancestors
280 280 reversed_ancestors = reversed(
281 281 [(a[1], b[1], b[0]) for a, b in zip(ancestors, ancestors[1:])])
282 282 for parent, tree, path in reversed_ancestors:
283 283 parent[path] = (DIR_STAT, tree.id)
284 284 object_store.add_object(tree)
285 285
286 286 object_store.add_object(blob)
287 287 for tree in new_trees:
288 288 object_store.add_object(tree)
289 289
290 290 for node_path in removed:
291 291 paths = node_path.split('/')
292 292 tree = commit_tree
293 293 trees = [tree]
294 294 # Traverse deep into the forest...
295 295 for path in paths:
296 296 try:
297 297 obj = repo[tree[path][1]]
298 298 if isinstance(obj, objects.Tree):
299 299 trees.append(obj)
300 300 tree = obj
301 301 except KeyError:
302 302 break
303 303 # Cut down the blob and all rotten trees on the way back...
304 304 for path, tree in reversed(zip(paths, trees)):
305 305 del tree[path]
306 306 if tree:
307 307 # This tree still has elements - don't remove it or any
308 308 # of it's parents
309 309 break
310 310
311 311 object_store.add_object(commit_tree)
312 312
313 313 # Create commit
314 314 commit = objects.Commit()
315 315 commit.tree = commit_tree.id
316 316 for k, v in commit_data.iteritems():
317 317 setattr(commit, k, v)
318 318 object_store.add_object(commit)
319 319
320 320 ref = 'refs/heads/%s' % branch
321 321 repo.refs[ref] = commit.id
322 322
323 323 return commit.id
324 324
325 325 @reraise_safe_exceptions
326 326 def fetch(self, wire, url, apply_refs=True, refs=None):
327 327 if url != 'default' and '://' not in url:
328 328 client = LocalGitClient(url)
329 329 else:
330 330 url_obj = hg_url(url)
331 331 o = self._build_opener(url)
332 332 url, _ = url_obj.authinfo()
333 333 client = HttpGitClient(base_url=url, opener=o)
334 334 repo = self._factory.repo(wire)
335 335
336 336 determine_wants = repo.object_store.determine_wants_all
337 337 if refs:
338 338 def determine_wants_requested(references):
339 339 return [references[r] for r in references if r in refs]
340 340 determine_wants = determine_wants_requested
341 341
342 342 try:
343 343 remote_refs = client.fetch(
344 344 path=url, target=repo, determine_wants=determine_wants)
345 345 except NotGitRepository:
346 346 log.warning(
347 347 'Trying to fetch from "%s" failed, not a Git repository.', url)
348 348 raise exceptions.AbortException()
349 349
350 350 # mikhail: client.fetch() returns all the remote refs, but fetches only
351 351 # refs filtered by `determine_wants` function. We need to filter result
352 352 # as well
353 353 if refs:
354 354 remote_refs = {k: remote_refs[k] for k in remote_refs if k in refs}
355 355
356 356 if apply_refs:
357 357 # TODO: johbo: Needs proper test coverage with a git repository
358 358 # that contains a tag object, so that we would end up with
359 359 # a peeled ref at this point.
360 360 PEELED_REF_MARKER = '^{}'
361 361 for k in remote_refs:
362 362 if k.endswith(PEELED_REF_MARKER):
363 363 log.info("Skipping peeled reference %s", k)
364 364 continue
365 365 repo[k] = remote_refs[k]
366 366
367 367 if refs:
368 368 # mikhail: explicitly set the head to the last ref.
369 369 repo['HEAD'] = remote_refs[refs[-1]]
370 370
371 371 # TODO: mikhail: should we return remote_refs here to be
372 372 # consistent?
373 373 else:
374 374 return remote_refs
375 375
376 376 @reraise_safe_exceptions
377 377 def get_remote_refs(self, wire, url):
378 378 repo = Repo(url)
379 379 return repo.get_refs()
380 380
381 381 @reraise_safe_exceptions
382 382 def get_description(self, wire):
383 383 repo = self._factory.repo(wire)
384 384 return repo.get_description()
385 385
386 386 @reraise_safe_exceptions
387 387 def get_file_history(self, wire, file_path, commit_id, limit):
388 388 repo = self._factory.repo(wire)
389 389 include = [commit_id]
390 390 paths = [file_path]
391 391
392 392 walker = repo.get_walker(include, paths=paths, max_entries=limit)
393 393 return [x.commit.id for x in walker]
394 394
395 395 @reraise_safe_exceptions
396 396 def get_missing_revs(self, wire, rev1, rev2, path2):
397 397 repo = self._factory.repo(wire)
398 398 LocalGitClient(thin_packs=False).fetch(path2, repo)
399 399
400 400 wire_remote = wire.copy()
401 401 wire_remote['path'] = path2
402 402 repo_remote = self._factory.repo(wire_remote)
403 403 LocalGitClient(thin_packs=False).fetch(wire["path"], repo_remote)
404 404
405 405 revs = [
406 406 x.commit.id
407 407 for x in repo_remote.get_walker(include=[rev2], exclude=[rev1])]
408 408 return revs
409 409
410 410 @reraise_safe_exceptions
411 411 def get_object(self, wire, sha):
412 412 repo = self._factory.repo(wire)
413 413 obj = repo.get_object(sha)
414 414 commit_id = obj.id
415 415
416 416 if isinstance(obj, Tag):
417 417 commit_id = obj.object[1]
418 418
419 419 return {
420 420 'id': obj.id,
421 421 'type': obj.type_name,
422 422 'commit_id': commit_id
423 423 }
424 424
425 425 @reraise_safe_exceptions
426 426 def get_object_attrs(self, wire, sha, *attrs):
427 427 repo = self._factory.repo(wire)
428 428 obj = repo.get_object(sha)
429 429 return list(getattr(obj, a) for a in attrs)
430 430
431 431 @reraise_safe_exceptions
432 432 def get_refs(self, wire):
433 433 repo = self._factory.repo(wire)
434 434 result = {}
435 435 for ref, sha in repo.refs.as_dict().items():
436 436 peeled_sha = repo.get_peeled(ref)
437 437 result[ref] = peeled_sha
438 438 return result
439 439
440 440 @reraise_safe_exceptions
441 441 def get_refs_path(self, wire):
442 442 repo = self._factory.repo(wire)
443 443 return repo.refs.path
444 444
445 445 @reraise_safe_exceptions
446 446 def head(self, wire):
447 447 repo = self._factory.repo(wire)
448 448 return repo.head()
449 449
450 450 @reraise_safe_exceptions
451 451 def init(self, wire):
452 452 repo_path = str_to_dulwich(wire['path'])
453 453 self.repo = Repo.init(repo_path)
454 454
455 455 @reraise_safe_exceptions
456 456 def init_bare(self, wire):
457 457 repo_path = str_to_dulwich(wire['path'])
458 458 self.repo = Repo.init_bare(repo_path)
459 459
460 460 @reraise_safe_exceptions
461 461 def revision(self, wire, rev):
462 462 repo = self._factory.repo(wire)
463 463 obj = repo[rev]
464 464 obj_data = {
465 465 'id': obj.id,
466 466 }
467 467 try:
468 468 obj_data['tree'] = obj.tree
469 469 except AttributeError:
470 470 pass
471 471 return obj_data
472 472
473 473 @reraise_safe_exceptions
474 474 def commit_attribute(self, wire, rev, attr):
475 475 repo = self._factory.repo(wire)
476 476 obj = repo[rev]
477 477 return getattr(obj, attr)
478 478
479 479 @reraise_safe_exceptions
480 480 def set_refs(self, wire, key, value):
481 481 repo = self._factory.repo(wire)
482 482 repo.refs[key] = value
483 483
484 484 @reraise_safe_exceptions
485 485 def remove_ref(self, wire, key):
486 486 repo = self._factory.repo(wire)
487 487 del repo.refs[key]
488 488
489 489 @reraise_safe_exceptions
490 490 def tree_changes(self, wire, source_id, target_id):
491 491 repo = self._factory.repo(wire)
492 492 source = repo[source_id].tree if source_id else None
493 493 target = repo[target_id].tree
494 494 result = repo.object_store.tree_changes(source, target)
495 495 return list(result)
496 496
497 497 @reraise_safe_exceptions
498 498 def tree_items(self, wire, tree_id):
499 499 repo = self._factory.repo(wire)
500 500 tree = repo[tree_id]
501 501
502 502 result = []
503 503 for item in tree.iteritems():
504 504 item_sha = item.sha
505 505 item_mode = item.mode
506 506
507 507 if FILE_MODE(item_mode) == GIT_LINK:
508 508 item_type = "link"
509 509 else:
510 510 item_type = repo[item_sha].type_name
511 511
512 512 result.append((item.path, item_mode, item_sha, item_type))
513 513 return result
514 514
515 515 @reraise_safe_exceptions
516 516 def update_server_info(self, wire):
517 517 repo = self._factory.repo(wire)
518 518 update_server_info(repo)
519 519
520 520 @reraise_safe_exceptions
521 521 def discover_git_version(self):
522 522 stdout, _ = self.run_git_command(
523 523 {}, ['--version'], _bare=True, _safe=True)
524 524 prefix = 'git version'
525 525 if stdout.startswith(prefix):
526 526 stdout = stdout[len(prefix):]
527 return stdout
527 return stdout.strip()
528 528
529 529 @reraise_safe_exceptions
530 530 def run_git_command(self, wire, cmd, **opts):
531 531 path = wire.get('path', None)
532 532
533 533 if path and os.path.isdir(path):
534 534 opts['cwd'] = path
535 535
536 536 if '_bare' in opts:
537 537 _copts = []
538 538 del opts['_bare']
539 539 else:
540 540 _copts = ['-c', 'core.quotepath=false', ]
541 541 safe_call = False
542 542 if '_safe' in opts:
543 543 # no exc on failure
544 544 del opts['_safe']
545 545 safe_call = True
546 546
547 547 gitenv = os.environ.copy()
548 548 gitenv.update(opts.pop('extra_env', {}))
549 549 # need to clean fix GIT_DIR !
550 550 if 'GIT_DIR' in gitenv:
551 551 del gitenv['GIT_DIR']
552 552 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
553 553
554 554 cmd = [settings.GIT_EXECUTABLE] + _copts + cmd
555 555
556 556 try:
557 557 _opts = {'env': gitenv, 'shell': False}
558 558 _opts.update(opts)
559 559 p = subprocessio.SubprocessIOChunker(cmd, **_opts)
560 560
561 561 return ''.join(p), ''.join(p.error)
562 562 except (EnvironmentError, OSError) as err:
563 563 tb_err = ("Couldn't run git command (%s).\n"
564 564 "Original error was:%s\n" % (cmd, err))
565 565 log.exception(tb_err)
566 566 if safe_call:
567 567 return '', err
568 568 else:
569 569 raise exceptions.VcsException(tb_err)
570 570
571 571
572 572 def str_to_dulwich(value):
573 573 """
574 574 Dulwich 0.10.1a requires `unicode` objects to be passed in.
575 575 """
576 576 return value.decode(settings.WIRE_ENCODING)
General Comments 0
You need to be logged in to leave comments. Login now