GitRepository/branch_contains: remove prefix '*' in branch name
[tools/git-buildpackage.git] / gbp / git / repository.py
1 # vim: set fileencoding=utf-8 :
2 #
3 # (C) 2006,2007,2008,2011 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 re
20 import subprocess
21 import os.path
22 from collections import defaultdict
23
24 import gbp.log as log
25 from gbp.command_wrappers import (GitCommand, CommandExecFailed)
26 from gbp.errors import GbpError
27 from gbp.git.modifier import GitModifier
28 from gbp.git.commit import GitCommit
29 from gbp.git.errors import GitError
30 from gbp.git.args import GitArgs
31
32
33 class GitRepositoryError(GitError):
34     """Exception thrown by L{GitRepository}"""
35     pass
36
37
38 class GitRepository(object):
39     """
40     Represents a git repository at I{path}. It's currently assumed that the git
41     repository is stored in a directory named I{.git/} below I{path}.
42
43     @ivar _path: The path to the working tree
44     @type _path: C{str}
45     @ivar _bare: Whether this is a bare repository
46     @type _bare: C{bool}
47     @raises GitRepositoryError: on git errors GitRepositoryError is raised by
48         all methods.
49     """
50
51     def _check_bare(self):
52         """Check whether this is a bare repository"""
53         out, dummy, ret = self._git_inout('rev-parse', ['--is-bare-repository'],
54                                           capture_stderr=True)
55         if ret:
56             raise GitRepositoryError(
57                 "Failed to get repository state at '%s'" % self.path)
58         self._bare = False if out.strip() != 'true' else True
59         self._git_dir = '' if self._bare else '.git'
60
61     def __init__(self, path):
62         self._path = os.path.abspath(path)
63         self._bare = False
64         try:
65             out, dummy, ret = self._git_inout('rev-parse', ['--show-cdup'],
66                                               capture_stderr=True)
67             if ret or out.strip():
68                 raise GitRepositoryError("No Git repository at '%s': '%s'" % (self.path, out))
69         except GitRepositoryError:
70             raise # We already have a useful error message
71         except:
72             raise GitRepositoryError("No Git repository at '%s'" % self.path)
73         self._check_bare()
74
75     def __build_env(self, 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
135         stderr_arg = subprocess.PIPE if capture_stderr else None
136
137         env = self.__build_env(extra_env)
138         cmd = ['git', command] + args
139         log.debug(cmd)
140         popen = subprocess.Popen(cmd,
141                                  stdin=subprocess.PIPE,
142                                  stdout=subprocess.PIPE,
143                                  stderr=stderr_arg,
144                                  env=env,
145                                  cwd=cwd)
146         (stdout, stderr) = popen.communicate(input)
147         return stdout, stderr, popen.returncode
148
149     def _git_command(self, command, args=[], extra_env=None):
150         """
151         Execute git command with arguments args and environment env
152         at path.
153
154         @param command: git command
155         @type command: C{str}
156         @param args: command line arguments
157         @type args: C{list}
158         @param extra_env: extra environment variables to set when running command
159         @type extra_env: C{dict}
160         """
161         try:
162             GitCommand(command, args, extra_env=extra_env, cwd=self.path)()
163         except CommandExecFailed as excobj:
164             raise GitRepositoryError("Error running git %s: %s" %
165                                      (command, excobj))
166
167     @property
168     def path(self):
169         """The absolute path to the repository"""
170         return self._path
171
172     @property
173     def git_dir(self):
174         """The absolute path to git's metadata"""
175         return os.path.join(self.path, self._git_dir)
176
177     @property
178     def bare(self):
179         """Wheter this is a bare repository"""
180         return self._bare
181
182     @property
183     def tags(self):
184         """List of all tags in the repository"""
185         return self.get_tags()
186
187     @property
188     def branch(self):
189         """The currently checked out branch"""
190         try:
191             return self.get_branch()
192         except GitRepositoryError:
193             return None
194
195     @property
196     def head(self):
197         """return the SHA1 of the current HEAD"""
198         return self.rev_parse('HEAD')
199
200 #{ Branches and Merging
201     def rename_branch(self, branch, newbranch):
202         """
203         Rename branch
204
205         @param branch: name of the branch to be renamed
206         @param newbranch: new name of the branch
207         """
208         args = GitArgs("-m", branch, newbranch)
209         self._git_command("branch", args.args)
210
211     def create_branch(self, branch, rev=None):
212         """
213         Create a new branch
214
215         @param branch: the branch's name
216         @param rev: where to start the branch from
217
218         If rev is None the branch starts form the current HEAD.
219         """
220         args = GitArgs(branch)
221         args.add_true(rev, rev)
222         self._git_command("branch", args.args)
223
224     def delete_branch(self, branch, remote=False):
225         """
226         Delete branch I{branch}
227
228         @param branch: name of the branch to delete
229         @type branch: C{str}
230         @param remote: delete a remote branch
231         @param remote: C{bool}
232         """
233         args = GitArgs('-D')
234         args.add_true(remote, '-r')
235         args.add(branch)
236
237         if self.branch != branch:
238             self._git_command("branch", args.args)
239         else:
240             raise GitRepositoryError("Can't delete the branch you're on")
241
242     def get_branch(self):
243         """
244         On what branch is the current working copy
245
246         @return: current branch
247         @rtype: C{str}
248         """
249         out, ret = self._git_getoutput('symbolic-ref', [ 'HEAD' ])
250         if ret:
251             raise GitRepositoryError("Currently not on a branch")
252
253         ref = out[0][:-1]
254         # Check if ref really exists
255         failed = self._git_getoutput('show-ref', [ ref ])[1]
256         if not failed:
257             return ref[11:] # strip /refs/heads
258
259     def has_branch(self, branch, remote=False):
260         """
261         Check if the repository has branch named I{branch}.
262
263         @param branch: branch to look for
264         @param remote: only look for remote branches
265         @type remote: C{bool}
266         @return: C{True} if the repository has this branch, C{False} otherwise
267         @rtype: C{bool}
268         """
269         if remote:
270             ref = 'refs/remotes/%s' % branch
271         else:
272             ref = 'refs/heads/%s' % branch
273         failed = self._git_getoutput('show-ref', [ ref ])[1]
274         if failed:
275             return False
276         return True
277
278     def set_branch(self, branch):
279         """
280         Switch to branch I{branch}
281
282         @param branch: name of the branch to switch to
283         @type branch: C{str}
284         """
285         if self.branch == branch:
286             return
287
288         if self.bare:
289             self._git_command("symbolic-ref",
290                               [ 'HEAD', 'refs/heads/%s' % branch ])
291         else:
292             self._git_command("checkout", [ branch ])
293
294     def get_merge_branch(self, branch):
295         """
296         Get the branch we'd merge from
297
298         @return: repo and branch we would merge from
299         @rtype: C{str}
300         """
301         try:
302             remote = self.get_config("branch.%s.remote" % branch)
303             merge = self.get_config("branch.%s.merge" % branch)
304         except KeyError:
305             return None
306         remote += merge.replace("refs/heads","", 1)
307         return remote
308
309     def get_merge_base(self, commit1, commit2):
310         """
311         Get the common ancestor between two commits
312
313         @param commit1: commit SHA1 or name of a branch or tag
314         @type commit1: C{str}
315         @param commit2: commit SHA1 or name of a branch or tag
316         @type commit2: C{str}
317         @return: SHA1 of the common ancestor
318         @rtype: C{str}
319         """
320         args = GitArgs()
321         args.add(commit1)
322         args.add(commit2)
323         sha1, stderr, ret = self._git_inout('merge-base', args.args, capture_stderr=True)
324         if not ret:
325             return sha1.strip()
326         else:
327             raise GitRepositoryError("Failed to get common ancestor: %s" % stderr.strip())
328
329     def merge(self, commit, verbose=False, edit=False):
330         """
331         Merge changes from the named commit into the current branch
332
333         @param commit: the commit to merge from (usually a branch name or tag)
334         @type commit: C{str}
335         @param verbose: whether to print a summary after the merge
336         @type verbose: C{bool}
337         @param edit: wheter to invoke an editor to edit the merge message
338         @type edit: C{bool}
339         """
340         args = GitArgs()
341         args.add_cond(verbose, '--summary', '--no-summary')
342         args.add_cond(edit, '--edit', '--no-edit')
343         args.add(commit)
344         self._git_command("merge", args.args)
345
346     def is_fast_forward(self, from_branch, to_branch):
347         """
348         Check if an update I{from from_branch} to I{to_branch} would be a fast
349         forward or if the branch is up to date already.
350
351         @return: can_fast_forward, up_to_date
352         @rtype: C{tuple}
353         """
354         has_local = False       # local repo has new commits
355         has_remote = False      # remote repo has new commits
356         out = self._git_getoutput('rev-list', ["--left-right",
357                                    "%s...%s" % (from_branch, to_branch),
358                                    "--"])[0]
359
360         if not out: # both branches have the same commits
361             return True, True
362
363         for line in out:
364             if line.startswith("<"):
365                 has_local = True
366             elif line.startswith(">"):
367                 has_remote = True
368
369         if has_local and has_remote:
370             return False, False
371         elif has_local:
372             return False, True
373         elif has_remote:
374             return True, False
375
376     def _get_branches(self, remote=False):
377         """
378         Get a list of branches
379
380         @param remote: whether to list local or remote branches
381         @type remote: C{bool}
382         @return: local or remote branches
383         @rtype: C{list}
384         """
385         args = [ '--format=%(refname:short)' ]
386         args += [ 'refs/remotes/' ] if remote else [ 'refs/heads/' ]
387         out = self._git_getoutput('for-each-ref', args)[0]
388         return [ ref.strip() for ref in out ]
389
390     def get_local_branches(self):
391         """
392         Get a list of local branches
393
394         @return: local branches
395         @rtype: C{list}
396         """
397         return self._get_branches(remote=False)
398
399
400     def get_remote_branches(self):
401         """
402         Get a list of remote branches
403
404         @return: remote branches
405         @rtype: C{list}
406         """
407         return self._get_branches(remote=True)
408
409     def update_ref(self, ref, new, old=None, msg=None):
410         """
411         Update ref I{ref} to commit I{new} if I{ref} currently points to
412         I{old}
413
414         @param ref: the ref to update
415         @type ref: C{str}
416         @param new: the new value for ref
417         @type new: C{str}
418         @param old: the old value of ref
419         @type old: C{str}
420         @param msg: the reason for the update
421         @type msg: C{str}
422         """
423         args = [ ref, new ]
424         if old:
425             args += [ old ]
426         if msg:
427             args = [ '-m', msg ] + args
428         self._git_command("update-ref", args)
429
430     def branch_contains(self, branch, commit, remote=False):
431         """
432         Check if branch I{branch} contains commit I{commit}
433
434         @param branch: the branch the commit should be on
435         @type branch: C{str}
436         @param commit: the C{str} commit to check
437         @type commit: C{str}
438         @param remote: whether to check remote instead of local branches
439         @type remote: C{bool}
440         """
441         args = GitArgs()
442         args.add_true(remote, '-r')
443         args.add('--contains')
444         args.add(commit)
445
446         out, ret =  self._git_getoutput('branch', args.args)
447         for line in out:
448             # remove prefix '*' for current branch before comparing
449             line = line.replace('*', '')
450             if line.strip() == branch:
451                 return True
452         return False
453
454 #{ Tags
455
456     def create_tag(self, name, msg=None, commit=None, sign=False, keyid=None):
457         """
458         Create a new tag.
459
460         @param name: the tag's name
461         @type name: C{str}
462         @param msg: The tag message.
463         @type msg: C{str}
464         @param commit: the commit or object to create the tag at, default
465             is I{HEAD}
466         @type commit: C{str}
467         @param sign: Whether to sing the tag
468         @type sign: C{bool}
469         @param keyid: the GPG keyid used to sign the tag
470         @type keyid: C{str}
471         """
472         args = []
473         args += [ '-m', msg ] if msg else []
474         if sign:
475             args += [ '-s' ]
476             args += [ '-u', keyid ] if keyid else []
477         args += [ name ]
478         args += [ commit ] if commit else []
479         self._git_command("tag", args)
480
481     def delete_tag(self, tag):
482         """
483         Delete a tag named I{tag}
484
485         @param tag: the tag to delete
486         @type tag: C{str}
487         """
488         if self.has_tag(tag):
489             self._git_command("tag", [ "-d", tag ])
490
491     def move_tag(self, old, new):
492         self._git_command("tag", [ new, old ])
493         self.delete_tag(old)
494
495     def has_tag(self, tag):
496         """
497         Check if the repository has a tag named I{tag}.
498
499         @param tag: tag to look for
500         @type tag: C{str}
501         @return: C{True} if the repository has that tag, C{False} otherwise
502         @rtype: C{bool}
503         """
504         out, ret = self._git_getoutput('tag', [ '-l', tag ])
505         return [ False, True ][len(out)]
506
507     def find_tag(self, commit, pattern=None):
508         """
509         Find the closest tag to a given commit
510
511         @param commit: the commit to describe
512         @type commit: C{str}
513         @param pattern: only look for tags matching I{pattern}
514         @type pattern: C{str}
515         @return: the found tag
516         @rtype: C{str}
517         """
518         args =  [ '--abbrev=0' ]
519         if pattern:
520             args += [ '--match' , pattern ]
521         args += [ commit ]
522
523         tag, ret = self._git_getoutput('describe', args)
524         if ret:
525             raise GitRepositoryError("Can't find tag for %s" % commit)
526         return tag[0].strip()
527
528     def get_tags(self, pattern=None):
529         """
530         List tags
531
532         @param pattern: only list tags matching I{pattern}
533         @type pattern: C{str}
534         @return: tags
535         @rtype: C{list} of C{str}
536         """
537         args = [ '-l', pattern ] if pattern else []
538         return [ line.strip() for line in self._git_getoutput('tag', args)[0] ]
539
540     def verify_tag(self, tag):
541         """
542         Verify a signed tag
543
544         @param tag: the tag's name
545         @type tag: C{str}
546         @return: Whether the signature on the tag could be verified
547         @rtype: C{bool}
548         """
549         args = GitArgs('-v', tag)
550
551         try:
552             self._git_command('tag', args.args)
553         except GitRepositoryError:
554             return False
555         return True
556
557 #}
558     def force_head(self, commit, hard=False):
559         """
560         Force HEAD to a specific commit
561
562         @param commit: commit to move HEAD to
563         @param hard: also update the working copy
564         @type hard: C{bool}
565         """
566         if not GitCommit.is_sha1(commit):
567             commit = self.rev_parse(commit)
568
569         if self.bare:
570             ref = "refs/heads/%s" % self.get_branch()
571             self._git_command("update-ref", [ ref, commit ])
572         else:
573             args = ['--quiet']
574             if hard:
575                 args += [ '--hard' ]
576             args += [ commit, '--' ]
577             self._git_command("reset", args)
578
579     def is_clean(self, ignore_untracked=False):
580         """
581         Does the repository contain any uncommitted modifications?
582
583         @param ignore_untracked: whether to ignore untracked files when
584             checking the repository status
585         @type ignore_untracked: C{bool}
586         @return: C{True} if the repository is clean, C{False} otherwise
587             and Git's status message
588         @rtype: C{tuple}
589         """
590         if self.bare:
591             return (True, '')
592
593         clean_msg = 'nothing to commit'
594
595         args = GitArgs()
596         args.add_true(ignore_untracked, '-uno')
597
598         out, ret = self._git_getoutput('status',
599                                        args.args,
600                                        extra_env={'LC_ALL': 'C'})
601         if ret:
602             raise GbpError("Can't get repository status")
603         ret = False
604         for line in out:
605             if line.startswith('#'):
606                 continue
607             if line.startswith(clean_msg):
608                 ret = True
609             break
610         return (ret, "".join(out))
611
612     def is_empty(self):
613         """
614         Is the repository empty?
615
616         @return: True if the repositorydoesn't have any commits,
617             False otherwise
618         @rtype: C{bool}
619         """
620         # an empty repo has no branches:
621         return False if self.branch else True
622
623     def rev_parse(self, name, short=0):
624         """
625         Find the SHA1 of a given name
626
627         @param name: the name to look for
628         @type name: C{str}
629         @param short:  try to abbreviate SHA1 to given length
630         @type short: C{int}
631         @return: the name's sha1
632         @rtype: C{str}
633         """
634         args = GitArgs("--quiet", "--verify")
635         args.add_cond(short, '--short=%d' % short)
636         args.add(name)
637         sha, ret = self._git_getoutput('rev-parse', args.args)
638         if ret:
639             raise GitRepositoryError("revision '%s' not found" % name)
640         return sha[0].strip()
641
642 #{ Trees
643     def checkout(self, treeish):
644         """
645         Checkout treeish
646
647         @param treeish: the treeish to check out
648         @type treeish: C{str}
649         """
650         self._git_command("checkout", ["--quiet", treeish])
651
652     def has_treeish(self, treeish):
653         """
654         Check if the repository has the treeish object I{treeish}.
655
656         @param treeish: treeish object to look for
657         @type treeish: C{str}
658         @return: C{True} if the repository has that tree, C{False} otherwise
659         @rtype: C{bool}
660         """
661         out, ret =  self._git_getoutput('ls-tree', [ treeish ])
662         return [ True, False ][ret != 0]
663
664     def write_tree(self, index_file=None):
665         """
666         Create a tree object from the current index
667
668         @param index_file: alternate index file to read changes from
669         @type index_file: C{str}
670         @return: the new tree object's sha1
671         @rtype: C{str}
672         """
673         if index_file:
674             extra_env = {'GIT_INDEX_FILE': index_file }
675         else:
676             extra_env = None
677
678         tree, ret = self._git_getoutput('write-tree', extra_env=extra_env)
679         if ret:
680             raise GitRepositoryError("Can't write out current index")
681         return tree[0].strip()
682
683     def make_tree(self, contents):
684         """
685         Create a tree based on contents. I{contents} has the same format than
686         the I{GitRepository.list_tree} output.
687         """
688         out=''
689         args = GitArgs('-z')
690
691         for obj in contents:
692              mode, type, sha1, name = obj
693              out += '%s %s %s\t%s\0' % (mode, type, sha1, name)
694
695         sha1, err, ret =  self._git_inout('mktree',
696                                           args.args,
697                                           out,
698                                           capture_stderr=True)
699         if ret:
700             raise GitRepositoryError("Failed to mktree: '%s'" % err)
701         return sha1.strip()
702
703     def get_obj_type(self, obj):
704         """
705         Get type of a git repository object
706
707         @param obj: repository object
708         @type obj: C{str}
709         @return: type of the repository object
710         @rtype: C{str}
711         """
712         out, ret = self._git_getoutput('cat-file', args=['-t', obj])
713         if ret:
714             raise GitRepositoryError("Not a Git repository object: '%s'" % obj)
715         return out[0].strip()
716
717     def list_tree(self, treeish, recurse=False):
718         """
719         Get a trees content. It returns a list of objects that match the
720         'ls-tree' output: [ mode, type, sha1, path ].
721
722         @param treeish: the treeish object to list
723         @type treeish: C{str}
724         @param recurse: whether to list the tree recursively
725         @type recurse: C{bool}
726         @return: the tree
727         @rtype: C{list} of objects. See above.
728         """
729         args = GitArgs('-z')
730         args.add_true(recurse, '-r')
731         args.add(treeish)
732
733         out, err, ret =  self._git_inout('ls-tree', args.args, capture_stderr=True)
734         if ret:
735             raise GitRepositoryError("Failed to ls-tree '%s': '%s'" % (treeish, err))
736
737         tree = []
738         for line in out.split('\0'):
739             if line:
740                 tree.append(line.split(None, 3))
741         return tree
742
743 #}
744
745     def get_config(self, name):
746         """
747         Gets the config value associated with I{name}
748
749         @param name: config value to get
750         @return: fetched config value
751         @rtype: C{str}
752         """
753         value, ret = self._git_getoutput('config', [ name ])
754         if ret: raise KeyError
755         return value[0][:-1] # first line with \n ending removed
756
757     def get_author_info(self):
758         """
759         Determine a sane values for author name and author email from git's
760         config and environment variables.
761
762         @return: name and email
763         @rtype: L{GitModifier}
764         """
765         try:
766            name =  self.get_config("user.name")
767         except KeyError:
768            name = os.getenv("USER")
769         try:
770            email =  self.get_config("user.email")
771         except KeyError:
772             email = os.getenv("EMAIL")
773         email = os.getenv("GIT_AUTHOR_EMAIL", email)
774         name = os.getenv("GIT_AUTHOR_NAME", name)
775         return GitModifier(name, email)
776
777 #{ Remote Repositories
778
779     def get_remote_repos(self):
780         """
781         Get all remote repositories
782
783         @return: remote repositories
784         @rtype: C{list} of C{str}
785         """
786         out = self._git_getoutput('remote')[0]
787         return [ remote.strip() for remote in out ]
788
789     def has_remote_repo(self, name):
790         """
791         Do we know about a remote named I{name}?
792
793         @param name: name of the remote repository
794         @type name: C{str}
795         @return: C{True} if the remote repositore is known, C{False} otherwise
796         @rtype: C{bool}
797         """
798         if name in self.get_remote_repos():
799             return True
800         else:
801             return False
802
803     def add_remote_repo(self, name, url, tags=True, fetch=False):
804         """
805         Add a tracked remote repository
806
807         @param name: the name to use for the remote
808         @type name: C{str}
809         @param url: the url to add
810         @type url: C{str}
811         @param tags: whether to fetch tags
812         @type tags: C{bool}
813         @param fetch: whether to fetch immediately from the remote side
814         @type fetch: C{bool}
815         """
816         args = GitArgs('add')
817         args.add_false(tags, '--no-tags')
818         args.add_true(fetch, '--fetch')
819         args.add(name, url)
820         self._git_command("remote", args.args)
821
822     def remove_remote_repo(self, name):
823         args = GitArgs('rm', name)
824         self._git_command("remote", args.args)
825
826     def fetch(self, repo=None, tags=False, depth=0):
827         """
828         Download objects and refs from another repository.
829
830         @param repo: repository to fetch from
831         @type repo: C{str}
832         @param tags: whether to fetch all tag objects
833         @type tags: C{bool}
834         @param depth: deepen the history of (shallow) repository to depth I{depth}
835         @type depth: C{int}
836         """
837         args = GitArgs('--quiet')
838         args.add_true(tags, '--tags')
839         args.add_cond(depth, '--depth=%s' % depth)
840         args.add_cond(repo, repo)
841
842         self._git_command("fetch", args.args)
843
844     def pull(self, repo=None, ff_only=False):
845         """
846         Fetch and merge from another repository
847
848         @param repo: repository to fetch from
849         @type repo: C{str}
850         @param ff_only: only merge if this results in a fast forward merge
851         @type ff_only: C{bool}
852         """
853         args = []
854         args += [ '--ff-only' ] if ff_only else []
855         args += [ repo ] if repo else []
856         self._git_command("pull", args)
857
858     def push(self, repo=None, src=None, dst=None, ff_only=True):
859         """
860         Push changes to the remote repo
861
862         @param repo: repository to push to
863         @type repo: C{str}
864         @param src: the source ref to push
865         @type src: C{str}
866         @param dst: the name of the destination ref to push to
867         @type dst: C{str}
868         @param ff_only: only push if it's a fast forward update
869         @type ff_only: C{bool}
870         """
871         args = GitArgs()
872         args.add_cond(repo, repo)
873
874         # Allow for src == '' to delete dst on the remote
875         if src != None:
876             refspec = src
877             if dst:
878                 refspec += ':%s' % dst
879             if not ff_only:
880                 refspec = '+%s' % refspec
881             args.add(refspec)
882         self._git_command("push", args.args)
883
884     def push_tag(self, repo, tag):
885         """
886         Push a tag to the remote repo
887
888         @param repo: repository to push to
889         @type repo: C{str}
890         @param tag: the name of the tag
891         @type tag: C{str}
892         """
893         args = GitArgs(repo, 'tag', tag)
894         self._git_command("push", args.args)
895
896 #{ Files
897
898     def add_files(self, paths, force=False, index_file=None, work_tree=None):
899         """
900         Add files to a the repository
901
902         @param paths: list of files to add
903         @type paths: list or C{str}
904         @param force: add files even if they would be ignored by .gitignore
905         @type force: C{bool}
906         @param index_file: alternative index file to use
907         @param work_tree: alternative working tree to use
908         """
909         extra_env = {}
910
911         if isinstance(paths, basestring):
912             paths = [ paths ]
913
914         args = [ '-f' ] if force else []
915
916         if index_file:
917             extra_env['GIT_INDEX_FILE'] =  index_file
918
919         if work_tree:
920             extra_env['GIT_WORK_TREE'] = work_tree
921
922         self._git_command("add", args + paths, extra_env)
923
924     def remove_files(self, paths, verbose=False):
925         """
926         Remove files from the repository
927
928         @param paths: list of files to remove
929         @param paths: C{list} or C{str}
930         @param verbose: be verbose
931         @type verbose: C{bool}
932         """
933         if isinstance(paths, basestring):
934             paths = [ paths ]
935
936         args =  [] if verbose else ['--quiet']
937         self._git_command("rm", args + paths)
938
939     def list_files(self, types=['cached']):
940         """
941         List files in index and working tree
942
943         @param types: list of types to show
944         @type types: C{list}
945         @return: list of files
946         @rtype: C{list} of C{str}
947         """
948         all_types = [ 'cached', 'deleted', 'others', 'ignored',  'stage'
949                       'unmerged', 'killed', 'modified' ]
950         args = [ '-z' ]
951
952         for t in types:
953             if t in all_types:
954                 args += [ '--%s' % t ]
955             else:
956                 raise GitRepositoryError("Unknown type '%s'" % t)
957         out, ret = self._git_getoutput('ls-files', args)
958         if ret:
959             raise GitRepositoryError("Error listing files: '%d'" % ret)
960         if out:
961             return [ file for file in out[0].split('\0') if file ]
962         else:
963             return []
964
965
966     def write_file(self, filename, filters=True):
967         """
968         Hash a single file and write it into the object database
969
970         @param filename: the filename to the content of the file to hash
971         @type filename: C{str}
972         @param filters: whether to run filters
973         @type filters: C{bool}
974         @return: the hash of the file
975         @rtype: C{str}
976         """
977         args = GitArgs('-w', '-t', 'blob')
978         args.add_false(filters, '--no-filters')
979         args.add(filename)
980
981         sha1, stderr, ret = self._git_inout('hash-object',
982                                             args.args,
983                                             capture_stderr=True)
984         if not ret:
985             return sha1.strip()
986         else:
987             raise GbpError("Failed to hash %s: %s" % (filename, stderr))
988 #}
989
990 #{ Comitting
991
992     def _commit(self, msg, args=[], author_info=None):
993         extra_env = author_info.get_author_env() if author_info else None
994         self._git_command("commit", ['-q', '-m', msg] + args, extra_env=extra_env)
995
996     def commit_staged(self, msg, author_info=None, edit=False):
997         """
998         Commit currently staged files to the repository
999
1000         @param msg: commit message
1001         @type msg: C{str}
1002         @param author_info: authorship information
1003         @type author_info: L{GitModifier}
1004         @param edit: whether to spawn an editor to edit the commit info
1005         @type edit: C{bool}
1006         """
1007         args = GitArgs()
1008         args.add_true(edit,  '--edit')
1009         self._commit(msg=msg, args=args.args, author_info=author_info)
1010
1011     def commit_all(self, msg, author_info=None, edit=False):
1012         """
1013         Commit all changes to the repository
1014         @param msg: commit message
1015         @type msg: C{str}
1016         @param author_info: authorship information
1017         @type author_info: L{GitModifier}
1018         """
1019         args = GitArgs('-a')
1020         args.add_true(edit,  '--edit')
1021         self._commit(msg=msg, args=args.args, author_info=author_info)
1022
1023     def commit_files(self, files, msg, author_info=None):
1024         """
1025         Commit the given files to the repository
1026
1027         @param files: file or files to commit
1028         @type files: C{str} or C{list}
1029         @param msg: commit message
1030         @type msg: C{str}
1031         @param author_info: authorship information
1032         @type author_info: L{GitModifier}
1033         """
1034         if isinstance(files, basestring):
1035             files = [ files ]
1036         self._commit(msg=msg, args=files, author_info=author_info)
1037
1038     def commit_dir(self, unpack_dir, msg, branch, other_parents=None,
1039                    author={}, committer={}, create_missing_branch=False):
1040         """
1041         Replace the current tip of branch I{branch} with the contents from I{unpack_dir}
1042
1043         @param unpack_dir: content to add
1044         @type unpack_dir: C{str}
1045         @param msg: commit message to use
1046         @type msg: C{str}
1047         @param branch: branch to add the contents of unpack_dir to
1048         @type branch: C{str}
1049         @param other_parents: additional parents of this commit
1050         @type other_parents: C{list} of C{str}
1051         @param author: author information to use for commit
1052         @type author: C{dict} with keys I{name}, I{email}, I{date}
1053         @param committer: committer information to use for commit
1054         @type committer: C{dict} with keys I{name}, I{email}, I{date}
1055             or L{GitModifier}
1056         @param create_missing_branch: create I{branch} as detached branch if it
1057             doesn't already exist.
1058         @type create_missing_branch: C{bool}
1059         """
1060
1061         git_index_file = os.path.join(self.path, self._git_dir, 'gbp_index')
1062         try:
1063             os.unlink(git_index_file)
1064         except OSError:
1065             pass
1066         self.add_files('.', force=True, index_file=git_index_file,
1067                        work_tree=unpack_dir)
1068         tree = self.write_tree(git_index_file)
1069
1070         if branch:
1071             try:
1072                 cur = self.rev_parse(branch)
1073             except GitRepositoryError:
1074                 if create_missing_branch:
1075                     log.debug("Will create missing branch '%s'..." % branch)
1076                     cur = None
1077                 else:
1078                     raise
1079         else: # emtpy repo
1080             cur = None
1081             branch = 'master'
1082
1083         # Build list of parents:
1084         parents = []
1085         if cur:
1086             parents = [ cur ]
1087         if other_parents:
1088             for parent in other_parents:
1089                 sha = self.rev_parse(parent)
1090                 if sha not in parents:
1091                     parents += [ sha ]
1092
1093         commit = self.commit_tree(tree=tree, msg=msg, parents=parents,
1094                                   author=author, committer=committer)
1095         if not commit:
1096             raise GbpError("Failed to commit tree")
1097         self.update_ref("refs/heads/%s" % branch, commit, cur)
1098         return commit
1099
1100     def commit_tree(self, tree, msg, parents, author={}, committer={}):
1101         """
1102         Commit a tree with commit msg I{msg} and parents I{parents}
1103
1104         @param tree: tree to commit
1105         @param msg: commit message
1106         @param parents: parents of this commit
1107         @param author: authorship information
1108         @type author: C{dict} with keys 'name' and 'email' or L{GitModifier}
1109         @param committer: comitter information
1110         @type committer: C{dict} with keys 'name' and 'email'
1111         """
1112         extra_env = {}
1113         for key, val in author.items():
1114             if val:
1115                 extra_env['GIT_AUTHOR_%s' % key.upper()] = val
1116         for key, val in committer.items():
1117             if val:
1118                 extra_env['GIT_COMMITTER_%s' % key.upper()] = val
1119
1120         args = [ tree ]
1121         for parent in parents:
1122             args += [ '-p' , parent ]
1123         sha1, stderr, ret = self._git_inout('commit-tree',
1124                                             args,
1125                                             msg,
1126                                             extra_env,
1127                                             capture_stderr=True)
1128         if not ret:
1129             return sha1.strip()
1130         else:
1131             raise GbpError("Failed to commit tree: %s" % stderr)
1132
1133 #{ Commit Information
1134
1135     def get_commits(self, since=None, until=None, paths=None, num=0,
1136                     first_parent=False, options=None):
1137         """
1138         Get commits from since to until touching paths
1139
1140         @param since: commit to start from
1141         @type since: C{str}
1142         @param until: last commit to get
1143         @type until: C{str}
1144         @param paths: only list commits touching paths
1145         @type paths: C{list} of C{str}
1146         @param num: maximum number of commits to fetch
1147         @type num: C{int}
1148         @param options: list of additional options passed to git log
1149         @type  options: C{list} of C{str}ings
1150         @param first_parent: only follow first parent when seeing a
1151                              merge commit
1152         @type first_parent: C{bool}
1153         """
1154         args = GitArgs('--pretty=format:%H')
1155         args.add_true(num, '-%d' % num)
1156         args.add_true(first_parent, '--first-parent')
1157         args.add_true(since and until, '%s..%s' % (since, until))
1158         args.add_cond(options, options)
1159         args.add("--")
1160         if isinstance(paths, basestring):
1161             paths = [ paths ]
1162         args.add_cond(paths, paths)
1163
1164         commits, ret = self._git_getoutput('log', args.args)
1165         if ret:
1166             where = " on %s" % paths if paths else ""
1167             raise GitRepositoryError("Error getting commits %s..%s%s" %
1168                         (since, until, where))
1169         return [ commit.strip() for commit in commits ]
1170
1171     def show(self, id):
1172         """git-show id"""
1173         commit, ret = self._git_getoutput('show', [ "--pretty=medium", id ])
1174         if ret:
1175             raise GitRepositoryError("can't get %s" % id)
1176         for line in commit:
1177             yield line
1178
1179     def grep_log(self, regex, since=None):
1180         """
1181         Get commmits matching I{regex}
1182
1183         @param regex: regular expression
1184         @type regex: C{str}
1185         @param since: where to start grepping (e.g. a branch)
1186         @type since: C{str}
1187         """
1188         args = ['--pretty=format:%H']
1189         args.append("--grep=%s" % regex)
1190         if since:
1191             args.append(since)
1192         args.append('--')
1193
1194         commits, ret = self._git_getoutput('log', args)
1195         if ret:
1196             raise GitRepositoryError("Error grepping log for %s" % regex)
1197         return [ commit.strip() for commit in commits[::-1] ]
1198
1199     def get_subject(self, commit):
1200         """
1201         Gets the subject of a commit.
1202
1203         @param commit: the commit to get the subject from
1204         @return: the commit's subject
1205         @rtype: C{str}
1206         """
1207         out, ret =  self._git_getoutput('log', ['-n1', '--pretty=format:%s',  commit])
1208         if ret:
1209             raise GitRepositoryError("Error getting subject of commit %s"
1210                                      % commit)
1211         return out[0].strip()
1212
1213     def get_commit_info(self, commit):
1214         """
1215         Look up data of a specific commit
1216
1217         @param commit: the commit to inspect
1218         @return: the commit's including id, author, email, subject and body
1219         @rtype: dict
1220         """
1221         args = GitArgs('--pretty=format:%an%x00%ae%x00%ad%x00%cn%x00%ce%x00%cd%x00%s%x00%b%x00',
1222                        '-z', '--date=raw', '--name-status', commit)
1223         out, err, ret =  self._git_inout('show', args.args)
1224         if ret:
1225             raise GitRepositoryError("Unable to retrieve commit info for %s"
1226                                      % commit)
1227
1228         fields = out.split('\x00')
1229
1230         author = GitModifier(fields[0].strip(),
1231                              fields[1].strip(),
1232                              fields[2].strip())
1233         committer = GitModifier(fields[3].strip(),
1234                                 fields[4].strip(),
1235                                 fields[5].strip())
1236
1237         files = defaultdict(list)
1238         file_fields = fields[8:]
1239         # For some reason git returns one extra empty field for merge commits
1240         if file_fields[0] == '': file_fields.pop(0)
1241         while len(file_fields) and file_fields[0] != '':
1242             status = file_fields.pop(0).strip()
1243             path = file_fields.pop(0)
1244             files[status].append(path)
1245
1246         return {'id' : commit,
1247                 'author' : author,
1248                 'committer' : committer,
1249                 'subject' : fields[6],
1250                 'body' : fields[7],
1251                 'files' : files}
1252
1253 #{ Patches
1254     def format_patches(self, start, end, output_dir, signature=True, thread=None):
1255         """
1256         Output the commits between start and end as patches in output_dir
1257         """
1258         options = GitArgs('-N', '-k',
1259                           '-o', output_dir)
1260         options.add_cond(not signature, '--no-signature')
1261         options.add('%s...%s' % (start, end))
1262         options.add_cond(thread, '--thread=%s' % thread, '--no-thread')
1263
1264         output, ret = self._git_getoutput('format-patch', options.args)
1265         return [ line.strip() for line in output ]
1266
1267     def apply_patch(self, patch, index=True, context=None, strip=None):
1268         """Apply a patch using git apply"""
1269         args = []
1270         if context:
1271             args += [ '-C', context ]
1272         if index:
1273             args.append("--index")
1274         if strip != None:
1275             args += [ '-p', str(strip) ]
1276         args.append(patch)
1277         self._git_command("apply", args)
1278
1279     def diff(self, obj1, obj2):
1280         """
1281         Diff two git repository objects
1282
1283         @param obj1: first object
1284         @type obj1: C{str}
1285         @param obj2: second object
1286         @type obj2: C{str}
1287         @return: diff
1288         @rtype: C{str}
1289         """
1290         options = GitArgs(obj1, obj2)
1291         output, ret = self._git_getoutput('diff', options.args)
1292         return output
1293 #}
1294
1295     def archive(self, format, prefix, output, treeish, **kwargs):
1296         """
1297         Create an archive from a treeish
1298
1299         @param format: the type of archive to create, e.g. 'tar.gz'
1300         @type format: C{str}
1301         @param prefix: prefix to prepend to each filename in the archive
1302         @type prefix: C{str}
1303         @param output: the name of the archive to create
1304         @type output: C{str}
1305         @param treeish: the treeish to create the archive from
1306         @type treeish: C{str}
1307         @param kwargs: additional commandline options passed to git-archive
1308         """
1309         args = [ '--format=%s' % format, '--prefix=%s' % prefix,
1310                  '--output=%s' % output, treeish ]
1311         out, ret = self._git_getoutput('archive', args, **kwargs)
1312         if ret:
1313             raise GitRepositoryError("Unable to archive %s" % treeish)
1314
1315     def collect_garbage(self, auto=False):
1316         """
1317         Cleanup unnecessary files and optimize the local repository
1318
1319         param auto: only cleanup if required
1320         param auto: C{bool}
1321         """
1322         args = [ '--auto' ] if auto else []
1323         self._git_command("gc", args)
1324
1325 #{ Submodules
1326
1327     def has_submodules(self):
1328         """
1329         Does the repo have any submodules?
1330
1331         @return: C{True} if the repository has any submodules, C{False}
1332             otherwise
1333         @rtype: C{bool}
1334         """
1335         if os.path.exists('.gitmodules'):
1336             return True
1337         else:
1338             return False
1339
1340
1341     def add_submodule(self, repo_path):
1342         """
1343         Add a submodule
1344
1345         @param repo_path: path to submodule
1346         @type repo_path: C{str}
1347         """
1348         self._git_command("submodule", [ "add", repo_path ])
1349
1350
1351     def update_submodules(self, init=True, recursive=True, fetch=False):
1352         """
1353         Update all submodules
1354
1355         @param init: whether to initialize the submodule if necessary
1356         @type init: C{bool}
1357         @param recursive: whether to update submodules recursively
1358         @type recursive: C{bool}
1359         @param fetch: whether to fetch new objects
1360         @type fetch: C{bool}
1361         """
1362
1363         if not self.has_submodules():
1364             return
1365         args = [ "update" ]
1366         if recursive:
1367             args.append("--recursive")
1368         if init:
1369             args.append("--init")
1370         if not fetch:
1371             args.append("--no-fetch")
1372
1373         self._git_command("submodule", args)
1374
1375
1376     def get_submodules(self, treeish, path=None, recursive=True):
1377         """
1378         List the submodules of treeish
1379
1380         @return: a list of submodule/commit-id tuples
1381         @rtype: list of tuples
1382         """
1383         # Note that we is lstree instead of submodule commands because
1384         # there's no way to list the submodules of another branch with
1385         # the latter.
1386         submodules = []
1387         if path is None:
1388             path = "."
1389
1390         args = [ treeish ]
1391         if recursive:
1392             args += ['-r']
1393
1394         out, ret =  self._git_getoutput('ls-tree', args, cwd=path)
1395         for line in out:
1396             mode, objtype, commit, name = line[:-1].split(None, 3)
1397             # A submodules is shown as "commit" object in ls-tree:
1398             if objtype == "commit":
1399                 nextpath = os.path.sep.join([path, name])
1400                 submodules.append( (nextpath, commit) )
1401                 if recursive:
1402                     submodules += self.get_submodules(commit, path=nextpath,
1403                                                       recursive=recursive)
1404         return submodules
1405
1406 #{ Repository Creation
1407
1408     @classmethod
1409     def create(klass, path, description=None, bare=False):
1410         """
1411         Create a repository at path
1412
1413         @param path: where to create the repository
1414         @type path: C{str}
1415         @param bare: whether to create a bare repository
1416         @type bare: C{bool}
1417         @return: git repository object
1418         @rtype: L{GitRepository}
1419         """
1420         args = GitArgs()
1421         abspath = os.path.abspath(path)
1422
1423         args.add_true(bare, '--bare')
1424         git_dir = '' if bare else '.git'
1425
1426         try:
1427             if not os.path.exists(abspath):
1428                 os.makedirs(abspath)
1429             try:
1430                 GitCommand("init", args.args, cwd=abspath)()
1431             except CommandExecFailed as excobj:
1432                 raise GitRepositoryError("Error running git init: %s" % excobj)
1433
1434             if description:
1435                 with file(os.path.join(abspath, git_dir, "description"), 'w') as f:
1436                     description += '\n' if description[-1] != '\n' else ''
1437                     f.write(description)
1438             return klass(abspath)
1439         except OSError as err:
1440             raise GitRepositoryError("Cannot create Git repository at '%s': %s"
1441                                      % (abspath, err[1]))
1442         return None
1443
1444     @classmethod
1445     def clone(klass, path, remote, depth=0, recursive=False, mirror=False,
1446               bare=False, auto_name=True):
1447         """
1448         Clone a git repository at I{remote} to I{path}.
1449
1450         @param path: where to clone the repository to
1451         @type path: C{str}
1452         @param remote: URL to clone
1453         @type remote: C{str}
1454         @param depth: create a shallow clone of depth I{depth}
1455         @type depth: C{int}
1456         @param recursive: whether to clone submodules
1457         @type recursive: C{bool}
1458         @param mirror: whether to pass --mirror to git-clone
1459         @type mirror: C{bool}
1460         @param bare: whether to create a bare repository
1461         @type bare: C{bool}
1462         @param auto_name: If I{True} create a directory below I{path} based on
1463             the I{remote}s name. Otherwise create the repo directly at I{path}.
1464         @type auto_name: C{bool}
1465         @return: git repository object
1466         @rtype: L{GitRepository}
1467         """
1468         abspath = os.path.abspath(path)
1469         if auto_name:
1470             name = None
1471         else:
1472             abspath, name = abspath.rsplit('/', 1)
1473
1474         args = GitArgs('--quiet')
1475         args.add_true(depth,  '--depth', depth)
1476         args.add_true(recursive, '--recursive')
1477         args.add_true(mirror, '--mirror')
1478         args.add_true(bare, '--bare')
1479         args.add(remote)
1480         args.add_true(name, name)
1481         try:
1482             if not os.path.exists(abspath):
1483                 os.makedirs(abspath)
1484
1485             try:
1486                 GitCommand("clone", args.args, cwd=abspath)()
1487             except CommandExecFailed as excobj:
1488                 raise GitRepositoryError("Error running git clone: %s" %
1489                                          excobj)
1490
1491             if not name:
1492                 name = remote.rstrip('/').rsplit('/',1)[1]
1493                 if (mirror or bare):
1494                     if not name.endswith('.git'):
1495                         name = "%s.git" % name
1496                 elif name.endswith('.git'):
1497                     name = name[:-4]
1498             return klass(os.path.join(abspath, name))
1499         except OSError as err:
1500             raise GitRepositoryError("Cannot clone Git repository "
1501                                      "'%s' to '%s': %s"
1502                                      % (remote, abspath, err[1]))
1503         return None
1504 #}
1505