Dulwich.io dulwich / 80fc304
Merge tag 'dulwich-0.9.0' into debian Jelmer Vernooij 6 years ago
30 changed file(s) with 1765 addition(s) and 131 deletion(s). Raw diff Collapse all Expand all
22 John Carr <john.carr@unrouted.co.uk>
33 Dave Borowitz <dborowitz@google.com>
44 Chris Eberle <eberle1080@gmail.com>
5 "milki" <milki@rescomp.berkeley.edu>
56
67 Hervé Cauwelier <herve@itaapy.com> wrote the original tutorial.
78
0 All functionality should be available in pure Python. Optional C
1 implementations may be written for performance reasons, but should never
2 replace the Python implementation. The C implementations should follow the
3 kernel/git coding style.
4
5 Where possible include updates to NEWS along with your improvements.
6
7 New functionality and bug fixes should be accompanied with matching unit tests.
8
09 Coding style
110 ------------
211 Where possible, please follow PEP8 with regard to coding style.
413 Furthermore, triple-quotes should always be """, single quotes are ' unless
514 using " would result in less escaping within the string.
615
7 All functionality should be available in pure Python. Optional C
8 implementations may be written for performance reasons, but should never
9 replace the Python implementation. The C implementations should follow the
10 kernel/git coding style.
11
1216 Public methods, functions and classes should all have doc strings. Please use
1317 epydoc style docstrings to document parameters and return values.
1418 You can generate the documentation by running "make doc".
15
16 Where possible please include updates to NEWS along with your improvements.
1719
1820 Running the tests
1921 -----------------
0 0.9.0 2013-05-31
1
2 BUG FIXES
3
4 * Push efficiency - report missing objects only. (#562676, Artem Tikhomirov)
5
6 * Use indentation consistent with C Git in config files.
7 (#1031356, Curt Moore, Jelmer Vernooij)
8
9 * Recognize and skip binary files in diff function.
10 (Takeshi Kanemoto)
11
12 * Fix handling of relative paths in dulwich.client.get_transport_and_path.
13 (Brian Visel, #1169368)
14
15 * Preserve ordering of entries in configuration.
16 (Benjamin Pollack)
17
18 * Support ~ expansion in SSH client paths. (milki, #1083439)
19
20 * Support relative paths in alternate paths.
21 (milki, Michel Lespinasse, #1175007)
22
23 * Log all error messages from wsgiref server to the logging module. This
24 makes the test suit quiet again. (Gary van der Merwe)
25
26 * Support passing None for empty tree in changes_from_tree.
27 (Kevin Watters)
28
29 * Support fetching empty repository in client. (milki, #1060462)
30
31 IMPROVEMENTS:
32
33 * Add optional honor_filemode flag to build_index_from_tree.
34 (Mark Mikofski)
35
36 * Support core/filemode setting when building trees. (Jelmer Vernooij)
37
38 * Add chapter on tags in tutorial. (Ryan Faulkner)
39
40 FEATURES
41
42 * Add support for mergetags. (milki, #963525)
43
44 * Add support for posix shell hooks. (milki)
45
46 0.8.7 2012-11-27
47
48 BUG FIXES
49
50 * Fix use of alternates in ``DiskObjectStore``.{__contains__,__iter__}.
51 (Dmitriy)
52
53 * Fix compatibility with Python 2.4. (David Carr)
54
055 0.8.6 2012-11-09
156
257 API CHANGES
1010 repo
1111 object-store
1212 remote
13 tag
1314 conclusion
1415
0 .. _tutorial-tag:
1
2 Tagging
3 =======
4
5 This tutorial will demonstrate how to add a tag to a commit via dulwich.
6
7 First let's initialize the repository:
8
9 >>> from dulwich.repo import Repo
10 >>> _repo = Repo("myrepo", mkdir=True)
11
12 Next we build the commit object and add it to the object store:
13
14 >>> from dulwich.objects import Blob, Tree, Commit, parse_timezone
15 >>> permissions = 0100644
16 >>> author = "John Smith"
17 >>> blob = Blob.from_string("empty")
18 >>> tree = Tree()
19 >>> tree.add(tag, permissions, blob.id)
20 >>> commit = Commit()
21 >>> commit.tree = tree.id
22 >>> commit.author = commit.committer = author
23 >>> commit.commit_time = commit.author_time = int(time())
24 >>> tz = parse_timezone('-0200')[0]
25 >>> commit.commit_timezone = commit.author_timezone = tz
26 >>> commit.encoding = "UTF-8"
27 >>> commit.message = 'Tagging repo: ' + message
28
29 Add objects to the repo store instance:
30
31 >>> object_store = _repo.object_store
32 >>> object_store.add_object(blob)
33 >>> object_store.add_object(tree)
34 >>> object_store.add_object(commit)
35 >>> master_branch = 'master'
36 >>> _repo.refs['refs/heads/' + master_branch] = commit.id
37
38 Finally, add the tag top the repo:
39
40 >>> _repo['refs/tags/' + commit] = commit.id
41
42 Alternatively, we can use the tag object if we'd like to annotate the tag:
43
44 >>> from dulwich.objects import Blob, Tree, Commit, parse_timezone, Tag
45 >>> tag_message = "Tag Annotation"
46 >>> tag = Tag()
47 >>> tag.tagger = author
48 >>> tag.message = message
49 >>> tag.name = "v0.1"
50 >>> tag.object = (Commit, commit.id)
51 >>> tag.tag_time = commit.author_time
52 >>> tag.tag_timezone = tz
53 >>> object_store.add_object(tag)
54 >>> _repo['refs/tags/' + tag] = tag.id
55
56
2020
2121 """Python implementation of the Git file formats and protocols."""
2222
23 __version__ = (0, 8, 6)
23 __version__ = (0, 9, 0)
267267 pass
268268
269269 return result
270
271
272 # Backport of OrderedDict() class that runs on Python 2.4, 2.5, 2.6, 2.7 and pypy.
273 # Passes Python2.7's test suite and incorporates all the latest updates.
274 # Copyright (C) Raymond Hettinger, MIT license
275
276 try:
277 from thread import get_ident as _get_ident
278 except ImportError:
279 from dummy_thread import get_ident as _get_ident
280
281 try:
282 from _abcoll import KeysView, ValuesView, ItemsView
283 except ImportError:
284 pass
285
286 class OrderedDict(dict):
287 'Dictionary that remembers insertion order'
288 # An inherited dict maps keys to values.
289 # The inherited dict provides __getitem__, __len__, __contains__, and get.
290 # The remaining methods are order-aware.
291 # Big-O running times for all methods are the same as for regular dictionaries.
292
293 # The internal self.__map dictionary maps keys to links in a doubly linked list.
294 # The circular doubly linked list starts and ends with a sentinel element.
295 # The sentinel element never gets deleted (this simplifies the algorithm).
296 # Each link is stored as a list of length three: [PREV, NEXT, KEY].
297
298 def __init__(self, *args, **kwds):
299 '''Initialize an ordered dictionary. Signature is the same as for
300 regular dictionaries, but keyword arguments are not recommended
301 because their insertion order is arbitrary.
302
303 '''
304 if len(args) > 1:
305 raise TypeError('expected at most 1 arguments, got %d' % len(args))
306 try:
307 self.__root
308 except AttributeError:
309 self.__root = root = [] # sentinel node
310 root[:] = [root, root, None]
311 self.__map = {}
312 self.__update(*args, **kwds)
313
314 def __setitem__(self, key, value, dict_setitem=dict.__setitem__):
315 'od.__setitem__(i, y) <==> od[i]=y'
316 # Setting a new item creates a new link which goes at the end of the linked
317 # list, and the inherited dictionary is updated with the new key/value pair.
318 if key not in self:
319 root = self.__root
320 last = root[0]
321 last[1] = root[0] = self.__map[key] = [last, root, key]
322 dict_setitem(self, key, value)
323
324 def __delitem__(self, key, dict_delitem=dict.__delitem__):
325 'od.__delitem__(y) <==> del od[y]'
326 # Deleting an existing item uses self.__map to find the link which is
327 # then removed by updating the links in the predecessor and successor nodes.
328 dict_delitem(self, key)
329 link_prev, link_next, key = self.__map.pop(key)
330 link_prev[1] = link_next
331 link_next[0] = link_prev
332
333 def __iter__(self):
334 'od.__iter__() <==> iter(od)'
335 root = self.__root
336 curr = root[1]
337 while curr is not root:
338 yield curr[2]
339 curr = curr[1]
340
341 def __reversed__(self):
342 'od.__reversed__() <==> reversed(od)'
343 root = self.__root
344 curr = root[0]
345 while curr is not root:
346 yield curr[2]
347 curr = curr[0]
348
349 def clear(self):
350 'od.clear() -> None. Remove all items from od.'
351 try:
352 for node in self.__map.itervalues():
353 del node[:]
354 root = self.__root
355 root[:] = [root, root, None]
356 self.__map.clear()
357 except AttributeError:
358 pass
359 dict.clear(self)
360
361 def popitem(self, last=True):
362 """od.popitem() -> (k, v), return and remove a (key, value) pair.
363 Pairs are returned in LIFO order if last is true or FIFO order if false.
364
365 """
366 if not self:
367 raise KeyError('dictionary is empty')
368 root = self.__root
369 if last:
370 link = root[0]
371 link_prev = link[0]
372 link_prev[1] = root
373 root[0] = link_prev
374 else:
375 link = root[1]
376 link_next = link[1]
377 root[1] = link_next
378 link_next[0] = root
379 key = link[2]
380 del self.__map[key]
381 value = dict.pop(self, key)
382 return key, value
383
384 # -- the following methods do not depend on the internal structure --
385
386 def keys(self):
387 """'od.keys() -> list of keys in od"""
388 return list(self)
389
390 def values(self):
391 """od.values() -> list of values in od"""
392 return [self[key] for key in self]
393
394 def items(self):
395 """od.items() -> list of (key, value) pairs in od"""
396 return [(key, self[key]) for key in self]
397
398 def iterkeys(self):
399 """od.iterkeys() -> an iterator over the keys in od"""
400 return iter(self)
401
402 def itervalues(self):
403 """od.itervalues -> an iterator over the values in od"""
404 for k in self:
405 yield self[k]
406
407 def iteritems(self):
408 """od.iteritems -> an iterator over the (key, value) items in od"""
409 for k in self:
410 yield (k, self[k])
411
412 def update(*args, **kwds):
413 """od.update(E, **F) -> None. Update od from dict/iterable E and F.
414
415 If E is a dict instance, does: for k in E: od[k] = E[k]
416 If E has a .keys() method, does: for k in E.keys(): od[k] = E[k]
417 Or if E is an iterable of items, does: for k, v in E: od[k] = v
418 In either case, this is followed by: for k, v in F.items(): od[k] = v
419
420 """
421 if len(args) > 2:
422 raise TypeError('update() takes at most 2 positional '
423 'arguments (%d given)' % (len(args),))
424 elif not args:
425 raise TypeError('update() takes at least 1 argument (0 given)')
426 self = args[0]
427 # Make progressively weaker assumptions about "other"
428 other = ()
429 if len(args) == 2:
430 other = args[1]
431 if isinstance(other, dict):
432 for key in other:
433 self[key] = other[key]
434 elif hasattr(other, 'keys'):
435 for key in other.keys():
436 self[key] = other[key]
437 else:
438 for key, value in other:
439 self[key] = value
440 for key, value in kwds.items():
441 self[key] = value
442
443 __update = update # let subclasses override update without breaking __init__
444
445 __marker = object()
446
447 def pop(self, key, default=__marker):
448 """od.pop(k[,d]) -> v, remove specified key and return the corresponding value.
449 If key is not found, d is returned if given, otherwise KeyError is raised.
450
451 """
452 if key in self:
453 result = self[key]
454 del self[key]
455 return result
456 if default is self.__marker:
457 raise KeyError(key)
458 return default
459
460 def setdefault(self, key, default=None):
461 'od.setdefault(k[,d]) -> od.get(k,d), also set od[k]=d if k not in od'
462 if key in self:
463 return self[key]
464 self[key] = default
465 return default
466
467 def __repr__(self, _repr_running={}):
468 'od.__repr__() <==> repr(od)'
469 call_key = id(self), _get_ident()
470 if call_key in _repr_running:
471 return '...'
472 _repr_running[call_key] = 1
473 try:
474 if not self:
475 return '%s()' % (self.__class__.__name__,)
476 return '%s(%r)' % (self.__class__.__name__, self.items())
477 finally:
478 del _repr_running[call_key]
479
480 def __reduce__(self):
481 'Return state information for pickling'
482 items = [[k, self[k]] for k in self]
483 inst_dict = vars(self).copy()
484 for k in vars(OrderedDict()):
485 inst_dict.pop(k, None)
486 if inst_dict:
487 return (self.__class__, (items,), inst_dict)
488 return self.__class__, (items,)
489
490 def copy(self):
491 'od.copy() -> a shallow copy of od'
492 return self.__class__(self)
493
494 @classmethod
495 def fromkeys(cls, iterable, value=None):
496 '''OD.fromkeys(S[, v]) -> New ordered dictionary with keys from S
497 and values equal to v (which defaults to None).
498
499 '''
500 d = cls()
501 for key in iterable:
502 d[key] = value
503 return d
504
505 def __eq__(self, other):
506 '''od.__eq__(y) <==> od==y. Comparison to another OD is order-sensitive
507 while comparison to a regular mapping is order-insensitive.
508
509 '''
510 if isinstance(other, OrderedDict):
511 return len(self)==len(other) and self.items() == other.items()
512 return dict.__eq__(self, other)
513
514 def __ne__(self, other):
515 return not self == other
516
517 # -- the following methods are only used in Python 2.7 --
518
519 def viewkeys(self):
520 "od.viewkeys() -> a set-like object providing a view on od's keys"
521 return KeysView(self)
522
523 def viewvalues(self):
524 "od.viewvalues() -> an object providing a view on od's values"
525 return ValuesView(self)
526
527 def viewitems(self):
528 "od.viewitems() -> a set-like object providing a view on od's items"
529 return ItemsView(self)
168168 if server_capabilities is None:
169169 (ref, server_capabilities) = extract_capabilities(ref)
170170 refs[ref] = sha
171
172 if len(refs) == 0:
173 return None, set([])
171174 return refs, set(server_capabilities)
172175
173176 def send_pack(self, path, determine_wants, generate_pack_contents,
198201 if determine_wants is None:
199202 determine_wants = target.object_store.determine_wants_all
200203 f, commit = target.object_store.add_pack()
201 try:
202 return self.fetch_pack(path, determine_wants,
204 result = self.fetch_pack(path, determine_wants,
203205 target.get_graph_walker(), f.write, progress)
204 finally:
205 commit()
206 commit()
207 return result
206208
207209 def fetch_pack(self, path, determine_wants, graph_walker, pack_data,
208210 progress=None):
469471 proto, can_read = self._connect('upload-pack', path)
470472 refs, server_capabilities = self._read_refs(proto)
471473 negotiated_capabilities = self._fetch_capabilities & server_capabilities
474
475 if refs is None:
476 proto.write_pkt_line(None)
477 return refs
478
472479 try:
473480 wants = determine_wants(refs)
474481 except:
621628 return self.alternative_paths.get(cmd, 'git-%s' % cmd)
622629
623630 def _connect(self, cmd, path):
631 if path.startswith("/~"):
632 path = path[1:]
624633 con = get_ssh_vendor().connect_ssh(
625634 self.host, ["%s '%s'" % (self._get_cmd_path(cmd), path)],
626635 port=self.port, username=self.username)
637646
638647 def _get_url(self, path):
639648 return urlparse.urljoin(self.base_url, path).rstrip("/") + "/"
649
650 def _http_request(self, url, headers={}, data=None):
651 req = urllib2.Request(url, headers=headers, data=data)
652 try:
653 resp = self._perform(req)
654 except urllib2.HTTPError as e:
655 if e.code == 404:
656 raise NotGitRepository()
657 if e.code != 200:
658 raise GitProtocolError("unexpected http response %d" % e.code)
659 return resp
640660
641661 def _perform(self, req):
642662 """Perform a HTTP request.
655675 if self.dumb != False:
656676 url += "?service=%s" % service
657677 headers["Content-Type"] = "application/x-%s-request" % service
658 req = urllib2.Request(url, headers=headers)
659 resp = self._perform(req)
660 if resp.getcode() == 404:
661 raise NotGitRepository()
662 if resp.getcode() != 200:
663 raise GitProtocolError("unexpected http response %d" %
664 resp.getcode())
678 resp = self._http_request(url, headers)
665679 self.dumb = (not resp.info().gettype().startswith("application/x-git-"))
666680 proto = Protocol(resp.read, None)
667681 if not self.dumb:
675689 def _smart_request(self, service, url, data):
676690 assert url[-1] == "/"
677691 url = urlparse.urljoin(url, service)
678 req = urllib2.Request(url,
679 headers={"Content-Type": "application/x-%s-request" % service},
680 data=data)
681 resp = self._perform(req)
682 if resp.getcode() == 404:
683 raise NotGitRepository()
684 if resp.getcode() != 200:
685 raise GitProtocolError("Invalid HTTP response from server: %d"
686 % resp.getcode())
692 headers = {"Content-Type": "application/x-%s-request" % service}
693 resp = self._http_request(url, headers, data)
687694 if resp.info().gettype() != ("application/x-%s-result" % service):
688695 raise GitProtocolError("Invalid content-type from server: %s"
689696 % resp.info().gettype())
775782 return (TCPGitClient(parsed.hostname, port=parsed.port, **kwargs),
776783 parsed.path)
777784 elif parsed.scheme == 'git+ssh':
785 path = parsed.path
786 if path.startswith('/'):
787 path = parsed.path[1:]
778788 return SSHGitClient(parsed.hostname, port=parsed.port,
779 username=parsed.username, **kwargs), parsed.path
789 username=parsed.username, **kwargs), path
780790 elif parsed.scheme in ('http', 'https'):
781791 return HttpGitClient(urlparse.urlunparse(parsed), **kwargs), parsed.path
782792
2727 import os
2828 import re
2929
30 try:
31 from collections import OrderedDict
32 except ImportError:
33 from dulwich._compat import OrderedDict
34
3035 from UserDict import DictMixin
3136
3237 from dulwich.file import GitFile
3742
3843 def get(self, section, name):
3944 """Retrieve the contents of a configuration setting.
40
45
4146 :param section: Tuple with section name and optional subsection namee
4247 :param subsection: Subsection name
4348 :return: Contents of the setting
6671
6772 def set(self, section, name, value):
6873 """Set a configuration value.
69
74
7075 :param name: Name of the configuration value, including section
7176 and optional subsection
7277 :param: Value of the setting
8085 def __init__(self, values=None):
8186 """Create a new ConfigDict."""
8287 if values is None:
83 values = {}
88 values = OrderedDict()
8489 self._values = values
8590
8691 def __repr__(self):
9398
9499 def __getitem__(self, key):
95100 return self._values[key]
96
101
97102 def __setitem__(self, key, value):
98103 self._values[key] = value
99
104
100105 def keys(self):
101106 return self._values.keys()
102107
121126 def set(self, section, name, value):
122127 if isinstance(section, basestring):
123128 section = (section, )
124 self._values.setdefault(section, {})[name] = value
129 self._values.setdefault(section, OrderedDict())[name] = value
125130
126131
127132 def _format_string(value):
235240 section = (pts[0], pts[1])
236241 else:
237242 section = (pts[0], )
238 ret._values[section] = {}
243 ret._values[section] = OrderedDict()
239244 if _strip_comments(line).strip() == "":
240245 continue
241246 if section is None:
303308 else:
304309 f.write("[%s \"%s\"]\n" % (section_name, subsection_name))
305310 for key, value in values.iteritems():
306 f.write("%s = %s\n" % (key, _escape_value(value)))
311 f.write("\t%s = %s\n" % (key, _escape_value(value)))
307312
308313
309314 class StackedConfig(Config):
170170
171171 class RefFormatError(Exception):
172172 """Indicates an invalid ref name."""
173
174
175 class HookError(Exception):
176 """An error occurred while executing a hook."""
0 # hooks.py -- for dealing with git hooks
1 #
2 # This program is free software; you can redistribute it and/or
3 # modify it under the terms of the GNU General Public License
4 # as published by the Free Software Foundation; version 2
5 # of the License or (at your option) a later version of the License.
6 #
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
11 #
12 # You should have received a copy of the GNU General Public License
13 # along with this program; if not, write to the Free Software
14 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
15 # MA 02110-1301, USA.
16
17 """Access to hooks."""
18
19 import os
20 import subprocess
21 import tempfile
22 import warnings
23
24 from dulwich.errors import (
25 HookError,
26 )
27
28
29 class Hook(object):
30 """Generic hook object."""
31
32 def execute(elf, *args):
33 """Execute the hook with the given args
34
35 :param args: argument list to hook
36 :raise HookError: hook execution failure
37 :return: a hook may return a useful value
38 """
39 raise NotImplementedError(self.execute)
40
41
42 class ShellHook(Hook):
43 """Hook by executable file
44
45 Implements standard githooks(5) [0]:
46
47 [0] http://www.kernel.org/pub/software/scm/git/docs/githooks.html
48 """
49
50 def __init__(self, name, path, numparam,
51 pre_exec_callback=None, post_exec_callback=None):
52 """Setup shell hook definition
53
54 :param name: name of hook for error messages
55 :param path: absolute path to executable file
56 :param numparam: number of requirements parameters
57 :param pre_exec_callback: closure for setup before execution
58 Defaults to None. Takes in the variable argument list from the
59 execute functions and returns a modified argument list for the
60 shell hook.
61 :param post_exec_callback: closure for cleanup after execution
62 Defaults to None. Takes in a boolean for hook success and the
63 modified argument list and returns the final hook return value
64 if applicable
65 """
66 self.name = name
67 self.filepath = path
68 self.numparam = numparam
69
70 self.pre_exec_callback = pre_exec_callback
71 self.post_exec_callback = post_exec_callback
72
73 def execute(self, *args):
74 """Execute the hook with given args"""
75
76 if len(args) != self.numparam:
77 raise HookError("Hook %s executed with wrong number of args. \
78 Expected %d. Saw %d. %s"
79 % (self.name, self.numparam, len(args)))
80
81 if (self.pre_exec_callback is not None):
82 args = self.pre_exec_callback(*args)
83
84 try:
85 ret = subprocess.call([self.filepath] + list(args))
86 if ret != 0:
87 if (self.post_exec_callback is not None):
88 self.post_exec_callback(0, *args)
89 raise HookError("Hook %s exited with non-zero status"
90 % (self.name))
91 if (self.post_exec_callback is not None):
92 return self.post_exec_callback(1, *args)
93 except OSError: # no file. silent failure.
94 if (self.post_exec_callback is not None):
95 self.post_exec_callback(0, *args)
96
97
98 class PreCommitShellHook(ShellHook):
99 """pre-commit shell hook"""
100
101 def __init__(self, controldir):
102 filepath = os.path.join(controldir, 'hooks', 'pre-commit')
103
104 ShellHook.__init__(self, 'pre-commit', filepath, 0)
105
106
107 class PostCommitShellHook(ShellHook):
108 """post-commit shell hook"""
109
110 def __init__(self, controldir):
111 filepath = os.path.join(controldir, 'hooks', 'post-commit')
112
113 ShellHook.__init__(self, 'post-commit', filepath, 0)
114
115
116 class CommitMsgShellHook(ShellHook):
117 """commit-msg shell hook
118
119 :param args[0]: commit message
120 :return: new commit message or None
121 """
122
123 def __init__(self, controldir):
124 filepath = os.path.join(controldir, 'hooks', 'commit-msg')
125
126 def prepare_msg(*args):
127 (fd, path) = tempfile.mkstemp()
128
129 f = os.fdopen(fd, 'wb')
130 try:
131 f.write(args[0])
132 finally:
133 f.close()
134
135 return (path,)
136
137 def clean_msg(success, *args):
138 if success:
139 with open(args[0], 'rb') as f:
140 new_msg = f.read()
141 os.unlink(args[0])
142 return new_msg
143 os.unlink(args[0])
144
145 ShellHook.__init__(self, 'commit-msg', filepath, 1,
146 prepare_msg, clean_msg)
1717
1818 """Parser for the git index file format."""
1919
20 import errno
2021 import os
2122 import stat
2223 import struct
353354 :param names: Iterable of names in the working copy
354355 :param lookup_entry: Function to lookup an entry in the working copy
355356 :param object_store: Object store to use for retrieving tree contents
356 :param tree: SHA1 of the root tree
357 :param tree: SHA1 of the root tree, or None for an empty tree
357358 :param want_unchanged: Whether unchanged files should be reported
358359 :return: Iterator over tuples with (oldpath, newpath), (oldmode, newmode),
359360 (oldsha, newsha)
360361 """
361362 other_names = set(names)
362 for (name, mode, sha) in object_store.iter_tree_contents(tree):
363 try:
364 (other_sha, other_mode) = lookup_entry(name)
365 except KeyError:
366 # Was removed
367 yield ((name, None), (mode, None), (sha, None))
368 else:
369 other_names.remove(name)
370 if (want_unchanged or other_sha != sha or other_mode != mode):
371 yield ((name, name), (mode, other_mode), (sha, other_sha))
363
364 if tree is not None:
365 for (name, mode, sha) in object_store.iter_tree_contents(tree):
366 try:
367 (other_sha, other_mode) = lookup_entry(name)
368 except KeyError:
369 # Was removed
370 yield ((name, None), (mode, None), (sha, None))
371 else:
372 other_names.remove(name)
373 if (want_unchanged or other_sha != sha or other_mode != mode):
374 yield ((name, name), (mode, other_mode), (sha, other_sha))
372375
373376 # Mention added files
374377 for name in other_names:
390393 stat_val.st_gid, stat_val.st_size, hex_sha, flags)
391394
392395
393 def build_index_from_tree(prefix, index_path, object_store, tree_id):
396 def build_index_from_tree(prefix, index_path, object_store, tree_id,
397 honor_filemode=True):
394398 """Generate and materialize index from a tree
395399
396400 :param tree_id: Tree to materialize
397401 :param prefix: Target dir for materialized index files
398402 :param index_path: Target path for generated index
399403 :param object_store: Non-empty object store holding tree contents
404 :param honor_filemode: An optional flag to honor core.filemode setting in
405 config file, default is core.filemode=True, change executable bit
400406
401407 :note:: existing index is wiped and contents are not merged
402408 in a working dir. Suiteable only for fresh clones.
413419 # FIXME: Merge new index into working tree
414420 if stat.S_ISLNK(entry.mode):
415421 # FIXME: This will fail on Windows. What should we do instead?
416 os.symlink(object_store[entry.sha].as_raw_string(), full_path)
422 src_path = object_store[entry.sha].as_raw_string()
423 try:
424 os.symlink(src_path, full_path)
425 except OSError, e:
426 if e.errno == errno.EEXIST:
427 os.unlink(full_path)
428 os.symlink(src_path, full_path)
429 else:
430 raise
417431 else:
418432 f = open(full_path, 'wb')
419433 try:
422436 finally:
423437 f.close()
424438
425 os.chmod(full_path, entry.mode)
439 if honor_filemode:
440 os.chmod(full_path, entry.mode)
426441
427442 # Add file to index
428443 st = os.lstat(full_path)
00 # object_store.py -- Object store for git objects
1 # Copyright (C) 2008-2009 Jelmer Vernooij <jelmer@samba.org>
1 # Copyright (C) 2008-2012 Jelmer Vernooij <jelmer@samba.org>
2 # and others
23 #
34 # This program is free software; you can redistribute it and/or
45 # modify it under the terms of the GNU General Public License
219220 obj = self[sha]
220221 return obj
221222
223 def _collect_ancestors(self, heads, common=set()):
224 """Collect all ancestors of heads up to (excluding) those in common.
225
226 :param heads: commits to start from
227 :param common: commits to end at, or empty set to walk repository
228 completely
229 :return: a tuple (A, B) where A - all commits reachable
230 from heads but not present in common, B - common (shared) elements
231 that are directly reachable from heads
232 """
233 bases = set()
234 commits = set()
235 queue = []
236 queue.extend(heads)
237 while queue:
238 e = queue.pop(0)
239 if e in common:
240 bases.add(e)
241 elif e not in commits:
242 commits.add(e)
243 cmt = self[e]
244 queue.extend(cmt.parents)
245 return (commits, bases)
246
222247
223248 class PackBasedObjectStore(BaseObjectStore):
224249
230255 return []
231256
232257 def contains_packed(self, sha):
233 """Check if a particular object is present by SHA1 and is packed."""
258 """Check if a particular object is present by SHA1 and is packed.
259
260 This does not check alternates.
261 """
234262 for pack in self.packs:
235263 if sha in pack:
264 return True
265 return False
266
267 def __contains__(self, sha):
268 """Check if a particular object is present by SHA1.
269
270 This method makes no distinction between loose and packed objects.
271 """
272 if self.contains_packed(sha) or self.contains_loose(sha):
273 return True
274 for alternate in self.alternates:
275 if sha in alternate:
236276 return True
237277 return False
238278
256296 if self._pack_cache is None or self._pack_cache_stale():
257297 self._pack_cache = self._load_packs()
258298 return self._pack_cache
299
300 def _iter_alternate_objects(self):
301 """Iterate over the SHAs of all the objects in alternate stores."""
302 for alternate in self.alternates:
303 for alternate_object in alternate:
304 yield alternate_object
259305
260306 def _iter_loose_objects(self):
261307 """Iterate over the SHAs of all loose objects."""
282328
283329 def __iter__(self):
284330 """Iterate over the SHAs that are present in this store."""
285 iterables = self.packs + [self._iter_loose_objects()]
331 iterables = self.packs + [self._iter_loose_objects()] + [self._iter_alternate_objects()]
286332 return itertools.chain(*iterables)
287333
288334 def contains_loose(self, sha):
289 """Check if a particular object is present by SHA1 and is loose."""
335 """Check if a particular object is present by SHA1 and is loose.
336
337 This does not check alternates.
338 """
290339 return self._get_loose_object(sha) is not None
291340
292341 def get_raw(self, name):
371420 l = l.rstrip("\n")
372421 if l[0] == "#":
373422 continue
374 if not os.path.isabs(l):
375 continue
376 ret.append(l)
423 if os.path.isabs(l):
424 ret.append(l)
425 else:
426 ret.append(os.path.join(self.path, l))
377427 return ret
378428 finally:
379429 f.close()
402452 f.write("%s\n" % path)
403453 finally:
404454 f.close()
455
456 if not os.path.isabs(path):
457 path = os.path.join(self.path, path)
405458 self.alternates.append(DiskObjectStore(path))
406459
407460 def _load_packs(self):
768821 return tree.lookup_path(lookup_obj, path)
769822
770823
824 def _collect_filetree_revs(obj_store, tree_sha, kset):
825 """Collect SHA1s of files and directories for specified tree.
826
827 :param obj_store: Object store to get objects by SHA from
828 :param tree_sha: tree reference to walk
829 :param kset: set to fill with references to files and directories
830 """
831 filetree = obj_store[tree_sha]
832 for name, mode, sha in filetree.iteritems():
833 if not S_ISGITLINK(mode) and sha not in kset:
834 kset.add(sha)
835 if stat.S_ISDIR(mode):
836 _collect_filetree_revs(obj_store, sha, kset)
837
838
839 def _split_commits_and_tags(obj_store, lst, ignore_unknown=False):
840 """Split object id list into two list with commit SHA1s and tag SHA1s.
841
842 Commits referenced by tags are included into commits
843 list as well. Only SHA1s known in this repository will get
844 through, and unless ignore_unknown argument is True, KeyError
845 is thrown for SHA1 missing in the repository
846
847 :param obj_store: Object store to get objects by SHA1 from
848 :param lst: Collection of commit and tag SHAs
849 :param ignore_unknown: True to skip SHA1 missing in the repository
850 silently.
851 :return: A tuple of (commits, tags) SHA1s
852 """
853 commits = set()
854 tags = set()
855 for e in lst:
856 try:
857 o = obj_store[e]
858 except KeyError:
859 if not ignore_unknown:
860 raise
861 else:
862 if isinstance(o, Commit):
863 commits.add(e)
864 elif isinstance(o, Tag):
865 tags.add(e)
866 commits.add(o.object[1])
867 else:
868 raise KeyError('Not a commit or a tag: %s' % e)
869 return (commits, tags)
870
871
771872 class MissingObjectFinder(object):
772873 """Find the objects missing from another object store.
773874
783884
784885 def __init__(self, object_store, haves, wants, progress=None,
785886 get_tagged=None):
786 haves = set(haves)
787 self.sha_done = haves
788 self.objects_to_send = set([(w, None, False) for w in wants
789 if w not in haves])
790887 self.object_store = object_store
888 # process Commits and Tags differently
889 # Note, while haves may list commits/tags not available locally,
890 # and such SHAs would get filtered out by _split_commits_and_tags,
891 # wants shall list only known SHAs, and otherwise
892 # _split_commits_and_tags fails with KeyError
893 have_commits, have_tags = \
894 _split_commits_and_tags(object_store, haves, True)
895 want_commits, want_tags = \
896 _split_commits_and_tags(object_store, wants, False)
897 # all_ancestors is a set of commits that shall not be sent
898 # (complete repository up to 'haves')
899 all_ancestors = object_store._collect_ancestors(have_commits)[0]
900 # all_missing - complete set of commits between haves and wants
901 # common - commits from all_ancestors we hit into while
902 # traversing parent hierarchy of wants
903 missing_commits, common_commits = \
904 object_store._collect_ancestors(want_commits, all_ancestors)
905 self.sha_done = set()
906 # Now, fill sha_done with commits and revisions of
907 # files and directories known to be both locally
908 # and on target. Thus these commits and files
909 # won't get selected for fetch
910 for h in common_commits:
911 self.sha_done.add(h)
912 cmt = object_store[h]
913 _collect_filetree_revs(object_store, cmt.tree, self.sha_done)
914 # record tags we have as visited, too
915 for t in have_tags:
916 self.sha_done.add(t)
917
918 missing_tags = want_tags.difference(have_tags)
919 # in fact, what we 'want' is commits and tags
920 # we've found missing
921 wants = missing_commits.union(missing_tags)
922
923 self.objects_to_send = set([(w, None, False) for w in wants])
924
791925 if progress is None:
792926 self.progress = lambda x: None
793927 else:
797931 def add_todo(self, entries):
798932 self.objects_to_send.update([e for e in entries
799933 if not e[0] in self.sha_done])
800
801 def parse_tree(self, tree):
802 self.add_todo([(sha, name, not stat.S_ISDIR(mode))
803 for name, mode, sha in tree.iteritems()
804 if not S_ISGITLINK(mode)])
805
806 def parse_commit(self, commit):
807 self.add_todo([(commit.tree, "", False)])
808 self.add_todo([(p, None, False) for p in commit.parents])
809
810 def parse_tag(self, tag):
811 self.add_todo([(tag.object[1], None, False)])
812934
813935 def next(self):
814936 while True:
820942 if not leaf:
821943 o = self.object_store[sha]
822944 if isinstance(o, Commit):
823 self.parse_commit(o)
945 self.add_todo([(o.tree, "", False)])
824946 elif isinstance(o, Tree):
825 self.parse_tree(o)
947 self.add_todo([(s, n, not stat.S_ISDIR(m))
948 for n, m, s in o.iteritems()
949 if not S_ISGITLINK(m)])
826950 elif isinstance(o, Tag):
827 self.parse_tag(o)
951 self.add_todo([(o.object[1], None, False)])
828952 if sha in self._tagged:
829953 self.add_todo([(self._tagged[sha], None, True)])
830954 self.sha_done.add(sha)
5050 _AUTHOR_HEADER = "author"
5151 _COMMITTER_HEADER = "committer"
5252 _ENCODING_HEADER = "encoding"
53
53 _MERGETAG_HEADER = "mergetag"
5454
5555 # Header fields for objects
5656 _OBJECT_HEADER = "object"
582582 field named None for the freeform tag/commit text.
583583 """
584584 f = StringIO(text)
585 k = None
586 v = ""
585587 for l in f:
586 l = l.rstrip("\n")
587 if l == "":
588 # Empty line indicates end of headers
589 break
590 yield l.split(" ", 1)
588 if l.startswith(" "):
589 v += l[1:]
590 else:
591 if k is not None:
592 yield (k, v.rstrip("\n"))
593 if l == "\n":
594 # Empty line indicates end of headers
595 break
596 (k, v) = l.split(" ", 1)
591597 yield (None, f.read())
592598 f.close()
593599
10371043 '_commit_timezone_neg_utc', '_commit_time',
10381044 '_author_time', '_author_timezone', '_commit_timezone',
10391045 '_author', '_committer', '_parents', '_extra',
1040 '_encoding', '_tree', '_message')
1046 '_encoding', '_tree', '_message', '_mergetag')
10411047
10421048 def __init__(self):
10431049 super(Commit, self).__init__()
10441050 self._parents = []
10451051 self._encoding = None
1052 self._mergetag = []
10461053 self._extra = []
10471054 self._author_timezone_neg_utc = False
10481055 self._commit_timezone_neg_utc = False
10771084 self._encoding = value
10781085 elif field is None:
10791086 self._message = value
1087 elif field == _MERGETAG_HEADER:
1088 self._mergetag.append(Tag.from_string(value + "\n"))
10801089 else:
10811090 self._extra.append((field, value))
10821091
11311140 self._commit_timezone_neg_utc)))
11321141 if self.encoding:
11331142 chunks.append("%s %s\n" % (_ENCODING_HEADER, self.encoding))
1143 for mergetag in self.mergetag:
1144 mergetag_chunks = mergetag.as_raw_string().split("\n")
1145
1146 chunks.append("%s %s\n" % (_MERGETAG_HEADER, mergetag_chunks[0]))
1147 # Embedded extra header needs leading space
1148 for chunk in mergetag_chunks[1:]:
1149 chunks.append(" %s\n" % chunk)
1150
1151 # No trailing empty line
1152 chunks[-1] = chunks[-1].rstrip(" \n")
11341153 for k, v in self.extra:
11351154 if "\n" in k or "\n" in v:
11361155 raise AssertionError("newline in extra data: %r -> %r" % (k, v))
11841203
11851204 encoding = serializable_property("encoding",
11861205 "Encoding of the commit message.")
1206
1207 mergetag = serializable_property("mergetag",
1208 "Associated signed tag.")
11871209
11881210
11891211 OBJECT_CLASSES = (
2929 Commit,
3030 S_ISGITLINK,
3131 )
32
33 FIRST_FEW_BYTES = 8000
34
3235
3336 def write_commit_patch(f, commit, contents, progress, version=None):
3437 """Write a individual file patch.
102105 yield '+' + line
103106
104107
108 def is_binary(content):
109 """See if the first few bytes contain any null characters.
110
111 :param content: Bytestring to check for binary content
112 """
113 return '\0' in content[:FIRST_FEW_BYTES]
114
115
105116 def write_object_diff(f, store, (old_path, old_mode, old_id),
106 (new_path, new_mode, new_id)):
117 (new_path, new_mode, new_id),
118 diff_binary=False):
107119 """Write the diff for an object.
108120
109121 :param f: File-like object to write to
110122 :param store: Store to retrieve objects from, if necessary
111123 :param (old_path, old_mode, old_hexsha): Old file
112124 :param (new_path, new_mode, new_hexsha): New file
125 :param diff_binary: Whether to diff files even if they
126 are considered binary files by is_binary().
113127
114128 :note: the tuple elements should be None for nonexistant files
115129 """
118132 return "0" * 7
119133 else:
120134 return hexsha[:7]
121 def lines(mode, hexsha):
135
136 def content(mode, hexsha):
122137 if hexsha is None:
138 return ''
139 elif S_ISGITLINK(mode):
140 return "Submodule commit " + hexsha + "\n"
141 else:
142 return store[hexsha].data
143
144 def lines(content):
145 if not content:
123146 return []
124 elif S_ISGITLINK(mode):
125 return ["Submodule commit " + hexsha + "\n"]
126 else:
127 return store[hexsha].data.splitlines(True)
147 else:
148 return content.splitlines(True)
149
128150 if old_path is None:
129151 old_path = "/dev/null"
130152 else:
145167 if new_mode is not None:
146168 f.write(" %o" % new_mode)
147169 f.write("\n")
148 old_contents = lines(old_mode, old_id)
149 new_contents = lines(new_mode, new_id)
150 f.writelines(unified_diff(old_contents, new_contents,
151 old_path, new_path))
170 old_content = content(old_mode, old_id)
171 new_content = content(new_mode, new_id)
172 if not diff_binary and (is_binary(old_content) or is_binary(new_content)):
173 f.write("Binary files %s and %s differ\n" % (old_path, new_path))
174 else:
175 f.writelines(unified_diff(lines(old_content), lines(new_content),
176 old_path, new_path))
152177
153178
154179 def write_blob_diff(f, (old_path, old_mode, old_blob),
197222 old_path, new_path))
198223
199224
200 def write_tree_diff(f, store, old_tree, new_tree):
225 def write_tree_diff(f, store, old_tree, new_tree, diff_binary=False):
201226 """Write tree diff.
202227
203228 :param f: File-like object to write to.
204229 :param old_tree: Old tree id
205230 :param new_tree: New tree id
231 :param diff_binary: Whether to diff files even if they
232 are considered binary files by is_binary().
206233 """
207234 changes = store.tree_changes(old_tree, new_tree)
208235 for (oldpath, newpath), (oldmode, newmode), (oldsha, newsha) in changes:
209236 write_object_diff(f, store, (oldpath, oldmode, oldsha),
210 (newpath, newmode, newsha))
237 (newpath, newmode, newsha),
238 diff_binary=diff_binary)
211239
212240
213241 def git_am_patch_split(f):
4040 PackedRefsException,
4141 CommitError,
4242 RefFormatError,
43 HookError,
4344 )
4445 from dulwich.file import (
4546 ensure_dir_exists,
5758 Tree,
5859 hex_to_sha,
5960 )
61
62 from dulwich.hooks import (
63 PreCommitShellHook,
64 PostCommitShellHook,
65 CommitMsgShellHook,
66 )
67
6068 import warnings
6169
6270
812820 self.object_store = object_store
813821 self.refs = refs
814822
823 self.hooks = {}
824
815825 def _init_files(self, bare):
816826 """Initialize a default set of named files."""
817827 from dulwich.config import ConfigFile
11781188 if len(tree) != 40:
11791189 raise ValueError("tree must be a 40-byte hex sha string")
11801190 c.tree = tree
1191
1192 try:
1193 self.hooks['pre-commit'].execute()
1194 except HookError, e:
1195 raise CommitError(e)
1196 except KeyError: # no hook defined, silent fallthrough
1197 pass
1198
11811199 if merge_heads is None:
11821200 # FIXME: Read merge heads from .git/MERGE_HEADS
11831201 merge_heads = []
12051223 if message is None:
12061224 # FIXME: Try to read commit message from .git/MERGE_MSG
12071225 raise ValueError("No commit message specified")
1208 c.message = message
1226
1227 try:
1228 c.message = self.hooks['commit-msg'].execute(message)
1229 if c.message is None:
1230 c.message = message
1231 except HookError, e:
1232 raise CommitError(e)
1233 except KeyError: # no hook defined, message not modified
1234 c.message = message
1235
12091236 try:
12101237 old_head = self.refs[ref]
12111238 c.parents = [old_head] + merge_heads
12201247 # all its objects as garbage.
12211248 raise CommitError("%s changed during commit" % (ref,))
12221249
1250 try:
1251 self.hooks['post-commit'].execute()
1252 except HookError, e: # silent failure
1253 warnings.warn("post-commit hook failed: %s" % e, UserWarning)
1254 except KeyError: # no hook defined, silent fallthrough
1255 pass
1256
12231257 return c.id
12241258
12251259
12421276 self._controldir = root
12431277 elif (os.path.isfile(os.path.join(root, ".git"))):
12441278 import re
1245 with open(os.path.join(root, ".git"), 'r') as f:
1279 f = open(os.path.join(root, ".git"), 'r')
1280 try:
12461281 _, path = re.match('(gitdir: )(.+$)', f.read()).groups()
1282 finally:
1283 f.close()
12471284 self.bare = False
12481285 self._controldir = os.path.join(root, path)
12491286 else:
12551292 OBJECTDIR))
12561293 refs = DiskRefsContainer(self.controldir())
12571294 BaseRepo.__init__(self, object_store, refs)
1295
1296 self.hooks['pre-commit'] = PreCommitShellHook(self.controldir())
1297 self.hooks['commit-msg'] = CommitMsgShellHook(self.controldir())
1298 self.hooks['post-commit'] = PostCommitShellHook(self.controldir())
12581299
12591300 def controldir(self):
12601301 """Return the path of the control directory."""
13791420
13801421 if not bare:
13811422 # Checkout HEAD to target dir
1382 from dulwich.index import build_index_from_tree
1383 build_index_from_tree(target.path, target.index_path(),
1384 target.object_store, target['HEAD'].tree)
1423 target._build_tree()
13851424
13861425 return target
1426
1427 def _build_tree(self):
1428 from dulwich.index import build_index_from_tree
1429 config = self.get_config()
1430 honor_filemode = config.get_boolean('core', 'filemode', os.name != "nt")
1431 return build_index_from_tree(self.path, self.index_path(),
1432 self.object_store, self['HEAD'].tree,
1433 honor_filemode=honor_filemode)
13871434
13881435 def get_config(self):
13891436 """Retrieve the config object.
116116 'diff_tree',
117117 'fastexport',
118118 'file',
119 'hooks',
119120 'index',
120121 'lru_cache',
121122 'objects',
122123 'object_store',
124 'missing_obj_finder',
123125 'pack',
124126 'patch',
125127 'protocol',
3535 from dulwich.web import (
3636 make_wsgi_chain,
3737 HTTPGitApplication,
38 HTTPGitRequestHandler,
38 WSGIRequestHandlerLogger,
39 WSGIServerLogger,
3940 )
4041
4142 from dulwich.tests.compat.server_utils import (
4950
5051
5152 if getattr(simple_server.WSGIServer, 'shutdown', None):
52 WSGIServer = simple_server.WSGIServer
53 WSGIServer = WSGIServerLogger
5354 else:
54 class WSGIServer(ShutdownServerMixIn, simple_server.WSGIServer):
55 class WSGIServer(ShutdownServerMixIn, WSGIServerLogger):
5556 """Subclass of WSGIServer that can be shut down."""
5657
5758 def __init__(self, *args, **kwargs):
7677 app = self._make_app(backend)
7778 dul_server = simple_server.make_server(
7879 'localhost', 0, app, server_class=WSGIServer,
79 handler_class=HTTPGitRequestHandler)
80 handler_class=WSGIRequestHandlerLogger)
8081 self.addCleanup(dul_server.shutdown)
8182 threading.Thread(target=dul_server.serve_forever).start()
8283 self._server = dul_server
1717
1818 from cStringIO import StringIO
1919
20 from dulwich import (
21 client,
22 )
2023 from dulwich.client import (
2124 TraditionalGitClient,
2225 TCPGitClient,
7477 self.client.archive('bla', 'HEAD', None, None)
7578 self.assertEqual(self.rout.getvalue(), '0011argument HEAD0000')
7679
80 def test_fetch_empty(self):
81 self.rin.write('0000')
82 self.rin.seek(0)
83 self.client.fetch_pack('/', lambda heads: [], None, None)
84
7785 def test_fetch_pack_none(self):
7886 self.rin.write(
7987 '008855dcc6bf963f922e1ed5c4bbaaefcfacef57b1d7 HEAD.multi_ack '
9199 self.assertEqual(TCP_GIT_PORT, client._port)
92100 self.assertEqual('/bar/baz', path)
93101
102 def test_get_transport_and_path_tcp_port(self):
94103 client, path = get_transport_and_path('git://foo.com:1234/bar/baz')
95104 self.assertTrue(isinstance(client, TCPGitClient))
96105 self.assertEqual('foo.com', client._host)
103112 self.assertEqual('foo.com', client.host)
104113 self.assertEqual(None, client.port)
105114 self.assertEqual(None, client.username)
106 self.assertEqual('/bar/baz', path)
107
115 self.assertEqual('bar/baz', path)
116
117 def test_get_transport_and_path_ssh_port_explicit(self):
108118 client, path = get_transport_and_path(
109119 'git+ssh://foo.com:1234/bar/baz')
110120 self.assertTrue(isinstance(client, SSHGitClient))
111121 self.assertEqual('foo.com', client.host)
112122 self.assertEqual(1234, client.port)
123 self.assertEqual('bar/baz', path)
124
125 def test_get_transport_and_path_ssh_abspath_explicit(self):
126 client, path = get_transport_and_path('git+ssh://foo.com//bar/baz')
127 self.assertTrue(isinstance(client, SSHGitClient))
128 self.assertEqual('foo.com', client.host)
129 self.assertEqual(None, client.port)
130 self.assertEqual(None, client.username)
131 self.assertEqual('/bar/baz', path)
132
133 def test_get_transport_and_path_ssh_port_abspath_explicit(self):
134 client, path = get_transport_and_path(
135 'git+ssh://foo.com:1234//bar/baz')
136 self.assertTrue(isinstance(client, SSHGitClient))
137 self.assertEqual('foo.com', client.host)
138 self.assertEqual(1234, client.port)
113139 self.assertEqual('/bar/baz', path)
114140
115141 def test_get_transport_and_path_ssh_implicit(self):
120146 self.assertEqual(None, client.username)
121147 self.assertEqual('/bar/baz', path)
122148
149 def test_get_transport_and_path_ssh_host(self):
123150 client, path = get_transport_and_path('foo.com:/bar/baz')
124151 self.assertTrue(isinstance(client, SSHGitClient))
125152 self.assertEqual('foo.com', client.host)
127154 self.assertEqual(None, client.username)
128155 self.assertEqual('/bar/baz', path)
129156
157 def test_get_transport_and_path_ssh_user_host(self):
130158 client, path = get_transport_and_path('user@foo.com:/bar/baz')
131159 self.assertTrue(isinstance(client, SSHGitClient))
132160 self.assertEqual('foo.com', client.host)
133161 self.assertEqual(None, client.port)
134162 self.assertEqual('user', client.username)
135163 self.assertEqual('/bar/baz', path)
164
165 def test_get_transport_and_path_ssh_relpath(self):
166 client, path = get_transport_and_path('foo:bar/baz')
167 self.assertTrue(isinstance(client, SSHGitClient))
168 self.assertEqual('foo', client.host)
169 self.assertEqual(None, client.port)
170 self.assertEqual(None, client.username)
171 self.assertEqual('bar/baz', path)
172
173 def test_get_transport_and_path_ssh_host_relpath(self):
174 client, path = get_transport_and_path('foo.com:bar/baz')
175 self.assertTrue(isinstance(client, SSHGitClient))
176 self.assertEqual('foo.com', client.host)
177 self.assertEqual(None, client.port)
178 self.assertEqual(None, client.username)
179 self.assertEqual('bar/baz', path)
180
181 def test_get_transport_and_path_ssh_user_host_relpath(self):
182 client, path = get_transport_and_path('user@foo.com:bar/baz')
183 self.assertTrue(isinstance(client, SSHGitClient))
184 self.assertEqual('foo.com', client.host)
185 self.assertEqual(None, client.port)
186 self.assertEqual('user', client.username)
187 self.assertEqual('bar/baz', path)
136188
137189 def test_get_transport_and_path_subprocess(self):
138190 client, path = get_transport_and_path('foo.bar/baz')
169221 self.client.send_pack, "blah", lambda x: {}, lambda h,w: [])
170222
171223
224 class TestSSHVendor(object):
225
226 def __init__(self):
227 self.host = None
228 self.command = ""
229 self.username = None
230 self.port = None
231
232 def connect_ssh(self, host, command, username=None, port=None):
233 self.host = host
234 self.command = command
235 self.username = username
236 self.port = port
237
238 class Subprocess: pass
239 setattr(Subprocess, 'read', lambda: None)
240 setattr(Subprocess, 'write', lambda: None)
241 setattr(Subprocess, 'can_read', lambda: None)
242 return Subprocess()
243
244
172245 class SSHGitClientTests(TestCase):
173246
174247 def setUp(self):
175248 super(SSHGitClientTests, self).setUp()
249
250 self.server = TestSSHVendor()
251 self.real_vendor = client.get_ssh_vendor
252 client.get_ssh_vendor = lambda: self.server
253
176254 self.client = SSHGitClient('git.samba.org')
255
256 def tearDown(self):
257 super(SSHGitClientTests, self).tearDown()
258 client.get_ssh_vendor = self.real_vendor
177259
178260 def test_default_command(self):
179261 self.assertEqual('git-upload-pack',
185267 self.assertEqual('/usr/lib/git/git-upload-pack',
186268 self.client._get_cmd_path('upload-pack'))
187269
270 def test_connect(self):
271 server = self.server
272 client = self.client
273
274 client.username = "username"
275 client.port = 1337
276
277 client._connect("command", "/path/to/repo")
278 self.assertEquals("username", server.username)
279 self.assertEquals(1337, server.port)
280 self.assertEquals(["git-command '/path/to/repo'"], server.command)
281
282 client._connect("relative-command", "/~/path/to/repo")
283 self.assertEquals(["git-relative-command '~/path/to/repo'"],
284 server.command)
188285
189286 class ReportStatusParserTests(TestCase):
190287
136136 c.set(("core", ), "foo", "bar")
137137 f = StringIO()
138138 c.write_to_file(f)
139 self.assertEqual("[core]\nfoo = bar\n", f.getvalue())
139 self.assertEqual("[core]\n\tfoo = bar\n", f.getvalue())
140140
141141 def test_write_to_file_subsection(self):
142142 c = ConfigFile()
143143 c.set(("branch", "blie"), "foo", "bar")
144144 f = StringIO()
145145 c.write_to_file(f)
146 self.assertEqual("[branch \"blie\"]\nfoo = bar\n", f.getvalue())
146 self.assertEqual("[branch \"blie\"]\n\tfoo = bar\n", f.getvalue())
147147
148148 def test_same_line(self):
149149 cf = self.from_file("[branch.foo] foo = bar\n")
174174 cd.set(("core", ), "foo", "bla")
175175 cd.set(("core2", ), "foo", "bloe")
176176
177 self.assertEqual([("core2", ), ("core", )], cd.keys())
177 self.assertEqual([("core", ), ("core2", )], cd.keys())
178178 self.assertEqual(cd[("core", )], {'foo': 'bla'})
179179
180180 cd['a'] = 'b'
0 # test_hooks.py -- Tests for executing hooks
1 #
2 # This program is free software; you can redistribute it and/or
3 # modify it under the terms of the GNU General Public License
4 # as published by the Free Software Foundation; either version 2
5 # or (at your option) a later version of the License.
6 #
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
11 #
12 # You should have received a copy of the GNU General Public License
13 # along with this program; if not, write to the Free Software
14 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
15 # MA 02110-1301, USA.
16
17 """Tests for executing hooks."""
18
19 import os
20 import stat
21 import shutil
22 import tempfile
23 import warnings
24
25 from dulwich import errors
26
27 from dulwich.hooks import (
28 PreCommitShellHook,
29 PostCommitShellHook,
30 CommitMsgShellHook,
31 )
32
33 from dulwich.tests import TestCase
34
35
36 class ShellHookTests(TestCase):
37
38 def setUp(self):
39 if os.name != 'posix':
40 self.skipTest('shell hook tests requires POSIX shell')
41
42 def test_hook_pre_commit(self):
43 pre_commit_fail = """#!/bin/sh
44 exit 1
45 """
46
47 pre_commit_success = """#!/bin/sh
48 exit 0
49 """
50
51 repo_dir = os.path.join(tempfile.mkdtemp())
52 os.mkdir(os.path.join(repo_dir, 'hooks'))
53 self.addCleanup(shutil.rmtree, repo_dir)
54
55 pre_commit = os.path.join(repo_dir, 'hooks', 'pre-commit')
56 hook = PreCommitShellHook(repo_dir)
57
58 f = open(pre_commit, 'wb')
59 try:
60 f.write(pre_commit_fail)
61 finally:
62 f.close()
63 os.chmod(pre_commit, stat.S_IREAD | stat.S_IWRITE | stat.S_IEXEC)
64
65 self.assertRaises(errors.HookError, hook.execute)
66
67 f = open(pre_commit, 'wb')
68 try:
69 f.write(pre_commit_success)
70 finally:
71 f.close()
72 os.chmod(pre_commit, stat.S_IREAD | stat.S_IWRITE | stat.S_IEXEC)
73
74 hook.execute()
75
76 def test_hook_commit_msg(self):
77
78 commit_msg_fail = """#!/bin/sh
79 exit 1
80 """
81
82 commit_msg_success = """#!/bin/sh
83 exit 0
84 """
85
86 repo_dir = os.path.join(tempfile.mkdtemp())
87 os.mkdir(os.path.join(repo_dir, 'hooks'))
88 self.addCleanup(shutil.rmtree, repo_dir)
89
90 commit_msg = os.path.join(repo_dir, 'hooks', 'commit-msg')
91 hook = CommitMsgShellHook(repo_dir)
92
93 f = open(commit_msg, 'wb')
94 try:
95 f.write(commit_msg_fail)
96 finally:
97 f.close()
98 os.chmod(commit_msg, stat.S_IREAD | stat.S_IWRITE | stat.S_IEXEC)
99
100 self.assertRaises(errors.HookError, hook.execute, 'failed commit')
101
102 f = open(commit_msg, 'wb')
103 try:
104 f.write(commit_msg_success)
105 finally:
106 f.close()
107 os.chmod(commit_msg, stat.S_IREAD | stat.S_IWRITE | stat.S_IEXEC)
108
109 hook.execute('empty commit')
110
111 def test_hook_post_commit(self):
112
113 (fd, path) = tempfile.mkstemp()
114 post_commit_msg = """#!/bin/sh
115 unlink %(file)s
116 """ % {'file': path}
117
118 post_commit_msg_fail = """#!/bin/sh
119 exit 1
120 """
121
122 repo_dir = os.path.join(tempfile.mkdtemp())
123 os.mkdir(os.path.join(repo_dir, 'hooks'))
124 self.addCleanup(shutil.rmtree, repo_dir)
125
126 post_commit = os.path.join(repo_dir, 'hooks', 'post-commit')
127 hook = PostCommitShellHook(repo_dir)
128
129 f = open(post_commit, 'wb')
130 try:
131 f.write(post_commit_msg_fail)
132 finally:
133 f.close()
134 os.chmod(post_commit, stat.S_IREAD | stat.S_IWRITE | stat.S_IEXEC)
135
136 self.assertRaises(errors.HookError, hook.execute)
137
138 f = open(post_commit, 'wb')
139 try:
140 f.write(post_commit_msg)
141 finally:
142 f.close()
143 os.chmod(post_commit, stat.S_IREAD | stat.S_IWRITE | stat.S_IEXEC)
144
145 hook.execute()
146 self.assertFalse(os.path.exists(path))
7575 self.assertEqual(0, len(i))
7676 self.assertFalse(os.path.exists(i._filename))
7777
78 def test_against_empty_tree(self):
79 i = self.get_simple_index("index")
80 changes = list(i.changes_from_tree(MemoryObjectStore(), None))
81 self.assertEqual(1, len(changes))
82 (oldname, newname), (oldmode, newmode), (oldsha, newsha) = changes[0]
83 self.assertEqual('bla', newname)
84 self.assertEqual('e69de29bb2d1d6434b8b29ae775ad8c2e48c5391', newsha)
7885
7986 class SimpleIndexWriterTestCase(IndexTestCase):
8087
0 # test_missing_obj_finder.py -- tests for MissingObjectFinder
1 # Copyright (C) 2012 syntevo GmbH
2 #
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; version 2
6 # or (at your option) any later version of the License.
7 #
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
12 #
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software
15 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
16 # MA 02110-1301, USA.
17
18 from dulwich.object_store import (
19 MemoryObjectStore,
20 )
21 from dulwich.objects import (
22 Blob,
23 )
24 from dulwich.tests import TestCase
25 from utils import (
26 make_object,
27 build_commit_graph,
28 )
29
30
31 class MissingObjectFinderTest(TestCase):
32
33 def setUp(self):
34 super(MissingObjectFinderTest, self).setUp()
35 self.store = MemoryObjectStore()
36 self.commits = []
37
38 def cmt(self, n):
39 return self.commits[n-1]
40
41 def assertMissingMatch(self, haves, wants, expected):
42 for sha, path in self.store.find_missing_objects(haves, wants):
43 self.assertTrue(sha in expected,
44 "(%s,%s) erroneously reported as missing" % (sha, path))
45 expected.remove(sha)
46
47 self.assertEquals(len(expected), 0,
48 "some objects are not reported as missing: %s" % (expected, ))
49
50
51 class MOFLinearRepoTest(MissingObjectFinderTest):
52
53 def setUp(self):
54 super(MOFLinearRepoTest, self).setUp()
55 f1_1 = make_object(Blob, data='f1') # present in 1, removed in 3
56 f2_1 = make_object(Blob, data='f2') # present in all revisions, changed in 2 and 3
57 f2_2 = make_object(Blob, data='f2-changed')
58 f2_3 = make_object(Blob, data='f2-changed-again')
59 f3_2 = make_object(Blob, data='f3') # added in 2, left unmodified in 3
60
61 commit_spec = [[1], [2, 1], [3, 2]]
62 trees = {1: [('f1', f1_1), ('f2', f2_1)],
63 2: [('f1', f1_1), ('f2', f2_2), ('f3', f3_2)],
64 3: [('f2', f2_3), ('f3', f3_2)] }
65 # commit 1: f1 and f2
66 # commit 2: f3 added, f2 changed. Missing shall report commit id and a
67 # tree referenced by commit
68 # commit 3: f1 removed, f2 changed. Commit sha and root tree sha shall
69 # be reported as modified
70 self.commits = build_commit_graph(self.store, commit_spec, trees)
71 self.missing_1_2 = [self.cmt(2).id, self.cmt(2).tree, f2_2.id, f3_2.id]
72 self.missing_2_3 = [self.cmt(3).id, self.cmt(3).tree, f2_3.id]
73 self.missing_1_3 = [
74 self.cmt(2).id, self.cmt(3).id,
75 self.cmt(2).tree, self.cmt(3).tree,
76 f2_2.id, f3_2.id, f2_3.id]
77
78 def test_1_to_2(self):
79 self.assertMissingMatch([self.cmt(1).id], [self.cmt(2).id],
80 self.missing_1_2)
81
82 def test_2_to_3(self):
83 self.assertMissingMatch([self.cmt(2).id], [self.cmt(3).id],
84 self.missing_2_3)
85
86 def test_1_to_3(self):
87 self.assertMissingMatch([self.cmt(1).id], [self.cmt(3).id],
88 self.missing_1_3)
89
90 def test_bogus_haves_failure(self):
91 """Ensure non-existent SHA in haves are not tolerated"""
92 bogus_sha = self.cmt(2).id[::-1]
93 haves = [self.cmt(1).id, bogus_sha]
94 wants = [self.cmt(3).id]
95 self.assertRaises(KeyError, self.store.find_missing_objects,
96 self.store, haves, wants)
97
98 def test_bogus_wants_failure(self):
99 """Ensure non-existent SHA in wants are not tolerated"""
100 bogus_sha = self.cmt(2).id[::-1]
101 haves = [self.cmt(1).id]
102 wants = [self.cmt(3).id, bogus_sha]
103 self.assertRaises(KeyError, self.store.find_missing_objects,
104 self.store, haves, wants)
105
106 def test_no_changes(self):
107 self.assertMissingMatch([self.cmt(3).id], [self.cmt(3).id], [])
108
109
110 class MOFMergeForkRepoTest(MissingObjectFinderTest):
111 # 1 --- 2 --- 4 --- 6 --- 7
112 # \ /
113 # 3 ---
114 # \
115 # 5
116
117 def setUp(self):
118 super(MOFMergeForkRepoTest, self).setUp()
119 f1_1 = make_object(Blob, data='f1')
120 f1_2 = make_object(Blob, data='f1-2')
121 f1_4 = make_object(Blob, data='f1-4')
122 f1_7 = make_object(Blob, data='f1-2') # same data as in rev 2
123 f2_1 = make_object(Blob, data='f2')
124 f2_3 = make_object(Blob, data='f2-3')
125 f3_3 = make_object(Blob, data='f3')
126 f3_5 = make_object(Blob, data='f3-5')
127 commit_spec = [[1], [2, 1], [3, 2], [4, 2], [5, 3], [6, 3, 4], [7, 6]]
128 trees = {1: [('f1', f1_1), ('f2', f2_1)],
129 2: [('f1', f1_2), ('f2', f2_1)], # f1 changed
130 # f3 added, f2 changed
131 3: [('f1', f1_2), ('f2', f2_3), ('f3', f3_3)],
132 4: [('f1', f1_4), ('f2', f2_1)], # f1 changed
133 5: [('f1', f1_2), ('f3', f3_5)], # f2 removed, f3 changed
134 6: [('f1', f1_4), ('f2', f2_3), ('f3', f3_3)], # merged 3 and 4
135 # f1 changed to match rev2. f3 removed
136 7: [('f1', f1_7), ('f2', f2_3)]}
137 self.commits = build_commit_graph(self.store, commit_spec, trees)
138
139 self.f1_2_id = f1_2.id
140 self.f1_4_id = f1_4.id
141 self.f1_7_id = f1_7.id
142 self.f2_3_id = f2_3.id
143 self.f3_3_id = f3_3.id
144
145 self.assertEquals(f1_2.id, f1_7.id, "[sanity]")
146
147 def test_have6_want7(self):
148 # have 6, want 7. Ideally, shall not report f1_7 as it's the same as
149 # f1_2, however, to do so, MissingObjectFinder shall not record trees
150 # of common commits only, but also all parent trees and tree items,
151 # which is an overkill (i.e. in sha_done it records f1_4 as known, and
152 # doesn't record f1_2 was known prior to that, hence can't detect f1_7
153 # is in fact f1_2 and shall not be reported)
154 self.assertMissingMatch([self.cmt(6).id], [self.cmt(7).id],
155 [self.cmt(7).id, self.cmt(7).tree, self.f1_7_id])
156
157 def test_have4_want7(self):
158 # have 4, want 7. Shall not include rev5 as it is not in the tree
159 # between 4 and 7 (well, it is, but its SHA's are irrelevant for 4..7
160 # commit hierarchy)
161 self.assertMissingMatch([self.cmt(4).id], [self.cmt(7).id], [
162 self.cmt(7).id, self.cmt(6).id, self.cmt(3).id,
163 self.cmt(7).tree, self.cmt(6).tree, self.cmt(3).tree,
164 self.f2_3_id, self.f3_3_id])
165
166 def test_have1_want6(self):
167 # have 1, want 6. Shall not include rev5
168 self.assertMissingMatch([self.cmt(1).id], [self.cmt(6).id], [
169 self.cmt(6).id, self.cmt(4).id, self.cmt(3).id, self.cmt(2).id,
170 self.cmt(6).tree, self.cmt(4).tree, self.cmt(3).tree,
171 self.cmt(2).tree, self.f1_2_id, self.f1_4_id, self.f2_3_id,
172 self.f3_3_id])
173
174 def test_have3_want6(self):
175 # have 3, want 7. Shall not report rev2 and its tree, because
176 # haves(3) means has parents, i.e. rev2, too
177 # BUT shall report any changes descending rev2 (excluding rev3)
178 # Shall NOT report f1_7 as it's techically == f1_2
179 self.assertMissingMatch([self.cmt(3).id], [self.cmt(7).id], [
180 self.cmt(7).id, self.cmt(6).id, self.cmt(4).id,
181 self.cmt(7).tree, self.cmt(6).tree, self.cmt(4).tree,
182 self.f1_4_id])
183
184 def test_have5_want7(self):
185 # have 5, want 7. Common parent is rev2, hence children of rev2 from
186 # a descent line other than rev5 shall be reported
187 # expects f1_4 from rev6. f3_5 is known in rev5;
188 # f1_7 shall be the same as f1_2 (known, too)
189 self.assertMissingMatch([self.cmt(5).id], [self.cmt(7).id], [
190 self.cmt(7).id, self.cmt(6).id, self.cmt(4).id,
191 self.cmt(7).tree, self.cmt(6).tree, self.cmt(4).tree,
192 self.f1_4_id])
236236 store = DiskObjectStore(self.store_dir)
237237 self.assertRaises(KeyError, store.__getitem__, b2.id)
238238 store.add_alternate_path(alternate_dir)
239 self.assertIn(b2.id, store)
239240 self.assertEqual(b2, store[b2.id])
240241
241242 def test_add_alternate_path(self):
247248 self.assertEqual(
248249 ["/foo/path", "/bar/path"],
249250 store._read_alternate_paths())
251
252 def test_rel_alternative_path(self):
253 alternate_dir = tempfile.mkdtemp()
254 self.addCleanup(shutil.rmtree, alternate_dir)
255 alternate_store = DiskObjectStore(alternate_dir)
256 b2 = make_object(Blob, data="yummy data")
257 alternate_store.add_object(b2)
258 store = DiskObjectStore(self.store_dir)
259 self.assertRaises(KeyError, store.__getitem__, b2.id)
260 store.add_alternate_path(os.path.relpath(alternate_dir, self.store_dir))
261 self.assertEqual(list(alternate_store), list(store.alternates[0]))
262 self.assertIn(b2.id, store)
263 self.assertEqual(b2, store[b2.id])
250264
251265 def test_pack_dir(self):
252266 o = DiskObjectStore(self.store_dir)
156156
157157 def test_read_tag_from_file(self):
158158 t = self.get_tag(tag_sha)
159 self.assertEqual(t.object, (Commit, '51b668fd5bf7061b7d6fa525f88803e6cfadaa51'))
159 self.assertEqual(t.object,
160 (Commit, '51b668fd5bf7061b7d6fa525f88803e6cfadaa51'))
160161 self.assertEqual(t.name,'signed')
161162 self.assertEqual(t.tagger,'Ali Sabil <ali.sabil@gmail.com>')
162163 self.assertEqual(t.tag_time, 1231203091)
311312 d = Commit()
312313 d._deserialize(c.as_raw_chunks())
313314 self.assertEqual(c, d)
315
316 def test_serialize_mergetag(self):
317 tag = make_object(
318 Tag, object=(Commit, "a38d6181ff27824c79fc7df825164a212eff6a3f"),
319 object_type_name="commit",
320 name="v2.6.22-rc7",
321 tag_time=1183319674,
322 tag_timezone=0,
323 tagger="Linus Torvalds <torvalds@woody.linux-foundation.org>",
324 message=default_message)
325 commit = self.make_commit(mergetag=[tag])
326
327 self.assertEqual("""tree d80c186a03f423a81b39df39dc87fd269736ca86
328 parent ab64bbdcc51b170d21588e5c5d391ee5c0c96dfd
329 parent 4cffe90e0a41ad3f5190079d7c8f036bde29cbe6
330 author James Westby <jw+debian@jameswestby.net> 1174773719 +0000
331 committer James Westby <jw+debian@jameswestby.net> 1174773719 +0000
332 mergetag object a38d6181ff27824c79fc7df825164a212eff6a3f
333 type commit
334 tag v2.6.22-rc7
335 tagger Linus Torvalds <torvalds@woody.linux-foundation.org> 1183319674 +0000
336
337 Linux 2.6.22-rc7
338 -----BEGIN PGP SIGNATURE-----
339 Version: GnuPG v1.4.7 (GNU/Linux)
340
341 iD8DBQBGiAaAF3YsRnbiHLsRAitMAKCiLboJkQECM/jpYsY3WPfvUgLXkACgg3ql
342 OK2XeQOiEeXtT76rV4t2WR4=
343 =ivrA
344 -----END PGP SIGNATURE-----
345
346 Merge ../b
347 """, commit.as_raw_string())
348
349 def test_serialize_mergetags(self):
350 tag = make_object(
351 Tag, object=(Commit, "a38d6181ff27824c79fc7df825164a212eff6a3f"),
352 object_type_name="commit",
353 name="v2.6.22-rc7",
354 tag_time=1183319674,
355 tag_timezone=0,
356 tagger="Linus Torvalds <torvalds@woody.linux-foundation.org>",
357 message=default_message)
358 commit = self.make_commit(mergetag=[tag, tag])
359
360 self.assertEqual("""tree d80c186a03f423a81b39df39dc87fd269736ca86
361 parent ab64bbdcc51b170d21588e5c5d391ee5c0c96dfd
362 parent 4cffe90e0a41ad3f5190079d7c8f036bde29cbe6
363 author James Westby <jw+debian@jameswestby.net> 1174773719 +0000
364 committer James Westby <jw+debian@jameswestby.net> 1174773719 +0000
365 mergetag object a38d6181ff27824c79fc7df825164a212eff6a3f
366 type commit
367 tag v2.6.22-rc7
368 tagger Linus Torvalds <torvalds@woody.linux-foundation.org> 1183319674 +0000
369
370 Linux 2.6.22-rc7
371 -----BEGIN PGP SIGNATURE-----
372 Version: GnuPG v1.4.7 (GNU/Linux)
373
374 iD8DBQBGiAaAF3YsRnbiHLsRAitMAKCiLboJkQECM/jpYsY3WPfvUgLXkACgg3ql
375 OK2XeQOiEeXtT76rV4t2WR4=
376 =ivrA
377 -----END PGP SIGNATURE-----
378 mergetag object a38d6181ff27824c79fc7df825164a212eff6a3f
379 type commit
380 tag v2.6.22-rc7
381 tagger Linus Torvalds <torvalds@woody.linux-foundation.org> 1183319674 +0000
382
383 Linux 2.6.22-rc7
384 -----BEGIN PGP SIGNATURE-----
385 Version: GnuPG v1.4.7 (GNU/Linux)
386
387 iD8DBQBGiAaAF3YsRnbiHLsRAitMAKCiLboJkQECM/jpYsY3WPfvUgLXkACgg3ql
388 OK2XeQOiEeXtT76rV4t2WR4=
389 =ivrA
390 -----END PGP SIGNATURE-----
391
392 Merge ../b
393 """, commit.as_raw_string())
394
395 def test_deserialize_mergetag(self):
396 tag = make_object(
397 Tag, object=(Commit, "a38d6181ff27824c79fc7df825164a212eff6a3f"),
398 object_type_name="commit",
399 name="v2.6.22-rc7",
400 tag_time=1183319674,
401 tag_timezone=0,
402 tagger="Linus Torvalds <torvalds@woody.linux-foundation.org>",
403 message=default_message)
404 commit = self.make_commit(mergetag=[tag])
405
406 d = Commit()
407 d._deserialize(commit.as_raw_chunks())
408 self.assertEqual(commit, d)
409
410 def test_deserialize_mergetags(self):
411 tag = make_object(
412 Tag, object=(Commit, "a38d6181ff27824c79fc7df825164a212eff6a3f"),
413 object_type_name="commit",
414 name="v2.6.22-rc7",
415 tag_time=1183319674,
416 tag_timezone=0,
417 tagger="Linus Torvalds <torvalds@woody.linux-foundation.org>",
418 message=default_message)
419 commit = self.make_commit(mergetag=[tag, tag])
420
421 d = Commit()
422 d._deserialize(commit.as_raw_chunks())
423 self.assertEquals(commit, d)
314424
315425
316426 default_committer = 'James Westby <jw+debian@jameswestby.net> 1174773719 +0000'
368368 '-same'
369369 ], f.getvalue().splitlines())
370370
371 def test_object_diff_bin_blob_force(self):
372 f = StringIO()
373 # Prepare two slightly different PNG headers
374 b1 = Blob.from_string(
375 "\x89\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52"
376 "\x00\x00\x01\xd5\x00\x00\x00\x9f\x08\x04\x00\x00\x00\x05\x04\x8b")
377 b2 = Blob.from_string(
378 "\x89\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52"
379 "\x00\x00\x01\xd5\x00\x00\x00\x9f\x08\x03\x00\x00\x00\x98\xd3\xb3")
380 store = MemoryObjectStore()
381 store.add_objects([(b1, None), (b2, None)])
382 write_object_diff(f, store, ('foo.png', 0644, b1.id),
383 ('bar.png', 0644, b2.id), diff_binary=True)
384 self.assertEqual([
385 'diff --git a/foo.png b/bar.png',
386 'index f73e47d..06364b7 644',
387 '--- a/foo.png',
388 '+++ b/bar.png',
389 '@@ -1,4 +1,4 @@',
390 ' \x89PNG',
391 ' \x1a',
392 ' \x00\x00\x00',
393 '-IHDR\x00\x00\x01\xd5\x00\x00\x00\x9f\x08\x04\x00\x00\x00\x05\x04\x8b',
394 '\\ No newline at end of file',
395 '+IHDR\x00\x00\x01\xd5\x00\x00\x00\x9f\x08\x03\x00\x00\x00\x98\xd3\xb3',
396 '\\ No newline at end of file'
397 ], f.getvalue().splitlines())
398
399 def test_object_diff_bin_blob(self):
400 f = StringIO()
401 # Prepare two slightly different PNG headers
402 b1 = Blob.from_string(
403 "\x89\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52"
404 "\x00\x00\x01\xd5\x00\x00\x00\x9f\x08\x04\x00\x00\x00\x05\x04\x8b")
405 b2 = Blob.from_string(
406 "\x89\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52"
407 "\x00\x00\x01\xd5\x00\x00\x00\x9f\x08\x03\x00\x00\x00\x98\xd3\xb3")
408 store = MemoryObjectStore()
409 store.add_objects([(b1, None), (b2, None)])
410 write_object_diff(f, store, ('foo.png', 0644, b1.id),
411 ('bar.png', 0644, b2.id))
412 self.assertEqual([
413 'diff --git a/foo.png b/bar.png',
414 'index f73e47d..06364b7 644',
415 'Binary files a/foo.png and b/bar.png differ'
416 ], f.getvalue().splitlines())
417
418 def test_object_diff_add_bin_blob(self):
419 f = StringIO()
420 b2 = Blob.from_string(
421 '\x89\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52'
422 '\x00\x00\x01\xd5\x00\x00\x00\x9f\x08\x03\x00\x00\x00\x98\xd3\xb3')
423 store = MemoryObjectStore()
424 store.add_object(b2)
425 write_object_diff(f, store, (None, None, None),
426 ('bar.png', 0644, b2.id))
427 self.assertEqual([
428 'diff --git /dev/null b/bar.png',
429 'new mode 644',
430 'index 0000000..06364b7 644',
431 'Binary files /dev/null and b/bar.png differ'
432 ], f.getvalue().splitlines())
433
434 def test_object_diff_remove_bin_blob(self):
435 f = StringIO()
436 b1 = Blob.from_string(
437 '\x89\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52'
438 '\x00\x00\x01\xd5\x00\x00\x00\x9f\x08\x04\x00\x00\x00\x05\x04\x8b')
439 store = MemoryObjectStore()
440 store.add_object(b1)
441 write_object_diff(f, store, ('foo.png', 0644, b1.id),
442 (None, None, None))
443 self.assertEqual([
444 'diff --git a/foo.png /dev/null',
445 'deleted mode 644',
446 'index f73e47d..0000000',
447 'Binary files a/foo.png and /dev/null differ'
448 ], f.getvalue().splitlines())
449
371450 def test_object_diff_kind_change(self):
372451 f = StringIO()
373452 b1 = Blob.from_string("new\nsame\n")
2020
2121 from cStringIO import StringIO
2222 import os
23 import stat
2324 import shutil
2425 import tempfile
2526 import warnings
5051 from dulwich.tests.utils import (
5152 open_repo,
5253 tear_down_repo,
54 setup_warning_catcher,
5355 )
5456
5557 missing_sha = 'b91fa4d900e17e99b433218e988c4eb4a3e9a097'
410412 finally:
411413 shutil.rmtree(r1_dir)
412414 shutil.rmtree(r2_dir)
415
416 def test_shell_hook_pre_commit(self):
417 if os.name != 'posix':
418 self.skipTest('shell hook tests requires POSIX shell')
419
420 pre_commit_fail = """#!/bin/sh
421 exit 1
422 """
423
424 pre_commit_success = """#!/bin/sh
425 exit 0
426 """
427
428 repo_dir = os.path.join(tempfile.mkdtemp())
429 r = Repo.init(repo_dir)
430 self.addCleanup(shutil.rmtree, repo_dir)
431
432 pre_commit = os.path.join(r.controldir(), 'hooks', 'pre-commit')
433
434 f = open(pre_commit, 'wb')
435 try:
436 f.write(pre_commit_fail)
437 finally:
438 f.close()
439 os.chmod(pre_commit, stat.S_IREAD | stat.S_IWRITE | stat.S_IEXEC)
440
441 self.assertRaises(errors.CommitError, r.do_commit, 'failed commit',
442 committer='Test Committer <test@nodomain.com>',
443 author='Test Author <test@nodomain.com>',
444 commit_timestamp=12345, commit_timezone=0,
445 author_timestamp=12345, author_timezone=0)
446
447 f = open(pre_commit, 'wb')
448 try:
449 f.write(pre_commit_success)
450 finally:
451 f.close()
452 os.chmod(pre_commit, stat.S_IREAD | stat.S_IWRITE | stat.S_IEXEC)
453
454 commit_sha = r.do_commit(
455 'empty commit',
456 committer='Test Committer <test@nodomain.com>',
457 author='Test Author <test@nodomain.com>',
458 commit_timestamp=12395, commit_timezone=0,
459 author_timestamp=12395, author_timezone=0)
460 self.assertEqual([], r[commit_sha].parents)
461
462 def test_shell_hook_commit_msg(self):
463 if os.name != 'posix':
464 self.skipTest('shell hook tests requires POSIX shell')
465
466 commit_msg_fail = """#!/bin/sh
467 exit 1
468 """
469
470 commit_msg_success = """#!/bin/sh
471 exit 0
472 """
473
474 repo_dir = os.path.join(tempfile.mkdtemp())
475 r = Repo.init(repo_dir)
476 self.addCleanup(shutil.rmtree, repo_dir)
477
478 commit_msg = os.path.join(r.controldir(), 'hooks', 'commit-msg')
479
480 f = open(commit_msg, 'wb')
481 try:
482 f.write(commit_msg_fail)
483 finally:
484 f.close()
485 os.chmod(commit_msg, stat.S_IREAD | stat.S_IWRITE | stat.S_IEXEC)
486
487 self.assertRaises(errors.CommitError, r.do_commit, 'failed commit',
488 committer='Test Committer <test@nodomain.com>',
489 author='Test Author <test@nodomain.com>',
490 commit_timestamp=12345, commit_timezone=0,
491 author_timestamp=12345, author_timezone=0)
492
493 f = open(commit_msg, 'wb')
494 try:
495 f.write(commit_msg_success)
496 finally:
497 f.close()
498 os.chmod(commit_msg, stat.S_IREAD | stat.S_IWRITE | stat.S_IEXEC)
499
500 commit_sha = r.do_commit(
501 'empty commit',
502 committer='Test Committer <test@nodomain.com>',
503 author='Test Author <test@nodomain.com>',
504 commit_timestamp=12395, commit_timezone=0,
505 author_timestamp=12395, author_timezone=0)
506 self.assertEqual([], r[commit_sha].parents)
507
508 def test_shell_hook_post_commit(self):
509 if os.name != 'posix':
510 self.skipTest('shell hook tests requires POSIX shell')
511
512 repo_dir = os.path.join(tempfile.mkdtemp())
513 r = Repo.init(repo_dir)
514 self.addCleanup(shutil.rmtree, repo_dir)
515
516 (fd, path) = tempfile.mkstemp(dir=repo_dir)
517 post_commit_msg = """#!/bin/sh
518 unlink %(file)s
519 """ % {'file': path}
520
521 root_sha = r.do_commit(
522 'empty commit',
523 committer='Test Committer <test@nodomain.com>',
524 author='Test Author <test@nodomain.com>',
525 commit_timestamp=12345, commit_timezone=0,
526 author_timestamp=12345, author_timezone=0)
527 self.assertEqual([], r[root_sha].parents)
528
529 post_commit = os.path.join(r.controldir(), 'hooks', 'post-commit')
530
531 f = open(post_commit, 'wb')
532 try:
533 f.write(post_commit_msg)
534 finally:
535 f.close()
536 os.chmod(post_commit, stat.S_IREAD | stat.S_IWRITE | stat.S_IEXEC)
537
538 commit_sha = r.do_commit(
539 'empty commit',
540 committer='Test Committer <test@nodomain.com>',
541 author='Test Author <test@nodomain.com>',
542 commit_timestamp=12345, commit_timezone=0,
543 author_timestamp=12345, author_timezone=0)
544 self.assertEqual([root_sha], r[commit_sha].parents)
545
546 self.assertFalse(os.path.exists(path))
547
548 post_commit_msg_fail = """#!/bin/sh
549 exit 1
550 """
551 f = open(post_commit, 'wb')
552 try:
553 f.write(post_commit_msg_fail)
554 finally:
555 f.close()
556 os.chmod(post_commit, stat.S_IREAD | stat.S_IWRITE | stat.S_IEXEC)
557
558 warnings.simplefilter("always", UserWarning)
559 self.addCleanup(warnings.resetwarnings)
560 warnings_list = setup_warning_catcher()
561
562 commit_sha2 = r.do_commit(
563 'empty commit',
564 committer='Test Committer <test@nodomain.com>',
565 author='Test Author <test@nodomain.com>',
566 commit_timestamp=12345, commit_timezone=0,
567 author_timestamp=12345, author_timezone=0)
568 self.assertEqual(len(warnings_list), 1)
569 self.assertIsInstance(warnings_list[-1], UserWarning)
570 self.assertTrue("post-commit hook failed: " in str(warnings_list[-1]))
571 self.assertEqual([commit_sha], r[commit_sha2].parents)
413572
414573
415574 class BuildRepoTests(TestCase):
2525 import tempfile
2626 import time
2727 import types
28 import warnings
2829
2930 from dulwich.index import (
3031 commit_tree,
309310 commits.append(commit_obj)
310311
311312 return commits
313
314
315 def setup_warning_catcher():
316 """Wrap warnings.showwarning with code that records warnings."""
317
318 caught_warnings = []
319 original_showwarning = warnings.showwarning
320
321 def custom_showwarning(*args, **kwargs):
322 caught_warnings.append(args[0])
323
324 warnings.showwarning = custom_showwarning
325 return caught_warnings
397397
398398
399399 # The reference server implementation is based on wsgiref, which is not
400 # distributed with python 2.4. If wsgiref is not present, users will not be able
401 # to use the HTTP server without a little extra work.
400 # distributed with python 2.4. If wsgiref is not present, users will not be
401 # able to use the HTTP server without a little extra work.
402402 try:
403403 from wsgiref.simple_server import (
404404 WSGIRequestHandler,
405 ServerHandler,
406 WSGIServer,
405407 make_server,
406 )
407
408 class HTTPGitRequestHandler(WSGIRequestHandler):
409 """Handler that uses dulwich's logger for logging exceptions."""
410
408 )
409 class ServerHandlerLogger(ServerHandler):
410 """ServerHandler that uses dulwich's logger for logging exceptions."""
411
411412 def log_exception(self, exc_info):
412413 logger.exception('Exception happened during processing of request',
413414 exc_info=exc_info)
418419 def log_error(self, *args):
419420 logger.error(*args)
420421
422 class WSGIRequestHandlerLogger(WSGIRequestHandler):
423 """WSGIRequestHandler that uses dulwich's logger for logging exceptions."""
424
425 def log_exception(self, exc_info):
426 logger.exception('Exception happened during processing of request',
427 exc_info=exc_info)
428
429 def log_message(self, format, *args):
430 logger.info(format, *args)
431
432 def log_error(self, *args):
433 logger.error(*args)
434
435 def handle(self):
436 """Handle a single HTTP request"""
437
438 self.raw_requestline = self.rfile.readline()
439 if not self.parse_request(): # An error code has been sent, just exit
440 return
441
442 handler = ServerHandlerLogger(
443 self.rfile, self.wfile, self.get_stderr(), self.get_environ()
444 )
445 handler.request_handler = self # backpointer for logging
446 handler.run(self.server.get_app())
447
448 class WSGIServerLogger(WSGIServer):
449 def handle_error(self, request, client_address):
450 """Handle an error. """
451 logger.exception('Exception happened during processing of request from %s' % str(client_address))
421452
422453 def main(argv=sys.argv):
423454 """Entry point for starting an HTTP git server."""
427458 gitdir = os.getcwd()
428459
429460 # TODO: allow serving on other addresses/ports via command-line flag
430 listen_addr=''
461 listen_addr = ''
431462 port = 8000
432463
433464 log_utils.default_logging_config()
434465 backend = DictBackend({'/': Repo(gitdir)})
435466 app = make_wsgi_chain(backend)
436467 server = make_server(listen_addr, port, app,
437 handler_class=HTTPGitRequestHandler)
468 handler_class=WSGIRequestHandlerLogger,
469 server_class=WSGIServerLogger)
438470 logger.info('Listening for HTTP connections on %s:%d', listen_addr,
439471 port)
440472 server.serve_forever()
441473
442474 except ImportError:
443 # No wsgiref found; don't provide the reference functionality, but leave the
444 # rest of the WSGI-based implementation.
475 # No wsgiref found; don't provide the reference functionality, but leave
476 # the rest of the WSGI-based implementation.
445477 def main(argv=sys.argv):
446478 """Stub entry point for failing to start a server without wsgiref."""
447 sys.stderr.write('Sorry, the wsgiref module is required for dul-web.\n')
479 sys.stderr.write(
480 'Sorry, the wsgiref module is required for dul-web.\n')
448481 sys.exit(1)
99 has_setuptools = False
1010 from distutils.core import Distribution
1111
12 dulwich_version_string = '0.8.6'
12 dulwich_version_string = '0.9.0'
1313
1414 include_dirs = []
1515 # Windows MSVC support
2929 return not self.pure and not '__pypy__' in sys.modules
3030
3131 global_options = Distribution.global_options + [
32 ('pure', None,
33 "use pure Python code instead of C extensions (slower on CPython)")]
32 ('pure', None, "use pure Python code instead of C "
33 "extensions (slower on CPython)")]
3434
3535 pure = False
3636
4444 out, err = p.communicate()
4545 for l in out.splitlines():
4646 # Also parse only first digit, because 3.2.1 can't be parsed nicely
47 if (l.startswith('Xcode') and
48 int(l.split()[1].split('.')[0]) >= 4):
47 if l.startswith('Xcode') and int(l.split()[1].split('.')[0]) >= 4:
4948 os.environ['ARCHFLAGS'] = ''
5049
5150 setup_kwargs = {}
5857 keywords='git',
5958 version=dulwich_version_string,
6059 url='http://samba.org/~jelmer/dulwich',
61 download_url='http://samba.org/~jelmer/dulwich/dulwich-%s.tar.gz' % dulwich_version_string,
60 download_url='http://samba.org/~jelmer/dulwich/'
61 'dulwich-%s.tar.gz' % dulwich_version_string,
6262 license='GPLv2 or later',
6363 author='Jelmer Vernooij',
6464 author_email='jelmer@samba.org',
7272 """,
7373 packages=['dulwich', 'dulwich.tests'],
7474 scripts=['bin/dulwich', 'bin/dul-daemon', 'bin/dul-web'],
75 ext_modules = [
75 ext_modules=[
7676 Extension('dulwich._objects', ['dulwich/_objects.c'],
7777 include_dirs=include_dirs),
7878 Extension('dulwich._pack', ['dulwich/_pack.c'],
7979 include_dirs=include_dirs),
8080 Extension('dulwich._diff_tree', ['dulwich/_diff_tree.c'],
8181 include_dirs=include_dirs),
82 ],
82 ],
8383 distclass=DulwichDistribution,
8484 **setup_kwargs
8585 )