1 # vim: set fileencoding=utf-8 :
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.
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.
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"""
22 from collections import defaultdict
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
33 class GitRepositoryError(GitError):
34 """Exception thrown by L{GitRepository}"""
38 class GitRepository(object):
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}.
43 @ivar _path: The path to the working tree
45 @ivar _bare: Whether this is a bare repository
47 @raises GitRepositoryError: on git errors GitRepositoryError is raised by
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'],
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'
61 def __init__(self, path):
62 self._path = os.path.abspath(path)
65 out, dummy, ret = self._git_inout('rev-parse', ['--show-cdup'],
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
72 raise GitRepositoryError("No Git repository at '%s'" % self.path)
75 def __build_env(self, extra_env):
76 """Prepare environment for subprocess calls"""
78 if extra_env is not None:
79 env = os.environ.copy()
83 def _git_getoutput(self, command, args=[], extra_env=None, cwd=None):
85 Run a git command and return the output
87 @param command: git command to run
89 @param args: list of arguments
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}
95 @return: stdout, return code
96 @rtype: C{tuple} of C{list} of C{str} and C{int}
98 @deprecated: use L{gbp.git.repository.GitRepository._git_inout} instead.
105 env = self.__build_env(extra_env)
106 cmd = ['git', command] + args
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
114 def _git_inout(self, command, args, input=None, extra_env=None, cwd=None,
115 capture_stderr=False):
117 Run a git command with input and return output
119 @param command: git command to run
120 @type command: C{str}
121 @param input: input to pipe to command
123 @param args: list of arguments
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}
135 stderr_arg = subprocess.PIPE if capture_stderr else None
137 env = self.__build_env(extra_env)
138 cmd = ['git', command] + args
140 popen = subprocess.Popen(cmd,
141 stdin=subprocess.PIPE,
142 stdout=subprocess.PIPE,
146 (stdout, stderr) = popen.communicate(input)
147 return stdout, stderr, popen.returncode
149 def _git_command(self, command, args=[], extra_env=None):
151 Execute git command with arguments args and environment env
154 @param command: git command
155 @type command: C{str}
156 @param args: command line arguments
158 @param extra_env: extra environment variables to set when running command
159 @type extra_env: C{dict}
162 GitCommand(command, args, extra_env=extra_env, cwd=self.path)()
163 except CommandExecFailed as excobj:
164 raise GitRepositoryError("Error running git %s: %s" %
169 """The absolute path to the repository"""
174 """The absolute path to git's metadata"""
175 return os.path.join(self.path, self._git_dir)
179 """Wheter this is a bare repository"""
184 """List of all tags in the repository"""
185 return self.get_tags()
189 """The currently checked out branch"""
191 return self.get_branch()
192 except GitRepositoryError:
197 """return the SHA1 of the current HEAD"""
198 return self.rev_parse('HEAD')
200 #{ Branches and Merging
201 def rename_branch(self, branch, newbranch):
205 @param branch: name of the branch to be renamed
206 @param newbranch: new name of the branch
208 args = GitArgs("-m", branch, newbranch)
209 self._git_command("branch", args.args)
211 def create_branch(self, branch, rev=None):
215 @param branch: the branch's name
216 @param rev: where to start the branch from
218 If rev is None the branch starts form the current HEAD.
220 args = GitArgs(branch)
221 args.add_true(rev, rev)
222 self._git_command("branch", args.args)
224 def delete_branch(self, branch, remote=False):
226 Delete branch I{branch}
228 @param branch: name of the branch to delete
230 @param remote: delete a remote branch
231 @param remote: C{bool}
234 args.add_true(remote, '-r')
237 if self.branch != branch:
238 self._git_command("branch", args.args)
240 raise GitRepositoryError("Can't delete the branch you're on")
242 def get_branch(self):
244 On what branch is the current working copy
246 @return: current branch
249 out, ret = self._git_getoutput('symbolic-ref', [ 'HEAD' ])
251 raise GitRepositoryError("Currently not on a branch")
254 # Check if ref really exists
255 failed = self._git_getoutput('show-ref', [ ref ])[1]
257 return ref[11:] # strip /refs/heads
259 def has_branch(self, branch, remote=False):
261 Check if the repository has branch named I{branch}.
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
270 ref = 'refs/remotes/%s' % branch
272 ref = 'refs/heads/%s' % branch
273 failed = self._git_getoutput('show-ref', [ ref ])[1]
278 def set_branch(self, branch):
280 Switch to branch I{branch}
282 @param branch: name of the branch to switch to
285 if self.branch == branch:
289 self._git_command("symbolic-ref",
290 [ 'HEAD', 'refs/heads/%s' % branch ])
292 self._git_command("checkout", [ branch ])
294 def get_merge_branch(self, branch):
296 Get the branch we'd merge from
298 @return: repo and branch we would merge from
302 remote = self.get_config("branch.%s.remote" % branch)
303 merge = self.get_config("branch.%s.merge" % branch)
306 remote += merge.replace("refs/heads","", 1)
309 def get_merge_base(self, commit1, commit2):
311 Get the common ancestor between two commits
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
323 sha1, stderr, ret = self._git_inout('merge-base', args.args, capture_stderr=True)
327 raise GitRepositoryError("Failed to get common ancestor: %s" % stderr.strip())
329 def merge(self, commit, verbose=False, edit=False):
331 Merge changes from the named commit into the current branch
333 @param commit: the commit to merge from (usually a branch name or tag)
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
341 args.add_cond(verbose, '--summary', '--no-summary')
342 args.add_cond(edit, '--edit', '--no-edit')
344 self._git_command("merge", args.args)
346 def is_fast_forward(self, from_branch, to_branch):
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.
351 @return: can_fast_forward, up_to_date
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),
360 if not out: # both branches have the same commits
364 if line.startswith("<"):
366 elif line.startswith(">"):
369 if has_local and has_remote:
376 def _get_branches(self, remote=False):
378 Get a list of branches
380 @param remote: whether to list local or remote branches
381 @type remote: C{bool}
382 @return: local or remote branches
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 ]
390 def get_local_branches(self):
392 Get a list of local branches
394 @return: local branches
397 return self._get_branches(remote=False)
400 def get_remote_branches(self):
402 Get a list of remote branches
404 @return: remote branches
407 return self._get_branches(remote=True)
409 def update_ref(self, ref, new, old=None, msg=None):
411 Update ref I{ref} to commit I{new} if I{ref} currently points to
414 @param ref: the ref to update
416 @param new: the new value for ref
418 @param old: the old value of ref
420 @param msg: the reason for the update
427 args = [ '-m', msg ] + args
428 self._git_command("update-ref", args)
430 def branch_contains(self, branch, commit, remote=False):
432 Check if branch I{branch} contains commit I{commit}
434 @param branch: the branch the commit should be on
436 @param commit: the C{str} commit to check
438 @param remote: whether to check remote instead of local branches
439 @type remote: C{bool}
442 args.add_true(remote, '-r')
443 args.add('--contains')
446 out, ret = self._git_getoutput('branch', args.args)
448 # remove prefix '*' for current branch before comparing
449 line = line.replace('*', '')
450 if line.strip() == branch:
456 def create_tag(self, name, msg=None, commit=None, sign=False, keyid=None):
460 @param name: the tag's name
462 @param msg: The tag message.
464 @param commit: the commit or object to create the tag at, default
467 @param sign: Whether to sing the tag
469 @param keyid: the GPG keyid used to sign the tag
473 args += [ '-m', msg ] if msg else []
476 args += [ '-u', keyid ] if keyid else []
478 args += [ commit ] if commit else []
479 self._git_command("tag", args)
481 def delete_tag(self, tag):
483 Delete a tag named I{tag}
485 @param tag: the tag to delete
488 if self.has_tag(tag):
489 self._git_command("tag", [ "-d", tag ])
491 def move_tag(self, old, new):
492 self._git_command("tag", [ new, old ])
495 def has_tag(self, tag):
497 Check if the repository has a tag named I{tag}.
499 @param tag: tag to look for
501 @return: C{True} if the repository has that tag, C{False} otherwise
504 out, ret = self._git_getoutput('tag', [ '-l', tag ])
505 return [ False, True ][len(out)]
507 def find_tag(self, commit, pattern=None):
509 Find the closest tag to a given commit
511 @param commit: the commit to describe
513 @param pattern: only look for tags matching I{pattern}
514 @type pattern: C{str}
515 @return: the found tag
518 args = [ '--abbrev=0' ]
520 args += [ '--match' , pattern ]
523 tag, ret = self._git_getoutput('describe', args)
525 raise GitRepositoryError("Can't find tag for %s" % commit)
526 return tag[0].strip()
528 def get_tags(self, pattern=None):
532 @param pattern: only list tags matching I{pattern}
533 @type pattern: C{str}
535 @rtype: C{list} of C{str}
537 args = [ '-l', pattern ] if pattern else []
538 return [ line.strip() for line in self._git_getoutput('tag', args)[0] ]
540 def verify_tag(self, tag):
544 @param tag: the tag's name
546 @return: Whether the signature on the tag could be verified
549 args = GitArgs('-v', tag)
552 self._git_command('tag', args.args)
553 except GitRepositoryError:
558 def force_head(self, commit, hard=False):
560 Force HEAD to a specific commit
562 @param commit: commit to move HEAD to
563 @param hard: also update the working copy
566 if not GitCommit.is_sha1(commit):
567 commit = self.rev_parse(commit)
570 ref = "refs/heads/%s" % self.get_branch()
571 self._git_command("update-ref", [ ref, commit ])
576 args += [ commit, '--' ]
577 self._git_command("reset", args)
579 def is_clean(self, ignore_untracked=False):
581 Does the repository contain any uncommitted modifications?
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
593 clean_msg = 'nothing to commit'
596 args.add_true(ignore_untracked, '-uno')
598 out, ret = self._git_getoutput('status',
600 extra_env={'LC_ALL': 'C'})
602 raise GbpError("Can't get repository status")
605 if line.startswith('#'):
607 if line.startswith(clean_msg):
610 return (ret, "".join(out))
614 Is the repository empty?
616 @return: True if the repositorydoesn't have any commits,
620 # an empty repo has no branches:
621 return False if self.branch else True
623 def rev_parse(self, name, short=0):
625 Find the SHA1 of a given name
627 @param name: the name to look for
629 @param short: try to abbreviate SHA1 to given length
631 @return: the name's sha1
634 args = GitArgs("--quiet", "--verify")
635 args.add_cond(short, '--short=%d' % short)
637 sha, ret = self._git_getoutput('rev-parse', args.args)
639 raise GitRepositoryError("revision '%s' not found" % name)
640 return sha[0].strip()
643 def checkout(self, treeish):
647 @param treeish: the treeish to check out
648 @type treeish: C{str}
650 self._git_command("checkout", ["--quiet", treeish])
652 def has_treeish(self, treeish):
654 Check if the repository has the treeish object I{treeish}.
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
661 out, ret = self._git_getoutput('ls-tree', [ treeish ])
662 return [ True, False ][ret != 0]
664 def write_tree(self, index_file=None):
666 Create a tree object from the current index
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
674 extra_env = {'GIT_INDEX_FILE': index_file }
678 tree, ret = self._git_getoutput('write-tree', extra_env=extra_env)
680 raise GitRepositoryError("Can't write out current index")
681 return tree[0].strip()
683 def make_tree(self, contents):
685 Create a tree based on contents. I{contents} has the same format than
686 the I{GitRepository.list_tree} output.
692 mode, type, sha1, name = obj
693 out += '%s %s %s\t%s\0' % (mode, type, sha1, name)
695 sha1, err, ret = self._git_inout('mktree',
700 raise GitRepositoryError("Failed to mktree: '%s'" % err)
703 def get_obj_type(self, obj):
705 Get type of a git repository object
707 @param obj: repository object
709 @return: type of the repository object
712 out, ret = self._git_getoutput('cat-file', args=['-t', obj])
714 raise GitRepositoryError("Not a Git repository object: '%s'" % obj)
715 return out[0].strip()
717 def list_tree(self, treeish, recurse=False):
719 Get a trees content. It returns a list of objects that match the
720 'ls-tree' output: [ mode, type, sha1, path ].
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}
727 @rtype: C{list} of objects. See above.
730 args.add_true(recurse, '-r')
733 out, err, ret = self._git_inout('ls-tree', args.args, capture_stderr=True)
735 raise GitRepositoryError("Failed to ls-tree '%s': '%s'" % (treeish, err))
738 for line in out.split('\0'):
740 tree.append(line.split(None, 3))
745 def get_config(self, name):
747 Gets the config value associated with I{name}
749 @param name: config value to get
750 @return: fetched config value
753 value, ret = self._git_getoutput('config', [ name ])
754 if ret: raise KeyError
755 return value[0][:-1] # first line with \n ending removed
757 def get_author_info(self):
759 Determine a sane values for author name and author email from git's
760 config and environment variables.
762 @return: name and email
763 @rtype: L{GitModifier}
766 name = self.get_config("user.name")
768 name = os.getenv("USER")
770 email = self.get_config("user.email")
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)
777 #{ Remote Repositories
779 def get_remote_repos(self):
781 Get all remote repositories
783 @return: remote repositories
784 @rtype: C{list} of C{str}
786 out = self._git_getoutput('remote')[0]
787 return [ remote.strip() for remote in out ]
789 def has_remote_repo(self, name):
791 Do we know about a remote named I{name}?
793 @param name: name of the remote repository
795 @return: C{True} if the remote repositore is known, C{False} otherwise
798 if name in self.get_remote_repos():
803 def add_remote_repo(self, name, url, tags=True, fetch=False):
805 Add a tracked remote repository
807 @param name: the name to use for the remote
809 @param url: the url to add
811 @param tags: whether to fetch tags
813 @param fetch: whether to fetch immediately from the remote side
816 args = GitArgs('add')
817 args.add_false(tags, '--no-tags')
818 args.add_true(fetch, '--fetch')
820 self._git_command("remote", args.args)
822 def remove_remote_repo(self, name):
823 args = GitArgs('rm', name)
824 self._git_command("remote", args.args)
826 def fetch(self, repo=None, tags=False, depth=0):
828 Download objects and refs from another repository.
830 @param repo: repository to fetch from
832 @param tags: whether to fetch all tag objects
834 @param depth: deepen the history of (shallow) repository to depth I{depth}
837 args = GitArgs('--quiet')
838 args.add_true(tags, '--tags')
839 args.add_cond(depth, '--depth=%s' % depth)
840 args.add_cond(repo, repo)
842 self._git_command("fetch", args.args)
844 def pull(self, repo=None, ff_only=False):
846 Fetch and merge from another repository
848 @param repo: repository to fetch from
850 @param ff_only: only merge if this results in a fast forward merge
851 @type ff_only: C{bool}
854 args += [ '--ff-only' ] if ff_only else []
855 args += [ repo ] if repo else []
856 self._git_command("pull", args)
858 def push(self, repo=None, src=None, dst=None, ff_only=True):
860 Push changes to the remote repo
862 @param repo: repository to push to
864 @param src: the source ref to push
866 @param dst: the name of the destination ref to push to
868 @param ff_only: only push if it's a fast forward update
869 @type ff_only: C{bool}
872 args.add_cond(repo, repo)
874 # Allow for src == '' to delete dst on the remote
878 refspec += ':%s' % dst
880 refspec = '+%s' % refspec
882 self._git_command("push", args.args)
884 def push_tag(self, repo, tag):
886 Push a tag to the remote repo
888 @param repo: repository to push to
890 @param tag: the name of the tag
893 args = GitArgs(repo, 'tag', tag)
894 self._git_command("push", args.args)
898 def add_files(self, paths, force=False, index_file=None, work_tree=None):
900 Add files to a the repository
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
906 @param index_file: alternative index file to use
907 @param work_tree: alternative working tree to use
911 if isinstance(paths, basestring):
914 args = [ '-f' ] if force else []
917 extra_env['GIT_INDEX_FILE'] = index_file
920 extra_env['GIT_WORK_TREE'] = work_tree
922 self._git_command("add", args + paths, extra_env)
924 def remove_files(self, paths, verbose=False):
926 Remove files from the repository
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}
933 if isinstance(paths, basestring):
936 args = [] if verbose else ['--quiet']
937 self._git_command("rm", args + paths)
939 def list_files(self, types=['cached']):
941 List files in index and working tree
943 @param types: list of types to show
945 @return: list of files
946 @rtype: C{list} of C{str}
948 all_types = [ 'cached', 'deleted', 'others', 'ignored', 'stage'
949 'unmerged', 'killed', 'modified' ]
954 args += [ '--%s' % t ]
956 raise GitRepositoryError("Unknown type '%s'" % t)
957 out, ret = self._git_getoutput('ls-files', args)
959 raise GitRepositoryError("Error listing files: '%d'" % ret)
961 return [ file for file in out[0].split('\0') if file ]
966 def write_file(self, filename, filters=True):
968 Hash a single file and write it into the object database
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
977 args = GitArgs('-w', '-t', 'blob')
978 args.add_false(filters, '--no-filters')
981 sha1, stderr, ret = self._git_inout('hash-object',
987 raise GbpError("Failed to hash %s: %s" % (filename, stderr))
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)
996 def commit_staged(self, msg, author_info=None, edit=False):
998 Commit currently staged files to the repository
1000 @param msg: commit message
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
1008 args.add_true(edit, '--edit')
1009 self._commit(msg=msg, args=args.args, author_info=author_info)
1011 def commit_all(self, msg, author_info=None, edit=False):
1013 Commit all changes to the repository
1014 @param msg: commit message
1016 @param author_info: authorship information
1017 @type author_info: L{GitModifier}
1019 args = GitArgs('-a')
1020 args.add_true(edit, '--edit')
1021 self._commit(msg=msg, args=args.args, author_info=author_info)
1023 def commit_files(self, files, msg, author_info=None):
1025 Commit the given files to the repository
1027 @param files: file or files to commit
1028 @type files: C{str} or C{list}
1029 @param msg: commit message
1031 @param author_info: authorship information
1032 @type author_info: L{GitModifier}
1034 if isinstance(files, basestring):
1036 self._commit(msg=msg, args=files, author_info=author_info)
1038 def commit_dir(self, unpack_dir, msg, branch, other_parents=None,
1039 author={}, committer={}, create_missing_branch=False):
1041 Replace the current tip of branch I{branch} with the contents from I{unpack_dir}
1043 @param unpack_dir: content to add
1044 @type unpack_dir: C{str}
1045 @param msg: commit message to use
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}
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}
1061 git_index_file = os.path.join(self.path, self._git_dir, 'gbp_index')
1063 os.unlink(git_index_file)
1066 self.add_files('.', force=True, index_file=git_index_file,
1067 work_tree=unpack_dir)
1068 tree = self.write_tree(git_index_file)
1072 cur = self.rev_parse(branch)
1073 except GitRepositoryError:
1074 if create_missing_branch:
1075 log.debug("Will create missing branch '%s'..." % branch)
1083 # Build list of parents:
1088 for parent in other_parents:
1089 sha = self.rev_parse(parent)
1090 if sha not in parents:
1093 commit = self.commit_tree(tree=tree, msg=msg, parents=parents,
1094 author=author, committer=committer)
1096 raise GbpError("Failed to commit tree")
1097 self.update_ref("refs/heads/%s" % branch, commit, cur)
1100 def commit_tree(self, tree, msg, parents, author={}, committer={}):
1102 Commit a tree with commit msg I{msg} and parents I{parents}
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'
1113 for key, val in author.items():
1115 extra_env['GIT_AUTHOR_%s' % key.upper()] = val
1116 for key, val in committer.items():
1118 extra_env['GIT_COMMITTER_%s' % key.upper()] = val
1121 for parent in parents:
1122 args += [ '-p' , parent ]
1123 sha1, stderr, ret = self._git_inout('commit-tree',
1127 capture_stderr=True)
1131 raise GbpError("Failed to commit tree: %s" % stderr)
1133 #{ Commit Information
1135 def get_commits(self, since=None, until=None, paths=None, num=0,
1136 first_parent=False, options=None):
1138 Get commits from since to until touching paths
1140 @param since: commit to start from
1142 @param until: last commit to get
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
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
1152 @type first_parent: C{bool}
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)
1160 if isinstance(paths, basestring):
1162 args.add_cond(paths, paths)
1164 commits, ret = self._git_getoutput('log', args.args)
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 ]
1173 commit, ret = self._git_getoutput('show', [ "--pretty=medium", id ])
1175 raise GitRepositoryError("can't get %s" % id)
1179 def grep_log(self, regex, since=None):
1181 Get commmits matching I{regex}
1183 @param regex: regular expression
1185 @param since: where to start grepping (e.g. a branch)
1188 args = ['--pretty=format:%H']
1189 args.append("--grep=%s" % regex)
1194 commits, ret = self._git_getoutput('log', args)
1196 raise GitRepositoryError("Error grepping log for %s" % regex)
1197 return [ commit.strip() for commit in commits[::-1] ]
1199 def get_subject(self, commit):
1201 Gets the subject of a commit.
1203 @param commit: the commit to get the subject from
1204 @return: the commit's subject
1207 out, ret = self._git_getoutput('log', ['-n1', '--pretty=format:%s', commit])
1209 raise GitRepositoryError("Error getting subject of commit %s"
1211 return out[0].strip()
1213 def get_commit_info(self, commit):
1215 Look up data of a specific commit
1217 @param commit: the commit to inspect
1218 @return: the commit's including id, author, email, subject and body
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)
1225 raise GitRepositoryError("Unable to retrieve commit info for %s"
1228 fields = out.split('\x00')
1230 author = GitModifier(fields[0].strip(),
1233 committer = GitModifier(fields[3].strip(),
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)
1246 return {'id' : commit,
1248 'committer' : committer,
1249 'subject' : fields[6],
1254 def format_patches(self, start, end, output_dir, signature=True, thread=None):
1256 Output the commits between start and end as patches in output_dir
1258 options = GitArgs('-N', '-k',
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')
1264 output, ret = self._git_getoutput('format-patch', options.args)
1265 return [ line.strip() for line in output ]
1267 def apply_patch(self, patch, index=True, context=None, strip=None):
1268 """Apply a patch using git apply"""
1271 args += [ '-C', context ]
1273 args.append("--index")
1275 args += [ '-p', str(strip) ]
1277 self._git_command("apply", args)
1279 def diff(self, obj1, obj2):
1281 Diff two git repository objects
1283 @param obj1: first object
1285 @param obj2: second object
1290 options = GitArgs(obj1, obj2)
1291 output, ret = self._git_getoutput('diff', options.args)
1295 def archive(self, format, prefix, output, treeish, **kwargs):
1297 Create an archive from a treeish
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
1309 args = [ '--format=%s' % format, '--prefix=%s' % prefix,
1310 '--output=%s' % output, treeish ]
1311 out, ret = self._git_getoutput('archive', args, **kwargs)
1313 raise GitRepositoryError("Unable to archive %s" % treeish)
1315 def collect_garbage(self, auto=False):
1317 Cleanup unnecessary files and optimize the local repository
1319 param auto: only cleanup if required
1322 args = [ '--auto' ] if auto else []
1323 self._git_command("gc", args)
1327 def has_submodules(self):
1329 Does the repo have any submodules?
1331 @return: C{True} if the repository has any submodules, C{False}
1335 if os.path.exists('.gitmodules'):
1341 def add_submodule(self, repo_path):
1345 @param repo_path: path to submodule
1346 @type repo_path: C{str}
1348 self._git_command("submodule", [ "add", repo_path ])
1351 def update_submodules(self, init=True, recursive=True, fetch=False):
1353 Update all submodules
1355 @param init: whether to initialize the submodule if necessary
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}
1363 if not self.has_submodules():
1367 args.append("--recursive")
1369 args.append("--init")
1371 args.append("--no-fetch")
1373 self._git_command("submodule", args)
1376 def get_submodules(self, treeish, path=None, recursive=True):
1378 List the submodules of treeish
1380 @return: a list of submodule/commit-id tuples
1381 @rtype: list of tuples
1383 # Note that we is lstree instead of submodule commands because
1384 # there's no way to list the submodules of another branch with
1394 out, ret = self._git_getoutput('ls-tree', args, cwd=path)
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) )
1402 submodules += self.get_submodules(commit, path=nextpath,
1403 recursive=recursive)
1406 #{ Repository Creation
1409 def create(klass, path, description=None, bare=False):
1411 Create a repository at path
1413 @param path: where to create the repository
1415 @param bare: whether to create a bare repository
1417 @return: git repository object
1418 @rtype: L{GitRepository}
1421 abspath = os.path.abspath(path)
1423 args.add_true(bare, '--bare')
1424 git_dir = '' if bare else '.git'
1427 if not os.path.exists(abspath):
1428 os.makedirs(abspath)
1430 GitCommand("init", args.args, cwd=abspath)()
1431 except CommandExecFailed as excobj:
1432 raise GitRepositoryError("Error running git init: %s" % excobj)
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]))
1445 def clone(klass, path, remote, depth=0, recursive=False, mirror=False,
1446 bare=False, auto_name=True):
1448 Clone a git repository at I{remote} to I{path}.
1450 @param path: where to clone the repository to
1452 @param remote: URL to clone
1453 @type remote: C{str}
1454 @param depth: create a shallow clone of depth I{depth}
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
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}
1468 abspath = os.path.abspath(path)
1472 abspath, name = abspath.rsplit('/', 1)
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')
1480 args.add_true(name, name)
1482 if not os.path.exists(abspath):
1483 os.makedirs(abspath)
1486 GitCommand("clone", args.args, cwd=abspath)()
1487 except CommandExecFailed as excobj:
1488 raise GitRepositoryError("Error running git clone: %s" %
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'):
1498 return klass(os.path.join(abspath, name))
1499 except OSError as err:
1500 raise GitRepositoryError("Cannot clone Git repository "
1502 % (remote, abspath, err[1]))