41e647170ecffc669ed55494a19d60535463fa45
[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 is_empty(self):
769         """
770         Is the repository empty?
771
772         @return: True if the repositorydoesn't have any commits,
773             False otherwise
774         @rtype: C{bool}
775         """
776         # an empty repo has no branches:
777         return False if self.branch else True
778
779     def rev_parse(self, name, short=0):
780         """
781         Find the SHA1 of a given name
782
783         @param name: the name to look for
784         @type name: C{str}
785         @param short:  try to abbreviate SHA1 to given length
786         @type short: C{int}
787         @return: the name's sha1
788         @rtype: C{str}
789         """
790         args = GitArgs("--quiet", "--verify")
791         args.add_cond(short, '--short=%d' % short)
792         args.add(name)
793         sha, ret = self._git_getoutput('rev-parse', args.args)
794         if ret:
795             raise GitRepositoryError("revision '%s' not found" % name)
796         return self.strip_sha1(sha[0], short)
797
798     @staticmethod
799     def strip_sha1(sha1, length=0):
800         """
801         Strip a given sha1 and check if the resulting
802         hash has the expected length.
803
804         >>> GitRepository.strip_sha1('  58ef37dbeb12c44b206b92f746385a6f61253c0a\\n')
805         '58ef37dbeb12c44b206b92f746385a6f61253c0a'
806         >>> GitRepository.strip_sha1('58ef37d', 10)
807         Traceback (most recent call last):
808         ...
809         GitRepositoryError: '58ef37d' is not a valid sha1 of length 10
810         >>> GitRepository.strip_sha1('58ef37d', 7)
811         '58ef37d'
812         >>> GitRepository.strip_sha1('123456789', 7)
813         '123456789'
814         >>> GitRepository.strip_sha1('foobar')
815         Traceback (most recent call last):
816         ...
817         GitRepositoryError: 'foobar' is not a valid sha1
818         """
819         maxlen = 40
820         s = sha1.strip()
821
822         l = length or maxlen
823
824         if len(s) < l or len(s) > maxlen:
825             raise GitRepositoryError("'%s' is not a valid sha1%s" %
826                                      (s, " of length %d" % l if length else ""))
827         return s
828
829 #{ Trees
830     def checkout(self, treeish):
831         """
832         Checkout treeish
833
834         @param treeish: the treeish to check out
835         @type treeish: C{str}
836         """
837         self._git_command("checkout", ["--quiet", treeish])
838
839     def has_treeish(self, treeish):
840         """
841         Check if the repository has the treeish object I{treeish}.
842
843         @param treeish: treeish object to look for
844         @type treeish: C{str}
845         @return: C{True} if the repository has that tree, C{False} otherwise
846         @rtype: C{bool}
847         """
848         _out, _err, ret =  self._git_inout('ls-tree', [treeish],
849                                            capture_stderr=True)
850         return [ True, False ][ret != 0]
851
852     def write_tree(self, index_file=None):
853         """
854         Create a tree object from the current index
855
856         @param index_file: alternate index file to read changes from
857         @type index_file: C{str}
858         @return: the new tree object's sha1
859         @rtype: C{str}
860         """
861         if index_file:
862             extra_env = {'GIT_INDEX_FILE': index_file }
863         else:
864             extra_env = None
865
866         tree, stderr, ret = self._git_inout('write-tree', [],
867                                             extra_env=extra_env,
868                                             capture_stderr=True)
869         if ret:
870             raise GitRepositoryError("Can't write out current index: %s" % stderr[:-1])
871         return tree.strip()
872
873     def make_tree(self, contents):
874         """
875         Create a tree based on contents. I{contents} has the same format than
876         the I{GitRepository.list_tree} output.
877         """
878         out=''
879         args = GitArgs('-z')
880
881         for obj in contents:
882              mode, type, sha1, name = obj
883              out += '%s %s %s\t%s\0' % (mode, type, sha1, name)
884
885         sha1, err, ret =  self._git_inout('mktree',
886                                           args.args,
887                                           out,
888                                           capture_stderr=True)
889         if ret:
890             raise GitRepositoryError("Failed to mktree: '%s'" % err)
891         return self.strip_sha1(sha1)
892
893     def get_obj_type(self, obj):
894         """
895         Get type of a git repository object
896
897         @param obj: repository object
898         @type obj: C{str}
899         @return: type of the repository object
900         @rtype: C{str}
901         """
902         out, ret = self._git_getoutput('cat-file', args=['-t', obj])
903         if ret:
904             raise GitRepositoryError("Not a Git repository object: '%s'" % obj)
905         return out[0].strip()
906
907     def list_tree(self, treeish, recurse=False):
908         """
909         Get a trees content. It returns a list of objects that match the
910         'ls-tree' output: [ mode, type, sha1, path ].
911
912         @param treeish: the treeish object to list
913         @type treeish: C{str}
914         @param recurse: whether to list the tree recursively
915         @type recurse: C{bool}
916         @return: the tree
917         @rtype: C{list} of objects. See above.
918         """
919         args = GitArgs('-z')
920         args.add_true(recurse, '-r')
921         args.add(treeish)
922
923         out, err, ret =  self._git_inout('ls-tree', args.args, capture_stderr=True)
924         if ret:
925             raise GitRepositoryError("Failed to ls-tree '%s': '%s'" % (treeish, err))
926
927         tree = []
928         for line in out.split('\0'):
929             if line:
930                 tree.append(line.split(None, 3))
931         return tree
932
933 #}
934
935     def get_config(self, name):
936         """
937         Gets the config value associated with I{name}
938
939         @param name: config value to get
940         @return: fetched config value
941         @rtype: C{str}
942         """
943         value, ret = self._git_getoutput('config', [ name ])
944         if ret: raise KeyError
945         return value[0][:-1] # first line with \n ending removed
946
947     def get_author_info(self):
948         """
949         Determine a sane values for author name and author email from git's
950         config and environment variables.
951
952         @return: name and email
953         @rtype: L{GitModifier}
954         """
955         try:
956            name =  self.get_config("user.name")
957         except KeyError:
958            name = os.getenv("USER")
959         try:
960            email =  self.get_config("user.email")
961         except KeyError:
962             email = os.getenv("EMAIL")
963         email = os.getenv("GIT_AUTHOR_EMAIL", email)
964         name = os.getenv("GIT_AUTHOR_NAME", name)
965         return GitModifier(name, email)
966
967 #{ Remote Repositories
968
969     def get_remote_repos(self):
970         """
971         Get all remote repositories
972
973         @return: remote repositories
974         @rtype: C{list} of C{str}
975         """
976         out = self._git_getoutput('remote')[0]
977         return [ remote.strip() for remote in out ]
978
979     def has_remote_repo(self, name):
980         """
981         Do we know about a remote named I{name}?
982
983         @param name: name of the remote repository
984         @type name: C{str}
985         @return: C{True} if the remote repositore is known, C{False} otherwise
986         @rtype: C{bool}
987         """
988         if name in self.get_remote_repos():
989             return True
990         else:
991             return False
992
993     def add_remote_repo(self, name, url, tags=True, fetch=False):
994         """
995         Add a tracked remote repository
996
997         @param name: the name to use for the remote
998         @type name: C{str}
999         @param url: the url to add
1000         @type url: C{str}
1001         @param tags: whether to fetch tags
1002         @type tags: C{bool}
1003         @param fetch: whether to fetch immediately from the remote side
1004         @type fetch: C{bool}
1005         """
1006         args = GitArgs('add')
1007         args.add_false(tags, '--no-tags')
1008         args.add_true(fetch, '--fetch')
1009         args.add(name, url)
1010         self._git_command("remote", args.args)
1011
1012     def remove_remote_repo(self, name):
1013         args = GitArgs('rm', name)
1014         self._git_command("remote", args.args)
1015
1016     def fetch(self, repo=None, tags=False, depth=0, refspec=None,
1017               all_remotes=False):
1018         """
1019         Download objects and refs from another repository.
1020
1021         @param repo: repository to fetch from
1022         @type repo: C{str}
1023         @param tags: whether to fetch all tag objects
1024         @type tags: C{bool}
1025         @param depth: deepen the history of (shallow) repository to depth I{depth}
1026         @type depth: C{int}
1027         @param refspec: refspec to use instead of the default from git config
1028         @type refspec: C{str}
1029         @param all_remotes: fetch all remotes
1030         @type all_remotes: C{bool}
1031         """
1032         args = GitArgs('--quiet')
1033         args.add_true(tags, '--tags')
1034         args.add_cond(depth, '--depth=%s' % depth)
1035         if all_remotes:
1036             args.add_true(all_remotes, '--all')
1037         else:
1038             args.add_cond(repo, repo)
1039             args.add_cond(refspec, refspec)
1040
1041         self._git_command("fetch", args.args)
1042
1043     def pull(self, repo=None, ff_only=False):
1044         """
1045         Fetch and merge from another repository
1046
1047         @param repo: repository to fetch from
1048         @type repo: C{str}
1049         @param ff_only: only merge if this results in a fast forward merge
1050         @type ff_only: C{bool}
1051         """
1052         args = []
1053         args += [ '--ff-only' ] if ff_only else []
1054         args += [ repo ] if repo else []
1055         self._git_command("pull", args)
1056
1057     def push(self, repo=None, src=None, dst=None, ff_only=True):
1058         """
1059         Push changes to the remote repo
1060
1061         @param repo: repository to push to
1062         @type repo: C{str}
1063         @param src: the source ref to push
1064         @type src: C{str}
1065         @param dst: the name of the destination ref to push to
1066         @type dst: C{str}
1067         @param ff_only: only push if it's a fast forward update
1068         @type ff_only: C{bool}
1069         """
1070         args = GitArgs()
1071         args.add_cond(repo, repo)
1072
1073         # Allow for src == '' to delete dst on the remote
1074         if src != None:
1075             refspec = src
1076             if dst:
1077                 refspec += ':%s' % dst
1078             if not ff_only:
1079                 refspec = '+%s' % refspec
1080             args.add(refspec)
1081         self._git_command("push", args.args)
1082
1083     def push_tag(self, repo, tag):
1084         """
1085         Push a tag to the remote repo
1086
1087         @param repo: repository to push to
1088         @type repo: C{str}
1089         @param tag: the name of the tag
1090         @type tag: C{str}
1091         """
1092         args = GitArgs(repo, 'tag', tag)
1093         self._git_command("push", args.args)
1094
1095 #{ Files
1096
1097     def add_files(self, paths, force=False, index_file=None, work_tree=None):
1098         """
1099         Add files to a the repository
1100
1101         @param paths: list of files to add
1102         @type paths: list or C{str}
1103         @param force: add files even if they would be ignored by .gitignore
1104         @type force: C{bool}
1105         @param index_file: alternative index file to use
1106         @param work_tree: alternative working tree to use
1107         """
1108         extra_env = {}
1109
1110         if isinstance(paths, basestring):
1111             paths = [ paths ]
1112
1113         args = [ '-f' ] if force else []
1114
1115         if index_file:
1116             extra_env['GIT_INDEX_FILE'] =  index_file
1117
1118         if work_tree:
1119             extra_env['GIT_WORK_TREE'] = work_tree
1120
1121         self._git_command("add", args + paths, extra_env)
1122
1123     def remove_files(self, paths, verbose=False):
1124         """
1125         Remove files from the repository
1126
1127         @param paths: list of files to remove
1128         @param paths: C{list} or C{str}
1129         @param verbose: be verbose
1130         @type verbose: C{bool}
1131         """
1132         if isinstance(paths, basestring):
1133             paths = [ paths ]
1134
1135         args =  [] if verbose else ['--quiet']
1136         self._git_command("rm", args + paths)
1137
1138     def list_files(self, types=['cached']):
1139         """
1140         List files in index and working tree
1141
1142         @param types: list of types to show
1143         @type types: C{list}
1144         @return: list of files
1145         @rtype: C{list} of C{str}
1146         """
1147         all_types = [ 'cached', 'deleted', 'others', 'ignored',  'stage'
1148                       'unmerged', 'killed', 'modified' ]
1149         args = [ '-z' ]
1150
1151         for t in types:
1152             if t in all_types:
1153                 args += [ '--%s' % t ]
1154             else:
1155                 raise GitRepositoryError("Unknown type '%s'" % t)
1156         out, ret = self._git_getoutput('ls-files', args)
1157         if ret:
1158             raise GitRepositoryError("Error listing files: '%d'" % ret)
1159         if out:
1160             return [ file for file in out[0].split('\0') if file ]
1161         else:
1162             return []
1163
1164
1165     def write_file(self, filename, filters=True):
1166         """
1167         Hash a single file and write it into the object database
1168
1169         @param filename: the filename to the content of the file to hash
1170         @type filename: C{str}
1171         @param filters: whether to run filters
1172         @type filters: C{bool}
1173         @return: the hash of the file
1174         @rtype: C{str}
1175         """
1176         args = GitArgs('-w', '-t', 'blob')
1177         args.add_false(filters, '--no-filters')
1178         args.add(filename)
1179
1180         sha1, stderr, ret = self._git_inout('hash-object',
1181                                             args.args,
1182                                             capture_stderr=True)
1183         if not ret:
1184             return self.strip_sha1(sha1)
1185         else:
1186             raise GbpError("Failed to hash %s: %s" % (filename, stderr))
1187 #}
1188
1189 #{ Comitting
1190
1191     def _commit(self, msg, args=[], author_info=None):
1192         extra_env = author_info.get_author_env() if author_info else None
1193         self._git_command("commit", ['-q', '-m', msg] + args, extra_env=extra_env)
1194
1195     def commit_staged(self, msg, author_info=None, edit=False):
1196         """
1197         Commit currently staged files to the repository
1198
1199         @param msg: commit message
1200         @type msg: C{str}
1201         @param author_info: authorship information
1202         @type author_info: L{GitModifier}
1203         @param edit: whether to spawn an editor to edit the commit info
1204         @type edit: C{bool}
1205         """
1206         args = GitArgs()
1207         args.add_true(edit,  '--edit')
1208         self._commit(msg=msg, args=args.args, author_info=author_info)
1209
1210     def commit_all(self, msg, author_info=None, edit=False):
1211         """
1212         Commit all changes to the repository
1213         @param msg: commit message
1214         @type msg: C{str}
1215         @param author_info: authorship information
1216         @type author_info: L{GitModifier}
1217         """
1218         args = GitArgs('-a')
1219         args.add_true(edit,  '--edit')
1220         self._commit(msg=msg, args=args.args, author_info=author_info)
1221
1222     def commit_files(self, files, msg, author_info=None):
1223         """
1224         Commit the given files to the repository
1225
1226         @param files: file or files to commit
1227         @type files: C{str} or C{list}
1228         @param msg: commit message
1229         @type msg: C{str}
1230         @param author_info: authorship information
1231         @type author_info: L{GitModifier}
1232         """
1233         if isinstance(files, basestring):
1234             files = [ files ]
1235         self._commit(msg=msg, args=files, author_info=author_info)
1236
1237     def commit_dir(self, unpack_dir, msg, branch, other_parents=None,
1238                    author={}, committer={}, create_missing_branch=False):
1239         """
1240         Replace the current tip of branch I{branch} with the contents from I{unpack_dir}
1241
1242         @param unpack_dir: content to add
1243         @type unpack_dir: C{str}
1244         @param msg: commit message to use
1245         @type msg: C{str}
1246         @param branch: branch to add the contents of unpack_dir to
1247         @type branch: C{str}
1248         @param other_parents: additional parents of this commit
1249         @type other_parents: C{list} of C{str}
1250         @param author: author information to use for commit
1251         @type author: C{dict} with keys I{name}, I{email}, I{date}
1252         @param committer: committer information to use for commit
1253         @type committer: C{dict} with keys I{name}, I{email}, I{date}
1254             or L{GitModifier}
1255         @param create_missing_branch: create I{branch} as detached branch if it
1256             doesn't already exist.
1257         @type create_missing_branch: C{bool}
1258         """
1259
1260         git_index_file = os.path.join(self.path, self._git_dir, 'gbp_index')
1261         try:
1262             os.unlink(git_index_file)
1263         except OSError:
1264             pass
1265         self.add_files('.', force=True, index_file=git_index_file,
1266                        work_tree=unpack_dir)
1267         tree = self.write_tree(git_index_file)
1268
1269         if branch:
1270             try:
1271                 cur = self.rev_parse(branch)
1272             except GitRepositoryError:
1273                 if create_missing_branch:
1274                     log.debug("Will create missing branch '%s'..." % branch)
1275                     cur = None
1276                 else:
1277                     raise
1278         else: # emtpy repo
1279             cur = None
1280             branch = 'master'
1281
1282         # Build list of parents:
1283         parents = []
1284         if cur:
1285             parents = [ cur ]
1286         if other_parents:
1287             for parent in other_parents:
1288                 sha = self.rev_parse(parent)
1289                 if sha not in parents:
1290                     parents += [ sha ]
1291
1292         commit = self.commit_tree(tree=tree, msg=msg, parents=parents,
1293                                   author=author, committer=committer)
1294         if not commit:
1295             raise GitRepositoryError("Failed to commit tree")
1296         self.update_ref("refs/heads/%s" % branch, commit, cur)
1297         return commit
1298
1299     def commit_tree(self, tree, msg, parents, author={}, committer={}):
1300         """
1301         Commit a tree with commit msg I{msg} and parents I{parents}
1302
1303         @param tree: tree to commit
1304         @param msg: commit message
1305         @param parents: parents of this commit
1306         @param author: authorship information
1307         @type author: C{dict} with keys 'name' and 'email' or L{GitModifier}
1308         @param committer: comitter information
1309         @type committer: C{dict} with keys 'name' and 'email'
1310         """
1311         extra_env = {}
1312         for key, val in author.items():
1313             if val:
1314                 extra_env['GIT_AUTHOR_%s' % key.upper()] = val
1315         for key, val in committer.items():
1316             if val:
1317                 extra_env['GIT_COMMITTER_%s' % key.upper()] = val
1318
1319         args = [ tree ]
1320         for parent in parents:
1321             args += [ '-p' , parent ]
1322         sha1, stderr, ret = self._git_inout('commit-tree',
1323                                             args,
1324                                             msg,
1325                                             extra_env,
1326                                             capture_stderr=True)
1327         if not ret:
1328             return self.strip_sha1(sha1)
1329         else:
1330             raise GbpError("Failed to commit tree: %s" % stderr)
1331
1332 #{ Commit Information
1333
1334     def get_commits(self, since=None, until=None, paths=None, num=0,
1335                     first_parent=False, options=None):
1336         """
1337         Get commits from since to until touching paths
1338
1339         @param since: commit to start from
1340         @type since: C{str}
1341         @param until: last commit to get
1342         @type until: C{str}
1343         @param paths: only list commits touching paths
1344         @type paths: C{list} of C{str}
1345         @param num: maximum number of commits to fetch
1346         @type num: C{int}
1347         @param options: list of additional options passed to git log
1348         @type  options: C{list} of C{str}ings
1349         @param first_parent: only follow first parent when seeing a
1350                              merge commit
1351         @type first_parent: C{bool}
1352         """
1353         args = GitArgs('--pretty=format:%H')
1354         args.add_true(num, '-%d' % num)
1355         args.add_true(first_parent, '--first-parent')
1356         if since:
1357             args.add("%s..%s" % (since, until or 'HEAD'))
1358         elif until:
1359             args.add(until)
1360         args.add_cond(options, options)
1361         args.add("--")
1362         if isinstance(paths, basestring):
1363             paths = [ paths ]
1364         args.add_cond(paths, paths)
1365
1366         commits, ret = self._git_getoutput('log', args.args)
1367         if ret:
1368             where = " on %s" % paths if paths else ""
1369             raise GitRepositoryError("Error getting commits %s..%s%s" %
1370                         (since, until, where))
1371         return [ commit.strip() for commit in commits ]
1372
1373     def show(self, id):
1374         """git-show id"""
1375         obj, stderr, ret = self._git_inout('show', ["--pretty=medium", id],
1376                                               capture_stderr=True)
1377         if ret:
1378             raise GitRepositoryError("can't get %s: %s" % (id, stderr.rstrip()))
1379         return obj
1380
1381     def grep_log(self, regex, since=None):
1382         """
1383         Get commmits matching I{regex}
1384
1385         @param regex: regular expression
1386         @type regex: C{str}
1387         @param since: where to start grepping (e.g. a branch)
1388         @type since: C{str}
1389         """
1390         args = ['--pretty=format:%H']
1391         args.append("--grep=%s" % regex)
1392         if since:
1393             args.append(since)
1394         args.append('--')
1395
1396         stdout, stderr, ret = self._git_inout('log', args,
1397                                               capture_stderr=True)
1398         if ret:
1399             raise GitRepositoryError("Error grepping log for %s: %s" %
1400                                      (regex, stderr[:-1]))
1401         if stdout:
1402             return [ commit.strip() for commit in stdout.split('\n')[::-1] ]
1403         else:
1404             return []
1405
1406     def get_subject(self, commit):
1407         """
1408         Gets the subject of a commit.
1409
1410         @deprecated: Use get_commit_info directly
1411
1412         @param commit: the commit to get the subject from
1413         @return: the commit's subject
1414         @rtype: C{str}
1415         """
1416         return self.get_commit_info(commit)['subject']
1417
1418     def get_commit_info(self, commitish):
1419         """
1420         Look up data of a specific commit-ish. Dereferences given commit-ish
1421         to the commit it points to.
1422
1423         @param commitish: the commit-ish to inspect
1424         @return: the commit's including id, author, email, subject and body
1425         @rtype: dict
1426         """
1427         commit_sha1 = self.rev_parse("%s^0" % commitish)
1428         args = GitArgs('--pretty=format:%an%x00%ae%x00%ad%x00%cn%x00%ce%x00%cd%x00%s%x00%f%x00%b%x00',
1429                        '-z', '--date=raw', '--name-status', commit_sha1)
1430         out, err, ret =  self._git_inout('show', args.args)
1431         if ret:
1432             raise GitRepositoryError("Unable to retrieve commit info for %s"
1433                                      % commitish)
1434
1435         fields = out.split('\x00')
1436
1437         author = GitModifier(fields[0].strip(),
1438                              fields[1].strip(),
1439                              fields[2].strip())
1440         committer = GitModifier(fields[3].strip(),
1441                                 fields[4].strip(),
1442                                 fields[5].strip())
1443
1444         files = defaultdict(list)
1445         file_fields = fields[9:]
1446         # For some reason git returns one extra empty field for merge commits
1447         if file_fields[0] == '': file_fields.pop(0)
1448         while len(file_fields) and file_fields[0] != '':
1449             status = file_fields.pop(0).strip()
1450             path = file_fields.pop(0)
1451             files[status].append(path)
1452
1453         return {'id' : commitish,
1454                 'author' : author,
1455                 'committer' : committer,
1456                 'subject' : fields[6],
1457                 'patchname' : fields[7],
1458                 'body' : fields[8],
1459                 'files' : files}
1460
1461 #{ Patches
1462     def format_patches(self, start, end, output_dir,
1463                        signature=True,
1464                        thread=None,
1465                        symmetric=True):
1466         """
1467         Output the commits between start and end as patches in output_dir.
1468
1469         This outputs the revisions I{start...end} by default. When using
1470         I{symmetric} to C{false} it uses I{start..end} instead.
1471
1472         @param start: the commit on the left side of the revision range
1473         @param end: the commit on the right hand side of the revisino range
1474         @param output_dir: directory to write the patches to
1475         @param signature: whether to output a signature
1476         @param thread: whether to include In-Reply-To references
1477         @param symmetric: whether to use the symmetric difference (see above)
1478         """
1479         options = GitArgs('-N', '-k',
1480                           '-o', output_dir)
1481         options.add_cond(not signature, '--no-signature')
1482         options.add('%s%s%s' % (start, '...' if symmetric else '..', end))
1483         options.add_cond(thread, '--thread=%s' % thread, '--no-thread')
1484
1485         output, ret = self._git_getoutput('format-patch', options.args)
1486         return [ line.strip() for line in output ]
1487
1488     def apply_patch(self, patch, index=True, context=None, strip=None):
1489         """Apply a patch using git apply"""
1490         args = []
1491         if context:
1492             args += [ '-C', context ]
1493         if index:
1494             args.append("--index")
1495         if strip != None:
1496             args += [ '-p', str(strip) ]
1497         args.append(patch)
1498         self._git_command("apply", args)
1499
1500     def diff(self, obj1, obj2=None, paths=None, stat=False, summary=False):
1501         """
1502         Diff two git repository objects
1503
1504         @param obj1: first object
1505         @type obj1: C{str}
1506         @param obj2: second object
1507         @type obj2: C{str}
1508         @param paths: List of paths to diff
1509         @type paths: C{list}
1510         @param stat: Show diffstat
1511         @type stat: C{bool} or C{int} or C{str}
1512         @param summary: Show diffstat
1513         @type summary: C{bool}
1514         @return: diff
1515         @rtype: C{str}
1516         """
1517         options = GitArgs('-p')
1518         if stat is True:
1519             options.add('--stat')
1520         elif stat:
1521             options.add('--stat=%s' % stat)
1522         options.add_true(summary, '--summary')
1523         options.add(obj1)
1524         options.add_true(obj2, obj2)
1525         if paths:
1526             options.add('--', paths)
1527         output, stderr, ret = self._git_inout('diff', options.args)
1528         if ret:
1529             raise GitRepositoryError("Git diff failed")
1530         return output
1531 #}
1532
1533     def archive(self, format, prefix, output, treeish, **kwargs):
1534         """
1535         Create an archive from a treeish
1536
1537         @param format: the type of archive to create, e.g. 'tar.gz'
1538         @type format: C{str}
1539         @param prefix: prefix to prepend to each filename in the archive
1540         @type prefix: C{str}
1541         @param output: the name of the archive to create
1542         @type output: C{str}
1543         @param treeish: the treeish to create the archive from
1544         @type treeish: C{str}
1545         @param kwargs: additional commandline options passed to git-archive
1546         """
1547         args = [ '--format=%s' % format, '--prefix=%s' % prefix,
1548                  '--output=%s' % output, treeish ]
1549         out, ret = self._git_getoutput('archive', args, **kwargs)
1550         if ret:
1551             raise GitRepositoryError("Unable to archive %s" % treeish)
1552
1553     def collect_garbage(self, auto=False):
1554         """
1555         Cleanup unnecessary files and optimize the local repository
1556
1557         param auto: only cleanup if required
1558         param auto: C{bool}
1559         """
1560         args = [ '--auto' ] if auto else []
1561         self._git_command("gc", args)
1562
1563 #{ Submodules
1564
1565     def has_submodules(self):
1566         """
1567         Does the repo have any submodules?
1568
1569         @return: C{True} if the repository has any submodules, C{False}
1570             otherwise
1571         @rtype: C{bool}
1572         """
1573         if os.path.exists(os.path.join(self.path, '.gitmodules')):
1574             return True
1575         else:
1576             return False
1577
1578
1579     def add_submodule(self, repo_path):
1580         """
1581         Add a submodule
1582
1583         @param repo_path: path to submodule
1584         @type repo_path: C{str}
1585         """
1586         self._git_command("submodule", [ "add", repo_path ])
1587
1588
1589     def update_submodules(self, init=True, recursive=True, fetch=False):
1590         """
1591         Update all submodules
1592
1593         @param init: whether to initialize the submodule if necessary
1594         @type init: C{bool}
1595         @param recursive: whether to update submodules recursively
1596         @type recursive: C{bool}
1597         @param fetch: whether to fetch new objects
1598         @type fetch: C{bool}
1599         """
1600
1601         if not self.has_submodules():
1602             return
1603         args = [ "update" ]
1604         if recursive:
1605             args.append("--recursive")
1606         if init:
1607             args.append("--init")
1608         if not fetch:
1609             args.append("--no-fetch")
1610
1611         self._git_command("submodule", args)
1612
1613
1614     def get_submodules(self, treeish, path=None, recursive=True):
1615         """
1616         List the submodules of treeish
1617
1618         @return: a list of submodule/commit-id tuples
1619         @rtype: list of tuples
1620         """
1621         # Note that we is lstree instead of submodule commands because
1622         # there's no way to list the submodules of another branch with
1623         # the latter.
1624         submodules = []
1625         if path is None:
1626             path = self.path
1627
1628         args = [ treeish ]
1629         if recursive:
1630             args += ['-r']
1631
1632         out, ret =  self._git_getoutput('ls-tree', args, cwd=path)
1633         for line in out:
1634             mode, objtype, commit, name = line[:-1].split(None, 3)
1635             # A submodules is shown as "commit" object in ls-tree:
1636             if objtype == "commit":
1637                 nextpath = os.path.join(path, name)
1638                 submodules.append( (nextpath.replace(self.path,'').lstrip('/'),
1639                                     commit) )
1640                 if recursive:
1641                     submodules += self.get_submodules(commit, path=nextpath,
1642                                                       recursive=recursive)
1643         return submodules
1644
1645 #{ Repository Creation
1646
1647     @classmethod
1648     def create(klass, path, description=None, bare=False):
1649         """
1650         Create a repository at path
1651
1652         @param path: where to create the repository
1653         @type path: C{str}
1654         @param bare: whether to create a bare repository
1655         @type bare: C{bool}
1656         @return: git repository object
1657         @rtype: L{GitRepository}
1658         """
1659         args = GitArgs()
1660         abspath = os.path.abspath(path)
1661
1662         args.add_true(bare, '--bare')
1663         git_dir = '' if bare else '.git'
1664
1665         try:
1666             if not os.path.exists(abspath):
1667                 os.makedirs(abspath)
1668             try:
1669                 stdout, stderr, ret = klass.__git_inout(command='init',
1670                                                         args=args.args,
1671                                                         input=None,
1672                                                         extra_env=None,
1673                                                         cwd=abspath,
1674                                                         capture_stderr=True)
1675             except Exception as excobj:
1676                 raise GitRepositoryError("Error running git init: %s" % excobj)
1677             if ret:
1678                 raise GitRepositoryError("Error running git init: %s" % stderr)
1679
1680             if description:
1681                 with open(os.path.join(abspath, git_dir, "description"), 'w') as f:
1682                     description += '\n' if description[-1] != '\n' else ''
1683                     f.write(description)
1684             return klass(abspath)
1685         except OSError as err:
1686             raise GitRepositoryError("Cannot create Git repository at '%s': %s"
1687                                      % (abspath, err[1]))
1688         return None
1689
1690     @classmethod
1691     def clone(klass, path, remote, depth=0, recursive=False, mirror=False,
1692               bare=False, auto_name=True):
1693         """
1694         Clone a git repository at I{remote} to I{path}.
1695
1696         @param path: where to clone the repository to
1697         @type path: C{str}
1698         @param remote: URL to clone
1699         @type remote: C{str}
1700         @param depth: create a shallow clone of depth I{depth}
1701         @type depth: C{int}
1702         @param recursive: whether to clone submodules
1703         @type recursive: C{bool}
1704         @param mirror: whether to pass --mirror to git-clone
1705         @type mirror: C{bool}
1706         @param bare: whether to create a bare repository
1707         @type bare: C{bool}
1708         @param auto_name: If I{True} create a directory below I{path} based on
1709             the I{remote}s name. Otherwise create the repo directly at I{path}.
1710         @type auto_name: C{bool}
1711         @return: git repository object
1712         @rtype: L{GitRepository}
1713         """
1714         abspath = os.path.abspath(path)
1715         if auto_name:
1716             name = None
1717         else:
1718             abspath, name = abspath.rsplit('/', 1)
1719
1720         args = GitArgs('--quiet')
1721         args.add_true(depth,  '--depth', depth)
1722         args.add_true(recursive, '--recursive')
1723         args.add_true(mirror, '--mirror')
1724         args.add_true(bare, '--bare')
1725         args.add(remote)
1726         args.add_true(name, name)
1727         try:
1728             if not os.path.exists(abspath):
1729                 os.makedirs(abspath)
1730
1731             try:
1732                 stdout, stderr, ret = klass.__git_inout(command='clone',
1733                                                         args=args.args,
1734                                                         input=None,
1735                                                         extra_env=None,
1736                                                         cwd=abspath,
1737                                                         capture_stderr=True)
1738             except Exception as excobj:
1739                 raise GitRepositoryError("Error running git clone: %s" % excobj)
1740             if ret:
1741                 raise GitRepositoryError("Error running git clone: %s" % stderr)
1742
1743             if not name:
1744                 name = remote.rstrip('/').rsplit('/',1)[1]
1745                 if (mirror or bare):
1746                     if not name.endswith('.git'):
1747                         name = "%s.git" % name
1748                 elif name.endswith('.git'):
1749                     name = name[:-4]
1750             return klass(os.path.join(abspath, name))
1751         except OSError as err:
1752             raise GitRepositoryError("Cannot clone Git repository "
1753                                      "'%s' to '%s': %s"
1754                                      % (remote, abspath, err[1]))
1755         return None
1756 #}
1757