GitRepository: Add clean() method
[tools/git-buildpackage.git] / gbp / git / repository.py
1 # vim: set fileencoding=utf-8 :
2 #
3 # (C) 2006,2007,2008,2011,2013 Guido Guenther <agx@sigxcpu.org>
4 #    This program is free software; you can redistribute it and/or modify
5 #    it under the terms of the GNU General Public License as published by
6 #    the Free Software Foundation; either version 2 of the License, or
7 #    (at your option) any later version.
8 #
9 #    This program is distributed in the hope that it will be useful,
10 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
11 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 #    GNU General Public License for more details.
13 #
14 #    You should have received a copy of the GNU General Public License
15 #    along with this program; if not, write to the Free Software
16 #    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
17 """A Git repository"""
18
19 import subprocess
20 import os.path
21 import re
22 from collections import defaultdict
23
24 import gbp.log as log
25 from gbp.errors import GbpError
26 from gbp.git.modifier import GitModifier
27 from gbp.git.commit import GitCommit
28 from gbp.git.errors import GitError
29 from gbp.git.args import GitArgs
30
31
32 class GitRepositoryError(GitError):
33     """Exception thrown by L{GitRepository}"""
34     pass
35
36
37 class GitRepository(object):
38     """
39     Represents a git repository at I{path}. It's currently assumed that the git
40     repository is stored in a directory named I{.git/} below I{path}.
41
42     @ivar _path: The path to the working tree
43     @type _path: C{str}
44     @ivar _bare: Whether this is a bare repository
45     @type _bare: C{bool}
46     @raises GitRepositoryError: on git errors GitRepositoryError is raised by
47         all methods.
48     """
49
50     def _check_bare(self):
51         """Check whether this is a bare repository"""
52         out, dummy, ret = self._git_inout('rev-parse', ['--is-bare-repository'],
53                                           capture_stderr=True)
54         if ret:
55             raise GitRepositoryError(
56                 "Failed to get repository state at '%s'" % self.path)
57         self._bare = False if out.strip() != 'true' else True
58         self._git_dir = '' if self._bare else '.git'
59
60     def __init__(self, path):
61         self._path = os.path.abspath(path)
62         self._bare = False
63         try:
64             out, dummy, ret = self._git_inout('rev-parse', ['--show-cdup'],
65                                               capture_stderr=True)
66             if ret or out.strip():
67                 raise GitRepositoryError("No Git repository at '%s': '%s'" % (self.path, out))
68         except GitRepositoryError:
69             raise # We already have a useful error message
70         except:
71             raise GitRepositoryError("No Git repository at '%s'" % self.path)
72         self._check_bare()
73
74     @staticmethod
75     def __build_env(extra_env):
76         """Prepare environment for subprocess calls"""
77         env = None
78         if extra_env is not None:
79             env = os.environ.copy()
80             env.update(extra_env)
81         return env
82
83     def _git_getoutput(self, command, args=[], extra_env=None, cwd=None):
84         """
85         Run a git command and return the output
86
87         @param command: git command to run
88         @type command: C{str}
89         @param args: list of arguments
90         @type args: C{list}
91         @param extra_env: extra environment variables to pass
92         @type extra_env: C{dict}
93         @param cwd: directory to swith to when running the command, defaults to I{self.path}
94         @type cwd: C{str}
95         @return: stdout, return code
96         @rtype: C{tuple} of C{list} of C{str} and C{int}
97
98         @deprecated: use L{gbp.git.repository.GitRepository._git_inout} instead.
99         """
100         output = []
101
102         if not cwd:
103             cwd = self.path
104
105         env = self.__build_env(extra_env)
106         cmd = ['git', command] + args
107         log.debug(cmd)
108         popen = subprocess.Popen(cmd, stdout=subprocess.PIPE, env=env, cwd=cwd)
109         while popen.poll() == None:
110             output += popen.stdout.readlines()
111         output += popen.stdout.readlines()
112         return output, popen.returncode
113
114     def _git_inout(self, command, args, input=None, extra_env=None, cwd=None,
115                    capture_stderr=False):
116         """
117         Run a git command with input and return output
118
119         @param command: git command to run
120         @type command: C{str}
121         @param input: input to pipe to command
122         @type input: C{str}
123         @param args: list of arguments
124         @type args: C{list}
125         @param extra_env: extra environment variables to pass
126         @type extra_env: C{dict}
127         @param capture_stderr: whether to capture stderr
128         @type capture_stderr: C{bool}
129         @return: stdout, stderr, return code
130         @rtype: C{tuple} of C{str}, C{str}, C{int}
131         """
132         if not cwd:
133             cwd = self.path
134         return self.__git_inout(command, args, input, extra_env, cwd, capture_stderr)
135
136     @classmethod
137     def __git_inout(cls, command, args, input, extra_env, cwd, capture_stderr):
138         """
139         As _git_inout but can be used without an instance
140         """
141         cmd = ['git', command] + args
142         env = cls.__build_env(extra_env)
143         stderr_arg = subprocess.PIPE if capture_stderr else None
144         stdin_arg = subprocess.PIPE if input else None
145
146         log.debug(cmd)
147         popen = subprocess.Popen(cmd,
148                                  stdin=stdin_arg,
149                                  stdout=subprocess.PIPE,
150                                  stderr=stderr_arg,
151                                  env=env,
152                                  close_fds=True,
153                                  cwd=cwd)
154         (stdout, stderr) = popen.communicate(input)
155         return stdout, stderr, popen.returncode
156
157     def _git_command(self, command, args=[], extra_env=None):
158         """
159         Execute git command with arguments args and environment env
160         at path.
161
162         @param command: git command
163         @type command: C{str}
164         @param args: command line arguments
165         @type args: C{list}
166         @param extra_env: extra environment variables to set when running command
167         @type extra_env: C{dict}
168         """
169         try:
170             stdout, stderr, ret = self._git_inout(command=command,
171                                                   args=args,
172                                                   input=None,
173                                                   extra_env=extra_env,
174                                                   capture_stderr=True)
175         except Exception as excobj:
176             raise GitRepositoryError("Error running git %s: %s" % (command, excobj))
177         if ret:
178             raise GitRepositoryError("Error running git %s: %s" % (command, stderr))
179
180
181     def _cmd_has_feature(self, command, feature):
182         """
183         Check if the git command has certain feature enabled.
184
185         @param command: git command
186         @type command: C{str}
187         @param feature: feature / command option to check
188         @type feature: C{str}
189         @return: True if feature is supported
190         @rtype: C{bool}
191         """
192         args = GitArgs(command, '-m')
193         help, stderr, ret = self._git_inout('help',
194                                            args.args,
195                                            extra_env={'LC_ALL': 'C'},
196                                            capture_stderr=True)
197         if ret:
198             raise GitRepositoryError("Invalid git command '%s': %s"
199                                      % (command, stderr[:-1]))
200
201         # Parse git command man page
202         section_re = re.compile(r'^(?P<section>[A-Z].*)')
203         option_re = re.compile(r'--?(?P<name>[a-zA-Z\-]+).*')
204         optopt_re = re.compile(r'--\[(?P<prefix>[a-zA-Z\-]+)\]-?')
205         man_section = None
206         for line in help.splitlines():
207             if man_section == "OPTIONS" and line.startswith('       -'):
208                 opts = line.split(',')
209                 for opt in opts:
210                     opt = opt.strip()
211                     match = optopt_re.match(opt)
212                     if match:
213                         opts.append(re.sub(optopt_re, '--', opt))
214                         prefix = match.group('prefix').strip('-')
215                         opt = re.sub(optopt_re, '--%s-' % prefix, opt)
216                     match = option_re.match(opt)
217                     if match and match.group('name') == feature:
218                         return True
219             # Check man section
220             match = section_re.match(line)
221             if match:
222                 man_section = match.group('section')
223         return False
224
225     @property
226     def path(self):
227         """The absolute path to the repository"""
228         return self._path
229
230     @property
231     def git_dir(self):
232         """The absolute path to git's metadata"""
233         return os.path.join(self.path, self._git_dir)
234
235     @property
236     def bare(self):
237         """Wheter this is a bare repository"""
238         return self._bare
239
240     @property
241     def tags(self):
242         """List of all tags in the repository"""
243         return self.get_tags()
244
245     @property
246     def branch(self):
247         """The currently checked out branch"""
248         try:
249             return self.get_branch()
250         except GitRepositoryError:
251             return None
252
253     @property
254     def head(self):
255         """return the SHA1 of the current HEAD"""
256         return self.rev_parse('HEAD')
257
258 #{ Branches and Merging
259     def rename_branch(self, branch, newbranch):
260         """
261         Rename branch
262
263         @param branch: name of the branch to be renamed
264         @param newbranch: new name of the branch
265         """
266         args = GitArgs("-m", branch, newbranch)
267         self._git_command("branch", args.args)
268
269     def create_branch(self, branch, rev=None):
270         """
271         Create a new branch
272
273         @param branch: the branch's name
274         @param rev: where to start the branch from
275
276         If rev is None the branch starts form the current HEAD.
277         """
278         args = GitArgs(branch)
279         args.add_true(rev, rev)
280         self._git_command("branch", args.args)
281
282     def delete_branch(self, branch, remote=False):
283         """
284         Delete branch I{branch}
285
286         @param branch: name of the branch to delete
287         @type branch: C{str}
288         @param remote: delete a remote branch
289         @param remote: C{bool}
290         """
291         args = GitArgs('-D')
292         args.add_true(remote, '-r')
293         args.add(branch)
294
295         if self.branch != branch:
296             self._git_command("branch", args.args)
297         else:
298             raise GitRepositoryError("Can't delete the branch you're on")
299
300     def get_branch(self):
301         """
302         On what branch is the current working copy
303
304         @return: current branch or C{None} in an empty repo
305         @rtype: C{str}
306         @raises GitRepositoryError: if HEAD is not a symbolic ref
307           (e.g. when in detached HEAD state)
308         """
309         out, dummy, ret = self._git_inout('symbolic-ref', [ 'HEAD' ],
310                                            capture_stderr=True)
311         if ret:
312             # We don't append stderr since
313             # "fatal: ref HEAD is not a symbolic ref" confuses people
314             raise GitRepositoryError("Currently not on a branch")
315         ref = out.split('\n')[0]
316
317         # Check if ref really exists
318         try:
319             self._git_command('show-ref', [ ref ])
320             branch = ref[11:] # strip /refs/heads
321         except GitRepositoryError:
322             branch = None  # empty repo
323         return branch
324
325
326     def has_branch(self, branch, remote=False):
327         """
328         Check if the repository has branch named I{branch}.
329
330         @param branch: branch to look for
331         @param remote: only look for remote branches
332         @type remote: C{bool}
333         @return: C{True} if the repository has this branch, C{False} otherwise
334         @rtype: C{bool}
335         """
336         if remote:
337             ref = 'refs/remotes/%s' % branch
338         else:
339             ref = 'refs/heads/%s' % branch
340         try:
341             self._git_command('show-ref', [ ref ])
342         except GitRepositoryError:
343             return False
344         return True
345
346     def set_branch(self, branch):
347         """
348         Switch to branch I{branch}
349
350         @param branch: name of the branch to switch to
351         @type branch: C{str}
352         """
353         if self.branch == branch:
354             return
355
356         if self.bare:
357             self._git_command("symbolic-ref",
358                               [ 'HEAD', 'refs/heads/%s' % branch ])
359         else:
360             self._git_command("checkout", [ branch ])
361
362     def get_merge_branch(self, branch):
363         """
364         Get the branch we'd merge from
365
366         @return: repo and branch we would merge from
367         @rtype: C{str}
368         """
369         try:
370             remote = self.get_config("branch.%s.remote" % branch)
371             merge = self.get_config("branch.%s.merge" % branch)
372         except KeyError:
373             return None
374         remote += merge.replace("refs/heads","", 1)
375         return remote
376
377     def get_merge_base(self, commit1, commit2):
378         """
379         Get the common ancestor between two commits
380
381         @param commit1: commit SHA1 or name of a branch or tag
382         @type commit1: C{str}
383         @param commit2: commit SHA1 or name of a branch or tag
384         @type commit2: C{str}
385         @return: SHA1 of the common ancestor
386         @rtype: C{str}
387         """
388         args = GitArgs()
389         args.add(commit1)
390         args.add(commit2)
391         sha1, stderr, ret = self._git_inout('merge-base', args.args, capture_stderr=True)
392         if not ret:
393             return self.strip_sha1(sha1)
394         else:
395             raise GitRepositoryError("Failed to get common ancestor: %s" % stderr.strip())
396
397     def merge(self, commit, verbose=False, edit=False):
398         """
399         Merge changes from the named commit into the current branch
400
401         @param commit: the commit to merge from (usually a branch name or tag)
402         @type commit: C{str}
403         @param verbose: whether to print a summary after the merge
404         @type verbose: C{bool}
405         @param edit: wheter to invoke an editor to edit the merge message
406         @type edit: C{bool}
407         """
408         args = GitArgs()
409         args.add_cond(verbose, '--summary', '--no-summary')
410         if (self._cmd_has_feature('merge', 'edit')):
411             args.add_cond(edit, '--edit', '--no-edit')
412         else:
413             log.debug("Your git suite doesn't support --edit/--no-edit "
414                       "option for git-merge ")
415         args.add(commit)
416         self._git_command("merge", args.args)
417
418     def is_fast_forward(self, from_branch, to_branch):
419         """
420         Check if an update I{from from_branch} to I{to_branch} would be a fast
421         forward or if the branch is up to date already.
422
423         @return: can_fast_forward, up_to_date
424         @rtype: C{tuple}
425         """
426         has_local = False       # local repo has new commits
427         has_remote = False      # remote repo has new commits
428         out = self._git_getoutput('rev-list', ["--left-right",
429                                    "%s...%s" % (from_branch, to_branch),
430                                    "--"])[0]
431
432         if not out: # both branches have the same commits
433             return True, True
434
435         for line in out:
436             if line.startswith("<"):
437                 has_local = True
438             elif line.startswith(">"):
439                 has_remote = True
440
441         if has_local and has_remote:
442             return False, False
443         elif has_local:
444             return False, True
445         elif has_remote:
446             return True, False
447
448     def _get_branches(self, remote=False):
449         """
450         Get a list of branches
451
452         @param remote: whether to list local or remote branches
453         @type remote: C{bool}
454         @return: local or remote branches
455         @rtype: C{list}
456         """
457         args = [ '--format=%(refname:short)' ]
458         args += [ 'refs/remotes/' ] if remote else [ 'refs/heads/' ]
459         out = self._git_getoutput('for-each-ref', args)[0]
460         return [ ref.strip() for ref in out ]
461
462     def get_local_branches(self):
463         """
464         Get a list of local branches
465
466         @return: local branches
467         @rtype: C{list}
468         """
469         return self._get_branches(remote=False)
470
471
472     def get_remote_branches(self):
473         """
474         Get a list of remote branches
475
476         @return: remote branches
477         @rtype: C{list}
478         """
479         return self._get_branches(remote=True)
480
481     def update_ref(self, ref, new, old=None, msg=None):
482         """
483         Update ref I{ref} to commit I{new} if I{ref} currently points to
484         I{old}
485
486         @param ref: the ref to update
487         @type ref: C{str}
488         @param new: the new value for ref
489         @type new: C{str}
490         @param old: the old value of ref
491         @type old: C{str}
492         @param msg: the reason for the update
493         @type msg: C{str}
494         """
495         args = [ ref, new ]
496         if old:
497             args += [ old ]
498         if msg:
499             args = [ '-m', msg ] + args
500         self._git_command("update-ref", args)
501
502     def branch_contains(self, branch, commit, remote=False):
503         """
504         Check if branch I{branch} contains commit I{commit}
505
506         @param branch: the branch the commit should be on
507         @type branch: C{str}
508         @param commit: the C{str} commit to check
509         @type commit: C{str}
510         @param remote: whether to check remote instead of local branches
511         @type remote: C{bool}
512         """
513         args = GitArgs()
514         args.add_true(remote, '-r')
515         args.add('--contains')
516         args.add(commit)
517
518         out, ret =  self._git_getoutput('branch', args.args)
519         for line in out:
520             # remove prefix '*' for current branch before comparing
521             line = line.replace('*', '')
522             if line.strip() == branch:
523                 return True
524         return False
525
526     def set_upstream_branch(self, local_branch, upstream):
527         """
528         Set upstream branches for local branch
529
530         @param local_branch: name of the local branch
531         @type local_branch: C{str}
532         @param upstream: remote/branch, for example origin/master
533         @type upstream: C{str}
534         """
535
536         # check if both branches exist
537         for branch, remote in [(local_branch, False), (upstream, True)]:
538             if not self.has_branch(branch, remote=remote):
539                 raise GitRepositoryError("Branch %s doesn't exist!" % branch)
540
541         if self._cmd_has_feature('branch', 'set-upstream-to'):
542             args = ['--set-upstream-to=%s' % upstream, local_branch]
543         else:
544             args = ["--set-upstream", local_branch, upstream]
545
546         dummy, err, ret = self._git_inout('branch',
547                                           args,
548                                           capture_stderr=True)
549         if ret:
550             raise GitRepositoryError(
551                 "Failed to set upstream branch '%s' for '%s': %s" %
552                 (upstream, local_branch, err.strip()))
553
554     def get_upstream_branch(self, local_branch):
555         """
556         Get upstream branch for the local branch
557
558         @param local_branch: name fo the local branch
559         @type local_branch: C{str}
560         @return: upstream (remote/branch) or  '' if no upstream found
561         @rtype: C{str}
562
563         """
564         args = GitArgs('--format=%(upstream:short)')
565         if self.has_branch(local_branch, remote=False):
566             args.add('refs/heads/%s' % local_branch)
567         else:
568             raise GitRepositoryError("Branch %s doesn't exist!" % local_branch)
569
570         out = self._git_getoutput('for-each-ref', args.args)[0]
571
572         return out[0].strip()
573
574 #{ Tags
575
576     def create_tag(self, name, msg=None, commit=None, sign=False, keyid=None):
577         """
578         Create a new tag.
579
580         @param name: the tag's name
581         @type name: C{str}
582         @param msg: The tag message.
583         @type msg: C{str}
584         @param commit: the commit or object to create the tag at, default
585             is I{HEAD}
586         @type commit: C{str}
587         @param sign: Whether to sing the tag
588         @type sign: C{bool}
589         @param keyid: the GPG keyid used to sign the tag
590         @type keyid: C{str}
591         """
592         args = []
593         args += [ '-m', msg ] if msg else []
594         if sign:
595             args += [ '-s' ]
596             args += [ '-u', keyid ] if keyid else []
597         args += [ name ]
598         args += [ commit ] if commit else []
599         self._git_command("tag", args)
600
601     def delete_tag(self, tag):
602         """
603         Delete a tag named I{tag}
604
605         @param tag: the tag to delete
606         @type tag: C{str}
607         """
608         if self.has_tag(tag):
609             self._git_command("tag", [ "-d", tag ])
610
611     def move_tag(self, old, new):
612         self._git_command("tag", [ new, old ])
613         self.delete_tag(old)
614
615     def has_tag(self, tag):
616         """
617         Check if the repository has a tag named I{tag}.
618
619         @param tag: tag to look for
620         @type tag: C{str}
621         @return: C{True} if the repository has that tag, C{False} otherwise
622         @rtype: C{bool}
623         """
624         out, ret = self._git_getoutput('tag', [ '-l', tag ])
625         return [ False, True ][len(out)]
626
627     def describe(self, commitish, pattern=None, longfmt=False, always=False,
628                  abbrev=None, tags=False, exact_match=False):
629         """
630         Describe commit, relative to the latest tag reachable from it.
631
632         @param commitish: the commit-ish to describe
633         @type commitish: C{str}
634         @param pattern: only look for tags matching I{pattern}
635         @type pattern: C{str}
636         @param longfmt: describe the commit in the long format
637         @type longfmt: C{bool}
638         @param always: return commit sha1 as fallback if no tag is found
639         @type always: C{bool}
640         @param abbrev: abbreviate sha1 to given length instead of the default
641         @type abbrev: None or C{long}
642         @param tags: enable matching a lightweight (non-annotated) tag
643         @type tags: C{bool}
644         @param exact_match: only output exact matches (a tag directly
645         references the supplied commit)
646         @type exact_match: C{bool}
647         @return: tag name plus/or the abbreviated sha1
648         @rtype: C{str}
649         """
650         args = GitArgs()
651         args.add_true(pattern, ['--match' , pattern])
652         args.add_true(longfmt, '--long')
653         # 'long' and 'abbrev=0' are incompatible, behave similar to
654         # 'always' and 'abbrev=0'
655         if longfmt and abbrev == 0:
656             args.add('--abbrev=40')
657         elif abbrev is not None:
658             args.add('--abbrev=%s' % abbrev)
659         args.add_true(always, '--always')
660         args.add_true(tags, '--tags')
661         args.add_true(exact_match, '--exact-match')
662         args.add(commitish)
663
664         tag, err, ret = self._git_inout('describe', args.args,
665                                         capture_stderr=True)
666         if ret:
667             raise GitRepositoryError("Can't describe %s. Git error: %s" % \
668                                          (commitish, err.strip()))
669         return tag.strip()
670
671     def find_tag(self, commit, pattern=None):
672         """
673         Find the closest tag to a given commit
674
675         @param commit: the commit to describe
676         @type commit: C{str}
677         @param pattern: only look for tags matching I{pattern}
678         @type pattern: C{str}
679         @return: the found tag
680         @rtype: C{str}
681         """
682         return self.describe(commit, pattern, abbrev=0)
683
684     def get_tags(self, pattern=None):
685         """
686         List tags
687
688         @param pattern: only list tags matching I{pattern}
689         @type pattern: C{str}
690         @return: tags
691         @rtype: C{list} of C{str}
692         """
693         args = [ '-l', pattern ] if pattern else []
694         return [ line.strip() for line in self._git_getoutput('tag', args)[0] ]
695
696     def verify_tag(self, tag):
697         """
698         Verify a signed tag
699
700         @param tag: the tag's name
701         @type tag: C{str}
702         @return: Whether the signature on the tag could be verified
703         @rtype: C{bool}
704         """
705         args = GitArgs('-v', tag)
706
707         try:
708             self._git_command('tag', args.args)
709         except GitRepositoryError:
710             return False
711         return True
712
713 #}
714     def force_head(self, commit, hard=False):
715         """
716         Force HEAD to a specific commit
717
718         @param commit: commit to move HEAD to
719         @param hard: also update the working copy
720         @type hard: C{bool}
721         """
722         if not GitCommit.is_sha1(commit):
723             commit = self.rev_parse(commit)
724
725         if self.bare:
726             ref = "refs/heads/%s" % self.get_branch()
727             self._git_command("update-ref", [ ref, commit ])
728         else:
729             args = ['--quiet']
730             if hard:
731                 args += [ '--hard' ]
732             args += [ commit, '--' ]
733             self._git_command("reset", args)
734
735     def is_clean(self, ignore_untracked=False):
736         """
737         Does the repository contain any uncommitted modifications?
738
739         @param ignore_untracked: whether to ignore untracked files when
740             checking the repository status
741         @type ignore_untracked: C{bool}
742         @return: C{True} if the repository is clean, C{False} otherwise
743             and Git's status message
744         @rtype: C{tuple}
745         """
746         if self.bare:
747             return (True, '')
748
749         clean_msg = 'nothing to commit'
750
751         args = GitArgs()
752         args.add_true(ignore_untracked, '-uno')
753
754         out, ret = self._git_getoutput('status',
755                                        args.args,
756                                        extra_env={'LC_ALL': 'C'})
757         if ret:
758             raise GbpError("Can't get repository status")
759         ret = False
760         for line in out:
761             if line.startswith('#'):
762                 continue
763             if line.startswith(clean_msg):
764                 ret = True
765             break
766         return (ret, "".join(out))
767
768     def clean(self, directories=False, force=False, dry_run=False):
769         """
770         Remove untracked files from the working tree.
771
772         @param directories: remove untracked directories, too
773         @type directories: C{bool}
774         @param force: satisfy git configuration variable clean.requireForce
775         @type force: C{bool}
776         @param dry_run: don’t actually remove anything
777         @type dry_run: C{bool}
778         """
779         options = GitArgs()
780         options.add_true(directories, '-d')
781         options.add_true(force, '-f')
782         options.add_true(dry_run, '-n')
783
784         _out, err, ret = self._git_inout('clean', options.args,
785                                          extra_env={'LC_ALL': 'C'})
786         if ret:
787             raise GitRepositoryError("Can't execute repository clean: %s" % err)
788
789     def is_empty(self):
790         """
791         Is the repository empty?
792
793         @return: True if the repositorydoesn't have any commits,
794             False otherwise
795         @rtype: C{bool}
796         """
797         # an empty repo has no branches:
798         return False if self.branch else True
799
800     def rev_parse(self, name, short=0):
801         """
802         Find the SHA1 of a given name
803
804         @param name: the name to look for
805         @type name: C{str}
806         @param short:  try to abbreviate SHA1 to given length
807         @type short: C{int}
808         @return: the name's sha1
809         @rtype: C{str}
810         """
811         args = GitArgs("--quiet", "--verify")
812         args.add_cond(short, '--short=%d' % short)
813         args.add(name)
814         sha, ret = self._git_getoutput('rev-parse', args.args)
815         if ret:
816             raise GitRepositoryError("revision '%s' not found" % name)
817         return self.strip_sha1(sha[0], short)
818
819     @staticmethod
820     def strip_sha1(sha1, length=0):
821         """
822         Strip a given sha1 and check if the resulting
823         hash has the expected length.
824
825         >>> GitRepository.strip_sha1('  58ef37dbeb12c44b206b92f746385a6f61253c0a\\n')
826         '58ef37dbeb12c44b206b92f746385a6f61253c0a'
827         >>> GitRepository.strip_sha1('58ef37d', 10)
828         Traceback (most recent call last):
829         ...
830         GitRepositoryError: '58ef37d' is not a valid sha1 of length 10
831         >>> GitRepository.strip_sha1('58ef37d', 7)
832         '58ef37d'
833         >>> GitRepository.strip_sha1('123456789', 7)
834         '123456789'
835         >>> GitRepository.strip_sha1('foobar')
836         Traceback (most recent call last):
837         ...
838         GitRepositoryError: 'foobar' is not a valid sha1
839         """
840         maxlen = 40
841         s = sha1.strip()
842
843         l = length or maxlen
844
845         if len(s) < l or len(s) > maxlen:
846             raise GitRepositoryError("'%s' is not a valid sha1%s" %
847                                      (s, " of length %d" % l if length else ""))
848         return s
849
850 #{ Trees
851     def checkout(self, treeish):
852         """
853         Checkout treeish
854
855         @param treeish: the treeish to check out
856         @type treeish: C{str}
857         """
858         self._git_command("checkout", ["--quiet", treeish])
859
860     def has_treeish(self, treeish):
861         """
862         Check if the repository has the treeish object I{treeish}.
863
864         @param treeish: treeish object to look for
865         @type treeish: C{str}
866         @return: C{True} if the repository has that tree, C{False} otherwise
867         @rtype: C{bool}
868         """
869         _out, _err, ret =  self._git_inout('ls-tree', [treeish],
870                                            capture_stderr=True)
871         return [ True, False ][ret != 0]
872
873     def write_tree(self, index_file=None):
874         """
875         Create a tree object from the current index
876
877         @param index_file: alternate index file to read changes from
878         @type index_file: C{str}
879         @return: the new tree object's sha1
880         @rtype: C{str}
881         """
882         if index_file:
883             extra_env = {'GIT_INDEX_FILE': index_file }
884         else:
885             extra_env = None
886
887         tree, stderr, ret = self._git_inout('write-tree', [],
888                                             extra_env=extra_env,
889                                             capture_stderr=True)
890         if ret:
891             raise GitRepositoryError("Can't write out current index: %s" % stderr[:-1])
892         return tree.strip()
893
894     def make_tree(self, contents):
895         """
896         Create a tree based on contents. I{contents} has the same format than
897         the I{GitRepository.list_tree} output.
898         """
899         out=''
900         args = GitArgs('-z')
901
902         for obj in contents:
903              mode, type, sha1, name = obj
904              out += '%s %s %s\t%s\0' % (mode, type, sha1, name)
905
906         sha1, err, ret =  self._git_inout('mktree',
907                                           args.args,
908                                           out,
909                                           capture_stderr=True)
910         if ret:
911             raise GitRepositoryError("Failed to mktree: '%s'" % err)
912         return self.strip_sha1(sha1)
913
914     def get_obj_type(self, obj):
915         """
916         Get type of a git repository object
917
918         @param obj: repository object
919         @type obj: C{str}
920         @return: type of the repository object
921         @rtype: C{str}
922         """
923         out, ret = self._git_getoutput('cat-file', args=['-t', obj])
924         if ret:
925             raise GitRepositoryError("Not a Git repository object: '%s'" % obj)
926         return out[0].strip()
927
928     def list_tree(self, treeish, recurse=False):
929         """
930         Get a trees content. It returns a list of objects that match the
931         'ls-tree' output: [ mode, type, sha1, path ].
932
933         @param treeish: the treeish object to list
934         @type treeish: C{str}
935         @param recurse: whether to list the tree recursively
936         @type recurse: C{bool}
937         @return: the tree
938         @rtype: C{list} of objects. See above.
939         """
940         args = GitArgs('-z')
941         args.add_true(recurse, '-r')
942         args.add(treeish)
943
944         out, err, ret =  self._git_inout('ls-tree', args.args, capture_stderr=True)
945         if ret:
946             raise GitRepositoryError("Failed to ls-tree '%s': '%s'" % (treeish, err))
947
948         tree = []
949         for line in out.split('\0'):
950             if line:
951                 tree.append(line.split(None, 3))
952         return tree
953
954 #}
955
956     def get_config(self, name):
957         """
958         Gets the config value associated with I{name}
959
960         @param name: config value to get
961         @return: fetched config value
962         @rtype: C{str}
963         """
964         value, ret = self._git_getoutput('config', [ name ])
965         if ret: raise KeyError
966         return value[0][:-1] # first line with \n ending removed
967
968     def get_author_info(self):
969         """
970         Determine a sane values for author name and author email from git's
971         config and environment variables.
972
973         @return: name and email
974         @rtype: L{GitModifier}
975         """
976         try:
977            name =  self.get_config("user.name")
978         except KeyError:
979            name = os.getenv("USER")
980         try:
981            email =  self.get_config("user.email")
982         except KeyError:
983             email = os.getenv("EMAIL")
984         email = os.getenv("GIT_AUTHOR_EMAIL", email)
985         name = os.getenv("GIT_AUTHOR_NAME", name)
986         return GitModifier(name, email)
987
988 #{ Remote Repositories
989
990     def get_remote_repos(self):
991         """
992         Get all remote repositories
993
994         @return: remote repositories
995         @rtype: C{list} of C{str}
996         """
997         out = self._git_getoutput('remote')[0]
998         return [ remote.strip() for remote in out ]
999
1000     def has_remote_repo(self, name):
1001         """
1002         Do we know about a remote named I{name}?
1003
1004         @param name: name of the remote repository
1005         @type name: C{str}
1006         @return: C{True} if the remote repositore is known, C{False} otherwise
1007         @rtype: C{bool}
1008         """
1009         if name in self.get_remote_repos():
1010             return True
1011         else:
1012             return False
1013
1014     def add_remote_repo(self, name, url, tags=True, fetch=False):
1015         """
1016         Add a tracked remote repository
1017
1018         @param name: the name to use for the remote
1019         @type name: C{str}
1020         @param url: the url to add
1021         @type url: C{str}
1022         @param tags: whether to fetch tags
1023         @type tags: C{bool}
1024         @param fetch: whether to fetch immediately from the remote side
1025         @type fetch: C{bool}
1026         """
1027         args = GitArgs('add')
1028         args.add_false(tags, '--no-tags')
1029         args.add_true(fetch, '--fetch')
1030         args.add(name, url)
1031         self._git_command("remote", args.args)
1032
1033     def remove_remote_repo(self, name):
1034         args = GitArgs('rm', name)
1035         self._git_command("remote", args.args)
1036
1037     def fetch(self, repo=None, tags=False, depth=0, refspec=None,
1038               all_remotes=False):
1039         """
1040         Download objects and refs from another repository.
1041
1042         @param repo: repository to fetch from
1043         @type repo: C{str}
1044         @param tags: whether to fetch all tag objects
1045         @type tags: C{bool}
1046         @param depth: deepen the history of (shallow) repository to depth I{depth}
1047         @type depth: C{int}
1048         @param refspec: refspec to use instead of the default from git config
1049         @type refspec: C{str}
1050         @param all_remotes: fetch all remotes
1051         @type all_remotes: C{bool}
1052         """
1053         args = GitArgs('--quiet')
1054         args.add_true(tags, '--tags')
1055         args.add_cond(depth, '--depth=%s' % depth)
1056         if all_remotes:
1057             args.add_true(all_remotes, '--all')
1058         else:
1059             args.add_cond(repo, repo)
1060             args.add_cond(refspec, refspec)
1061
1062         self._git_command("fetch", args.args)
1063
1064     def pull(self, repo=None, ff_only=False):
1065         """
1066         Fetch and merge from another repository
1067
1068         @param repo: repository to fetch from
1069         @type repo: C{str}
1070         @param ff_only: only merge if this results in a fast forward merge
1071         @type ff_only: C{bool}
1072         """
1073         args = []
1074         args += [ '--ff-only' ] if ff_only else []
1075         args += [ repo ] if repo else []
1076         self._git_command("pull", args)
1077
1078     def push(self, repo=None, src=None, dst=None, ff_only=True):
1079         """
1080         Push changes to the remote repo
1081
1082         @param repo: repository to push to
1083         @type repo: C{str}
1084         @param src: the source ref to push
1085         @type src: C{str}
1086         @param dst: the name of the destination ref to push to
1087         @type dst: C{str}
1088         @param ff_only: only push if it's a fast forward update
1089         @type ff_only: C{bool}
1090         """
1091         args = GitArgs()
1092         args.add_cond(repo, repo)
1093
1094         # Allow for src == '' to delete dst on the remote
1095         if src != None:
1096             refspec = src
1097             if dst:
1098                 refspec += ':%s' % dst
1099             if not ff_only:
1100                 refspec = '+%s' % refspec
1101             args.add(refspec)
1102         self._git_command("push", args.args)
1103
1104     def push_tag(self, repo, tag):
1105         """
1106         Push a tag to the remote repo
1107
1108         @param repo: repository to push to
1109         @type repo: C{str}
1110         @param tag: the name of the tag
1111         @type tag: C{str}
1112         """
1113         args = GitArgs(repo, 'tag', tag)
1114         self._git_command("push", args.args)
1115
1116 #{ Files
1117
1118     def add_files(self, paths, force=False, index_file=None, work_tree=None):
1119         """
1120         Add files to a the repository
1121
1122         @param paths: list of files to add
1123         @type paths: list or C{str}
1124         @param force: add files even if they would be ignored by .gitignore
1125         @type force: C{bool}
1126         @param index_file: alternative index file to use
1127         @param work_tree: alternative working tree to use
1128         """
1129         extra_env = {}
1130
1131         if isinstance(paths, basestring):
1132             paths = [ paths ]
1133
1134         args = [ '-f' ] if force else []
1135
1136         if index_file:
1137             extra_env['GIT_INDEX_FILE'] =  index_file
1138
1139         if work_tree:
1140             extra_env['GIT_WORK_TREE'] = work_tree
1141
1142         self._git_command("add", args + paths, extra_env)
1143
1144     def remove_files(self, paths, verbose=False):
1145         """
1146         Remove files from the repository
1147
1148         @param paths: list of files to remove
1149         @param paths: C{list} or C{str}
1150         @param verbose: be verbose
1151         @type verbose: C{bool}
1152         """
1153         if isinstance(paths, basestring):
1154             paths = [ paths ]
1155
1156         args =  [] if verbose else ['--quiet']
1157         self._git_command("rm", args + paths)
1158
1159     def list_files(self, types=['cached']):
1160         """
1161         List files in index and working tree
1162
1163         @param types: list of types to show
1164         @type types: C{list}
1165         @return: list of files
1166         @rtype: C{list} of C{str}
1167         """
1168         all_types = [ 'cached', 'deleted', 'others', 'ignored',  'stage'
1169                       'unmerged', 'killed', 'modified' ]
1170         args = [ '-z' ]
1171
1172         for t in types:
1173             if t in all_types:
1174                 args += [ '--%s' % t ]
1175             else:
1176                 raise GitRepositoryError("Unknown type '%s'" % t)
1177         out, ret = self._git_getoutput('ls-files', args)
1178         if ret:
1179             raise GitRepositoryError("Error listing files: '%d'" % ret)
1180         if out:
1181             return [ file for file in out[0].split('\0') if file ]
1182         else:
1183             return []
1184
1185
1186     def write_file(self, filename, filters=True):
1187         """
1188         Hash a single file and write it into the object database
1189
1190         @param filename: the filename to the content of the file to hash
1191         @type filename: C{str}
1192         @param filters: whether to run filters
1193         @type filters: C{bool}
1194         @return: the hash of the file
1195         @rtype: C{str}
1196         """
1197         args = GitArgs('-w', '-t', 'blob')
1198         args.add_false(filters, '--no-filters')
1199         args.add(filename)
1200
1201         sha1, stderr, ret = self._git_inout('hash-object',
1202                                             args.args,
1203                                             capture_stderr=True)
1204         if not ret:
1205             return self.strip_sha1(sha1)
1206         else:
1207             raise GbpError("Failed to hash %s: %s" % (filename, stderr))
1208 #}
1209
1210 #{ Comitting
1211
1212     def _commit(self, msg, args=[], author_info=None):
1213         extra_env = author_info.get_author_env() if author_info else None
1214         self._git_command("commit", ['-q', '-m', msg] + args, extra_env=extra_env)
1215
1216     def commit_staged(self, msg, author_info=None, edit=False):
1217         """
1218         Commit currently staged files to the repository
1219
1220         @param msg: commit message
1221         @type msg: C{str}
1222         @param author_info: authorship information
1223         @type author_info: L{GitModifier}
1224         @param edit: whether to spawn an editor to edit the commit info
1225         @type edit: C{bool}
1226         """
1227         args = GitArgs()
1228         args.add_true(edit,  '--edit')
1229         self._commit(msg=msg, args=args.args, author_info=author_info)
1230
1231     def commit_all(self, msg, author_info=None, edit=False):
1232         """
1233         Commit all changes to the repository
1234         @param msg: commit message
1235         @type msg: C{str}
1236         @param author_info: authorship information
1237         @type author_info: L{GitModifier}
1238         """
1239         args = GitArgs('-a')
1240         args.add_true(edit,  '--edit')
1241         self._commit(msg=msg, args=args.args, author_info=author_info)
1242
1243     def commit_files(self, files, msg, author_info=None):
1244         """
1245         Commit the given files to the repository
1246
1247         @param files: file or files to commit
1248         @type files: C{str} or C{list}
1249         @param msg: commit message
1250         @type msg: C{str}
1251         @param author_info: authorship information
1252         @type author_info: L{GitModifier}
1253         """
1254         if isinstance(files, basestring):
1255             files = [ files ]
1256         self._commit(msg=msg, args=files, author_info=author_info)
1257
1258     def commit_dir(self, unpack_dir, msg, branch, other_parents=None,
1259                    author={}, committer={}, create_missing_branch=False):
1260         """
1261         Replace the current tip of branch I{branch} with the contents from I{unpack_dir}
1262
1263         @param unpack_dir: content to add
1264         @type unpack_dir: C{str}
1265         @param msg: commit message to use
1266         @type msg: C{str}
1267         @param branch: branch to add the contents of unpack_dir to
1268         @type branch: C{str}
1269         @param other_parents: additional parents of this commit
1270         @type other_parents: C{list} of C{str}
1271         @param author: author information to use for commit
1272         @type author: C{dict} with keys I{name}, I{email}, I{date}
1273         @param committer: committer information to use for commit
1274         @type committer: C{dict} with keys I{name}, I{email}, I{date}
1275             or L{GitModifier}
1276         @param create_missing_branch: create I{branch} as detached branch if it
1277             doesn't already exist.
1278         @type create_missing_branch: C{bool}
1279         """
1280
1281         git_index_file = os.path.join(self.path, self._git_dir, 'gbp_index')
1282         try:
1283             os.unlink(git_index_file)
1284         except OSError:
1285             pass
1286         self.add_files('.', force=True, index_file=git_index_file,
1287                        work_tree=unpack_dir)
1288         tree = self.write_tree(git_index_file)
1289
1290         if branch:
1291             try:
1292                 cur = self.rev_parse(branch)
1293             except GitRepositoryError:
1294                 if create_missing_branch:
1295                     log.debug("Will create missing branch '%s'..." % branch)
1296                     cur = None
1297                 else:
1298                     raise
1299         else: # emtpy repo
1300             cur = None
1301             branch = 'master'
1302
1303         # Build list of parents:
1304         parents = []
1305         if cur:
1306             parents = [ cur ]
1307         if other_parents:
1308             for parent in other_parents:
1309                 sha = self.rev_parse(parent)
1310                 if sha not in parents:
1311                     parents += [ sha ]
1312
1313         commit = self.commit_tree(tree=tree, msg=msg, parents=parents,
1314                                   author=author, committer=committer)
1315         if not commit:
1316             raise GitRepositoryError("Failed to commit tree")
1317         self.update_ref("refs/heads/%s" % branch, commit, cur)
1318         return commit
1319
1320     def commit_tree(self, tree, msg, parents, author={}, committer={}):
1321         """
1322         Commit a tree with commit msg I{msg} and parents I{parents}
1323
1324         @param tree: tree to commit
1325         @param msg: commit message
1326         @param parents: parents of this commit
1327         @param author: authorship information
1328         @type author: C{dict} with keys 'name' and 'email' or L{GitModifier}
1329         @param committer: comitter information
1330         @type committer: C{dict} with keys 'name' and 'email'
1331         """
1332         extra_env = {}
1333         for key, val in author.items():
1334             if val:
1335                 extra_env['GIT_AUTHOR_%s' % key.upper()] = val
1336         for key, val in committer.items():
1337             if val:
1338                 extra_env['GIT_COMMITTER_%s' % key.upper()] = val
1339
1340         args = [ tree ]
1341         for parent in parents:
1342             args += [ '-p' , parent ]
1343         sha1, stderr, ret = self._git_inout('commit-tree',
1344                                             args,
1345                                             msg,
1346                                             extra_env,
1347                                             capture_stderr=True)
1348         if not ret:
1349             return self.strip_sha1(sha1)
1350         else:
1351             raise GbpError("Failed to commit tree: %s" % stderr)
1352
1353 #{ Commit Information
1354
1355     def get_commits(self, since=None, until=None, paths=None, num=0,
1356                     first_parent=False, options=None):
1357         """
1358         Get commits from since to until touching paths
1359
1360         @param since: commit to start from
1361         @type since: C{str}
1362         @param until: last commit to get
1363         @type until: C{str}
1364         @param paths: only list commits touching paths
1365         @type paths: C{list} of C{str}
1366         @param num: maximum number of commits to fetch
1367         @type num: C{int}
1368         @param options: list of additional options passed to git log
1369         @type  options: C{list} of C{str}ings
1370         @param first_parent: only follow first parent when seeing a
1371                              merge commit
1372         @type first_parent: C{bool}
1373         """
1374         args = GitArgs('--pretty=format:%H')
1375         args.add_true(num, '-%d' % num)
1376         args.add_true(first_parent, '--first-parent')
1377         if since:
1378             args.add("%s..%s" % (since, until or 'HEAD'))
1379         elif until:
1380             args.add(until)
1381         args.add_cond(options, options)
1382         args.add("--")
1383         if isinstance(paths, basestring):
1384             paths = [ paths ]
1385         args.add_cond(paths, paths)
1386
1387         commits, ret = self._git_getoutput('log', args.args)
1388         if ret:
1389             where = " on %s" % paths if paths else ""
1390             raise GitRepositoryError("Error getting commits %s..%s%s" %
1391                         (since, until, where))
1392         return [ commit.strip() for commit in commits ]
1393
1394     def show(self, id):
1395         """git-show id"""
1396         obj, stderr, ret = self._git_inout('show', ["--pretty=medium", id],
1397                                               capture_stderr=True)
1398         if ret:
1399             raise GitRepositoryError("can't get %s: %s" % (id, stderr.rstrip()))
1400         return obj
1401
1402     def grep_log(self, regex, since=None):
1403         """
1404         Get commmits matching I{regex}
1405
1406         @param regex: regular expression
1407         @type regex: C{str}
1408         @param since: where to start grepping (e.g. a branch)
1409         @type since: C{str}
1410         """
1411         args = ['--pretty=format:%H']
1412         args.append("--grep=%s" % regex)
1413         if since:
1414             args.append(since)
1415         args.append('--')
1416
1417         stdout, stderr, ret = self._git_inout('log', args,
1418                                               capture_stderr=True)
1419         if ret:
1420             raise GitRepositoryError("Error grepping log for %s: %s" %
1421                                      (regex, stderr[:-1]))
1422         if stdout:
1423             return [ commit.strip() for commit in stdout.split('\n')[::-1] ]
1424         else:
1425             return []
1426
1427     def get_subject(self, commit):
1428         """
1429         Gets the subject of a commit.
1430
1431         @deprecated: Use get_commit_info directly
1432
1433         @param commit: the commit to get the subject from
1434         @return: the commit's subject
1435         @rtype: C{str}
1436         """
1437         return self.get_commit_info(commit)['subject']
1438
1439     def get_commit_info(self, commitish):
1440         """
1441         Look up data of a specific commit-ish. Dereferences given commit-ish
1442         to the commit it points to.
1443
1444         @param commitish: the commit-ish to inspect
1445         @return: the commit's including id, author, email, subject and body
1446         @rtype: dict
1447         """
1448         commit_sha1 = self.rev_parse("%s^0" % commitish)
1449         args = GitArgs('--pretty=format:%an%x00%ae%x00%ad%x00%cn%x00%ce%x00%cd%x00%s%x00%f%x00%b%x00',
1450                        '-z', '--date=raw', '--name-status', commit_sha1)
1451         out, err, ret =  self._git_inout('show', args.args)
1452         if ret:
1453             raise GitRepositoryError("Unable to retrieve commit info for %s"
1454                                      % commitish)
1455
1456         fields = out.split('\x00')
1457
1458         author = GitModifier(fields[0].strip(),
1459                              fields[1].strip(),
1460                              fields[2].strip())
1461         committer = GitModifier(fields[3].strip(),
1462                                 fields[4].strip(),
1463                                 fields[5].strip())
1464
1465         files = defaultdict(list)
1466         file_fields = fields[9:]
1467         # For some reason git returns one extra empty field for merge commits
1468         if file_fields[0] == '': file_fields.pop(0)
1469         while len(file_fields) and file_fields[0] != '':
1470             status = file_fields.pop(0).strip()
1471             path = file_fields.pop(0)
1472             files[status].append(path)
1473
1474         return {'id' : commitish,
1475                 'author' : author,
1476                 'committer' : committer,
1477                 'subject' : fields[6],
1478                 'patchname' : fields[7],
1479                 'body' : fields[8],
1480                 'files' : files}
1481
1482 #{ Patches
1483     def format_patches(self, start, end, output_dir,
1484                        signature=True,
1485                        thread=None,
1486                        symmetric=True):
1487         """
1488         Output the commits between start and end as patches in output_dir.
1489
1490         This outputs the revisions I{start...end} by default. When using
1491         I{symmetric} to C{false} it uses I{start..end} instead.
1492
1493         @param start: the commit on the left side of the revision range
1494         @param end: the commit on the right hand side of the revisino range
1495         @param output_dir: directory to write the patches to
1496         @param signature: whether to output a signature
1497         @param thread: whether to include In-Reply-To references
1498         @param symmetric: whether to use the symmetric difference (see above)
1499         """
1500         options = GitArgs('-N', '-k',
1501                           '-o', output_dir)
1502         options.add_cond(not signature, '--no-signature')
1503         options.add('%s%s%s' % (start, '...' if symmetric else '..', end))
1504         options.add_cond(thread, '--thread=%s' % thread, '--no-thread')
1505
1506         output, ret = self._git_getoutput('format-patch', options.args)
1507         return [ line.strip() for line in output ]
1508
1509     def apply_patch(self, patch, index=True, context=None, strip=None):
1510         """Apply a patch using git apply"""
1511         args = []
1512         if context:
1513             args += [ '-C', context ]
1514         if index:
1515             args.append("--index")
1516         if strip != None:
1517             args += [ '-p', str(strip) ]
1518         args.append(patch)
1519         self._git_command("apply", args)
1520
1521     def diff(self, obj1, obj2=None, paths=None, stat=False, summary=False):
1522         """
1523         Diff two git repository objects
1524
1525         @param obj1: first object
1526         @type obj1: C{str}
1527         @param obj2: second object
1528         @type obj2: C{str}
1529         @param paths: List of paths to diff
1530         @type paths: C{list}
1531         @param stat: Show diffstat
1532         @type stat: C{bool} or C{int} or C{str}
1533         @param summary: Show diffstat
1534         @type summary: C{bool}
1535         @return: diff
1536         @rtype: C{str}
1537         """
1538         options = GitArgs('-p')
1539         if stat is True:
1540             options.add('--stat')
1541         elif stat:
1542             options.add('--stat=%s' % stat)
1543         options.add_true(summary, '--summary')
1544         options.add(obj1)
1545         options.add_true(obj2, obj2)
1546         if paths:
1547             options.add('--', paths)
1548         output, stderr, ret = self._git_inout('diff', options.args)
1549         if ret:
1550             raise GitRepositoryError("Git diff failed")
1551         return output
1552 #}
1553
1554     def archive(self, format, prefix, output, treeish, **kwargs):
1555         """
1556         Create an archive from a treeish
1557
1558         @param format: the type of archive to create, e.g. 'tar.gz'
1559         @type format: C{str}
1560         @param prefix: prefix to prepend to each filename in the archive
1561         @type prefix: C{str}
1562         @param output: the name of the archive to create
1563         @type output: C{str}
1564         @param treeish: the treeish to create the archive from
1565         @type treeish: C{str}
1566         @param kwargs: additional commandline options passed to git-archive
1567         """
1568         args = [ '--format=%s' % format, '--prefix=%s' % prefix,
1569                  '--output=%s' % output, treeish ]
1570         out, ret = self._git_getoutput('archive', args, **kwargs)
1571         if ret:
1572             raise GitRepositoryError("Unable to archive %s" % treeish)
1573
1574     def collect_garbage(self, auto=False):
1575         """
1576         Cleanup unnecessary files and optimize the local repository
1577
1578         param auto: only cleanup if required
1579         param auto: C{bool}
1580         """
1581         args = [ '--auto' ] if auto else []
1582         self._git_command("gc", args)
1583
1584 #{ Submodules
1585
1586     def has_submodules(self):
1587         """
1588         Does the repo have any submodules?
1589
1590         @return: C{True} if the repository has any submodules, C{False}
1591             otherwise
1592         @rtype: C{bool}
1593         """
1594         if os.path.exists(os.path.join(self.path, '.gitmodules')):
1595             return True
1596         else:
1597             return False
1598
1599
1600     def add_submodule(self, repo_path):
1601         """
1602         Add a submodule
1603
1604         @param repo_path: path to submodule
1605         @type repo_path: C{str}
1606         """
1607         self._git_command("submodule", [ "add", repo_path ])
1608
1609
1610     def update_submodules(self, init=True, recursive=True, fetch=False):
1611         """
1612         Update all submodules
1613
1614         @param init: whether to initialize the submodule if necessary
1615         @type init: C{bool}
1616         @param recursive: whether to update submodules recursively
1617         @type recursive: C{bool}
1618         @param fetch: whether to fetch new objects
1619         @type fetch: C{bool}
1620         """
1621
1622         if not self.has_submodules():
1623             return
1624         args = [ "update" ]
1625         if recursive:
1626             args.append("--recursive")
1627         if init:
1628             args.append("--init")
1629         if not fetch:
1630             args.append("--no-fetch")
1631
1632         self._git_command("submodule", args)
1633
1634
1635     def get_submodules(self, treeish, path=None, recursive=True):
1636         """
1637         List the submodules of treeish
1638
1639         @return: a list of submodule/commit-id tuples
1640         @rtype: list of tuples
1641         """
1642         # Note that we is lstree instead of submodule commands because
1643         # there's no way to list the submodules of another branch with
1644         # the latter.
1645         submodules = []
1646         if path is None:
1647             path = self.path
1648
1649         args = [ treeish ]
1650         if recursive:
1651             args += ['-r']
1652
1653         out, ret =  self._git_getoutput('ls-tree', args, cwd=path)
1654         for line in out:
1655             mode, objtype, commit, name = line[:-1].split(None, 3)
1656             # A submodules is shown as "commit" object in ls-tree:
1657             if objtype == "commit":
1658                 nextpath = os.path.join(path, name)
1659                 submodules.append( (nextpath.replace(self.path,'').lstrip('/'),
1660                                     commit) )
1661                 if recursive:
1662                     submodules += self.get_submodules(commit, path=nextpath,
1663                                                       recursive=recursive)
1664         return submodules
1665
1666 #{ Repository Creation
1667
1668     @classmethod
1669     def create(klass, path, description=None, bare=False):
1670         """
1671         Create a repository at path
1672
1673         @param path: where to create the repository
1674         @type path: C{str}
1675         @param bare: whether to create a bare repository
1676         @type bare: C{bool}
1677         @return: git repository object
1678         @rtype: L{GitRepository}
1679         """
1680         args = GitArgs()
1681         abspath = os.path.abspath(path)
1682
1683         args.add_true(bare, '--bare')
1684         git_dir = '' if bare else '.git'
1685
1686         try:
1687             if not os.path.exists(abspath):
1688                 os.makedirs(abspath)
1689             try:
1690                 stdout, stderr, ret = klass.__git_inout(command='init',
1691                                                         args=args.args,
1692                                                         input=None,
1693                                                         extra_env=None,
1694                                                         cwd=abspath,
1695                                                         capture_stderr=True)
1696             except Exception as excobj:
1697                 raise GitRepositoryError("Error running git init: %s" % excobj)
1698             if ret:
1699                 raise GitRepositoryError("Error running git init: %s" % stderr)
1700
1701             if description:
1702                 with open(os.path.join(abspath, git_dir, "description"), 'w') as f:
1703                     description += '\n' if description[-1] != '\n' else ''
1704                     f.write(description)
1705             return klass(abspath)
1706         except OSError as err:
1707             raise GitRepositoryError("Cannot create Git repository at '%s': %s"
1708                                      % (abspath, err[1]))
1709         return None
1710
1711     @classmethod
1712     def clone(klass, path, remote, depth=0, recursive=False, mirror=False,
1713               bare=False, auto_name=True):
1714         """
1715         Clone a git repository at I{remote} to I{path}.
1716
1717         @param path: where to clone the repository to
1718         @type path: C{str}
1719         @param remote: URL to clone
1720         @type remote: C{str}
1721         @param depth: create a shallow clone of depth I{depth}
1722         @type depth: C{int}
1723         @param recursive: whether to clone submodules
1724         @type recursive: C{bool}
1725         @param mirror: whether to pass --mirror to git-clone
1726         @type mirror: C{bool}
1727         @param bare: whether to create a bare repository
1728         @type bare: C{bool}
1729         @param auto_name: If I{True} create a directory below I{path} based on
1730             the I{remote}s name. Otherwise create the repo directly at I{path}.
1731         @type auto_name: C{bool}
1732         @return: git repository object
1733         @rtype: L{GitRepository}
1734         """
1735         abspath = os.path.abspath(path)
1736         if auto_name:
1737             name = None
1738         else:
1739             abspath, name = abspath.rsplit('/', 1)
1740
1741         args = GitArgs('--quiet')
1742         args.add_true(depth,  '--depth', depth)
1743         args.add_true(recursive, '--recursive')
1744         args.add_true(mirror, '--mirror')
1745         args.add_true(bare, '--bare')
1746         args.add(remote)
1747         args.add_true(name, name)
1748         try:
1749             if not os.path.exists(abspath):
1750                 os.makedirs(abspath)
1751
1752             try:
1753                 stdout, stderr, ret = klass.__git_inout(command='clone',
1754                                                         args=args.args,
1755                                                         input=None,
1756                                                         extra_env=None,
1757                                                         cwd=abspath,
1758                                                         capture_stderr=True)
1759             except Exception as excobj:
1760                 raise GitRepositoryError("Error running git clone: %s" % excobj)
1761             if ret:
1762                 raise GitRepositoryError("Error running git clone: %s" % stderr)
1763
1764             if not name:
1765                 name = remote.rstrip('/').rsplit('/',1)[1]
1766                 if (mirror or bare):
1767                     if not name.endswith('.git'):
1768                         name = "%s.git" % name
1769                 elif name.endswith('.git'):
1770                     name = name[:-4]
1771             return klass(os.path.join(abspath, name))
1772         except OSError as err:
1773             raise GitRepositoryError("Cannot clone Git repository "
1774                                      "'%s' to '%s': %s"
1775                                      % (remote, abspath, err[1]))
1776         return None
1777 #}
1778