1 # vim: set fileencoding=utf-8 :
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.
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.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
32 class GitRepositoryError(GitError):
33 """Exception thrown by L{GitRepository}"""
37 class GitRepository(object):
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}.
42 @ivar _path: The path to the working tree
44 @ivar _bare: Whether this is a bare repository
46 @raises GitRepositoryError: on git errors GitRepositoryError is raised by
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'],
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'
60 def __init__(self, path):
61 self._path = os.path.abspath(path)
64 out, dummy, ret = self._git_inout('rev-parse', ['--show-cdup'],
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
71 raise GitRepositoryError("No Git repository at '%s'" % self.path)
75 def __build_env(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}
134 return self.__git_inout(command, args, input, extra_env, cwd, capture_stderr)
137 def __git_inout(cls, command, args, input, extra_env, cwd, capture_stderr):
139 As _git_inout but can be used without an instance
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
147 popen = subprocess.Popen(cmd,
149 stdout=subprocess.PIPE,
154 (stdout, stderr) = popen.communicate(input)
155 return stdout, stderr, popen.returncode
157 def _git_command(self, command, args=[], extra_env=None):
159 Execute git command with arguments args and environment env
162 @param command: git command
163 @type command: C{str}
164 @param args: command line arguments
166 @param extra_env: extra environment variables to set when running command
167 @type extra_env: C{dict}
170 stdout, stderr, ret = self._git_inout(command=command,
175 except Exception as excobj:
176 raise GitRepositoryError("Error running git %s: %s" % (command, excobj))
178 raise GitRepositoryError("Error running git %s: %s" % (command, stderr))
181 def _cmd_has_feature(self, command, feature):
183 Check if the git command has certain feature enabled.
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
192 args = GitArgs(command, '-m')
193 help, stderr, ret = self._git_inout('help',
195 extra_env={'LC_ALL': 'C'},
198 raise GitRepositoryError("Invalid git command '%s': %s"
199 % (command, stderr[:-1]))
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\-]+)\]-?')
206 for line in help.splitlines():
207 if man_section == "OPTIONS" and line.startswith(' -'):
208 opts = line.split(',')
211 match = optopt_re.match(opt)
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:
220 match = section_re.match(line)
222 man_section = match.group('section')
227 """The absolute path to the repository"""
232 """The absolute path to git's metadata"""
233 return os.path.join(self.path, self._git_dir)
237 """Wheter this is a bare repository"""
242 """List of all tags in the repository"""
243 return self.get_tags()
247 """The currently checked out branch"""
249 return self.get_branch()
250 except GitRepositoryError:
255 """return the SHA1 of the current HEAD"""
256 return self.rev_parse('HEAD')
258 #{ Branches and Merging
259 def rename_branch(self, branch, newbranch):
263 @param branch: name of the branch to be renamed
264 @param newbranch: new name of the branch
266 args = GitArgs("-m", branch, newbranch)
267 self._git_command("branch", args.args)
269 def create_branch(self, branch, rev=None):
273 @param branch: the branch's name
274 @param rev: where to start the branch from
276 If rev is None the branch starts form the current HEAD.
278 args = GitArgs(branch)
279 args.add_true(rev, rev)
280 self._git_command("branch", args.args)
282 def delete_branch(self, branch, remote=False):
284 Delete branch I{branch}
286 @param branch: name of the branch to delete
288 @param remote: delete a remote branch
289 @param remote: C{bool}
292 args.add_true(remote, '-r')
295 if self.branch != branch:
296 self._git_command("branch", args.args)
298 raise GitRepositoryError("Can't delete the branch you're on")
300 def get_branch(self):
302 On what branch is the current working copy
304 @return: current branch or C{None} in an empty repo
306 @raises GitRepositoryError: if HEAD is not a symbolic ref
307 (e.g. when in detached HEAD state)
309 out, dummy, ret = self._git_inout('symbolic-ref', [ 'HEAD' ],
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]
317 # Check if ref really exists
319 self._git_command('show-ref', [ ref ])
320 branch = ref[11:] # strip /refs/heads
321 except GitRepositoryError:
322 branch = None # empty repo
326 def has_branch(self, branch, remote=False):
328 Check if the repository has branch named I{branch}.
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
337 ref = 'refs/remotes/%s' % branch
339 ref = 'refs/heads/%s' % branch
341 self._git_command('show-ref', [ ref ])
342 except GitRepositoryError:
346 def set_branch(self, branch):
348 Switch to branch I{branch}
350 @param branch: name of the branch to switch to
353 if self.branch == branch:
357 self._git_command("symbolic-ref",
358 [ 'HEAD', 'refs/heads/%s' % branch ])
360 self._git_command("checkout", [ branch ])
362 def get_merge_branch(self, branch):
364 Get the branch we'd merge from
366 @return: repo and branch we would merge from
370 remote = self.get_config("branch.%s.remote" % branch)
371 merge = self.get_config("branch.%s.merge" % branch)
374 remote += merge.replace("refs/heads","", 1)
377 def get_merge_base(self, commit1, commit2):
379 Get the common ancestor between two commits
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
391 sha1, stderr, ret = self._git_inout('merge-base', args.args, capture_stderr=True)
393 return self.strip_sha1(sha1)
395 raise GitRepositoryError("Failed to get common ancestor: %s" % stderr.strip())
397 def merge(self, commit, verbose=False, edit=False):
399 Merge changes from the named commit into the current branch
401 @param commit: the commit to merge from (usually a branch name or tag)
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
409 args.add_cond(verbose, '--summary', '--no-summary')
410 if (self._cmd_has_feature('merge', 'edit')):
411 args.add_cond(edit, '--edit', '--no-edit')
413 log.debug("Your git suite doesn't support --edit/--no-edit "
414 "option for git-merge ")
416 self._git_command("merge", args.args)
418 def is_fast_forward(self, from_branch, to_branch):
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.
423 @return: can_fast_forward, up_to_date
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),
432 if not out: # both branches have the same commits
436 if line.startswith("<"):
438 elif line.startswith(">"):
441 if has_local and has_remote:
448 def _get_branches(self, remote=False):
450 Get a list of branches
452 @param remote: whether to list local or remote branches
453 @type remote: C{bool}
454 @return: local or remote branches
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 ]
462 def get_local_branches(self):
464 Get a list of local branches
466 @return: local branches
469 return self._get_branches(remote=False)
472 def get_remote_branches(self):
474 Get a list of remote branches
476 @return: remote branches
479 return self._get_branches(remote=True)
481 def update_ref(self, ref, new, old=None, msg=None):
483 Update ref I{ref} to commit I{new} if I{ref} currently points to
486 @param ref: the ref to update
488 @param new: the new value for ref
490 @param old: the old value of ref
492 @param msg: the reason for the update
499 args = [ '-m', msg ] + args
500 self._git_command("update-ref", args)
502 def branch_contains(self, branch, commit, remote=False):
504 Check if branch I{branch} contains commit I{commit}
506 @param branch: the branch the commit should be on
508 @param commit: the C{str} commit to check
510 @param remote: whether to check remote instead of local branches
511 @type remote: C{bool}
514 args.add_true(remote, '-r')
515 args.add('--contains')
518 out, ret = self._git_getoutput('branch', args.args)
520 # remove prefix '*' for current branch before comparing
521 line = line.replace('*', '')
522 if line.strip() == branch:
526 def set_upstream_branch(self, local_branch, upstream):
528 Set upstream branches for local branch
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}
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)
541 if self._cmd_has_feature('branch', 'set-upstream-to'):
542 args = ['--set-upstream-to=%s' % upstream, local_branch]
544 args = ["--set-upstream", local_branch, upstream]
546 dummy, err, ret = self._git_inout('branch',
550 raise GitRepositoryError(
551 "Failed to set upstream branch '%s' for '%s': %s" %
552 (upstream, local_branch, err.strip()))
554 def get_upstream_branch(self, local_branch):
556 Get upstream branch for the local branch
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
564 args = GitArgs('--format=%(upstream:short)')
565 if self.has_branch(local_branch, remote=False):
566 args.add('refs/heads/%s' % local_branch)
568 raise GitRepositoryError("Branch %s doesn't exist!" % local_branch)
570 out = self._git_getoutput('for-each-ref', args.args)[0]
572 return out[0].strip()
576 def create_tag(self, name, msg=None, commit=None, sign=False, keyid=None):
580 @param name: the tag's name
582 @param msg: The tag message.
584 @param commit: the commit or object to create the tag at, default
587 @param sign: Whether to sing the tag
589 @param keyid: the GPG keyid used to sign the tag
593 args += [ '-m', msg ] if msg else []
596 args += [ '-u', keyid ] if keyid else []
598 args += [ commit ] if commit else []
599 self._git_command("tag", args)
601 def delete_tag(self, tag):
603 Delete a tag named I{tag}
605 @param tag: the tag to delete
608 if self.has_tag(tag):
609 self._git_command("tag", [ "-d", tag ])
611 def move_tag(self, old, new):
612 self._git_command("tag", [ new, old ])
615 def has_tag(self, tag):
617 Check if the repository has a tag named I{tag}.
619 @param tag: tag to look for
621 @return: C{True} if the repository has that tag, C{False} otherwise
624 out, ret = self._git_getoutput('tag', [ '-l', tag ])
625 return [ False, True ][len(out)]
627 def describe(self, commitish, pattern=None, longfmt=False, always=False,
628 abbrev=None, tags=False, exact_match=False):
630 Describe commit, relative to the latest tag reachable from it.
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
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
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')
664 tag, err, ret = self._git_inout('describe', args.args,
667 raise GitRepositoryError("Can't describe %s. Git error: %s" % \
668 (commitish, err.strip()))
671 def find_tag(self, commit, pattern=None):
673 Find the closest tag to a given commit
675 @param commit: the commit to describe
677 @param pattern: only look for tags matching I{pattern}
678 @type pattern: C{str}
679 @return: the found tag
682 return self.describe(commit, pattern, abbrev=0)
684 def get_tags(self, pattern=None):
688 @param pattern: only list tags matching I{pattern}
689 @type pattern: C{str}
691 @rtype: C{list} of C{str}
693 args = [ '-l', pattern ] if pattern else []
694 return [ line.strip() for line in self._git_getoutput('tag', args)[0] ]
696 def verify_tag(self, tag):
700 @param tag: the tag's name
702 @return: Whether the signature on the tag could be verified
705 args = GitArgs('-v', tag)
708 self._git_command('tag', args.args)
709 except GitRepositoryError:
714 def force_head(self, commit, hard=False):
716 Force HEAD to a specific commit
718 @param commit: commit to move HEAD to
719 @param hard: also update the working copy
722 if not GitCommit.is_sha1(commit):
723 commit = self.rev_parse(commit)
726 ref = "refs/heads/%s" % self.get_branch()
727 self._git_command("update-ref", [ ref, commit ])
732 args += [ commit, '--' ]
733 self._git_command("reset", args)
735 def is_clean(self, ignore_untracked=False):
737 Does the repository contain any uncommitted modifications?
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
749 clean_msg = 'nothing to commit'
752 args.add_true(ignore_untracked, '-uno')
754 out, ret = self._git_getoutput('status',
756 extra_env={'LC_ALL': 'C'})
758 raise GbpError("Can't get repository status")
761 if line.startswith('#'):
763 if line.startswith(clean_msg):
766 return (ret, "".join(out))
770 Is the repository empty?
772 @return: True if the repositorydoesn't have any commits,
776 # an empty repo has no branches:
777 return False if self.branch else True
779 def rev_parse(self, name, short=0):
781 Find the SHA1 of a given name
783 @param name: the name to look for
785 @param short: try to abbreviate SHA1 to given length
787 @return: the name's sha1
790 args = GitArgs("--quiet", "--verify")
791 args.add_cond(short, '--short=%d' % short)
793 sha, ret = self._git_getoutput('rev-parse', args.args)
795 raise GitRepositoryError("revision '%s' not found" % name)
796 return self.strip_sha1(sha[0], short)
799 def strip_sha1(sha1, length=0):
801 Strip a given sha1 and check if the resulting
802 hash has the expected length.
804 >>> GitRepository.strip_sha1(' 58ef37dbeb12c44b206b92f746385a6f61253c0a\\n')
805 '58ef37dbeb12c44b206b92f746385a6f61253c0a'
806 >>> GitRepository.strip_sha1('58ef37d', 10)
807 Traceback (most recent call last):
809 GitRepositoryError: '58ef37d' is not a valid sha1 of length 10
810 >>> GitRepository.strip_sha1('58ef37d', 7)
812 >>> GitRepository.strip_sha1('123456789', 7)
814 >>> GitRepository.strip_sha1('foobar')
815 Traceback (most recent call last):
817 GitRepositoryError: 'foobar' is not a valid sha1
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 ""))
830 def checkout(self, treeish):
834 @param treeish: the treeish to check out
835 @type treeish: C{str}
837 self._git_command("checkout", ["--quiet", treeish])
839 def has_treeish(self, treeish):
841 Check if the repository has the treeish object I{treeish}.
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
848 _out, _err, ret = self._git_inout('ls-tree', [treeish],
850 return [ True, False ][ret != 0]
852 def write_tree(self, index_file=None):
854 Create a tree object from the current index
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
862 extra_env = {'GIT_INDEX_FILE': index_file }
866 tree, stderr, ret = self._git_inout('write-tree', [],
870 raise GitRepositoryError("Can't write out current index: %s" % stderr[:-1])
873 def make_tree(self, contents):
875 Create a tree based on contents. I{contents} has the same format than
876 the I{GitRepository.list_tree} output.
882 mode, type, sha1, name = obj
883 out += '%s %s %s\t%s\0' % (mode, type, sha1, name)
885 sha1, err, ret = self._git_inout('mktree',
890 raise GitRepositoryError("Failed to mktree: '%s'" % err)
891 return self.strip_sha1(sha1)
893 def get_obj_type(self, obj):
895 Get type of a git repository object
897 @param obj: repository object
899 @return: type of the repository object
902 out, ret = self._git_getoutput('cat-file', args=['-t', obj])
904 raise GitRepositoryError("Not a Git repository object: '%s'" % obj)
905 return out[0].strip()
907 def list_tree(self, treeish, recurse=False):
909 Get a trees content. It returns a list of objects that match the
910 'ls-tree' output: [ mode, type, sha1, path ].
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}
917 @rtype: C{list} of objects. See above.
920 args.add_true(recurse, '-r')
923 out, err, ret = self._git_inout('ls-tree', args.args, capture_stderr=True)
925 raise GitRepositoryError("Failed to ls-tree '%s': '%s'" % (treeish, err))
928 for line in out.split('\0'):
930 tree.append(line.split(None, 3))
935 def get_config(self, name):
937 Gets the config value associated with I{name}
939 @param name: config value to get
940 @return: fetched config value
943 value, ret = self._git_getoutput('config', [ name ])
944 if ret: raise KeyError
945 return value[0][:-1] # first line with \n ending removed
947 def get_author_info(self):
949 Determine a sane values for author name and author email from git's
950 config and environment variables.
952 @return: name and email
953 @rtype: L{GitModifier}
956 name = self.get_config("user.name")
958 name = os.getenv("USER")
960 email = self.get_config("user.email")
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)
967 #{ Remote Repositories
969 def get_remote_repos(self):
971 Get all remote repositories
973 @return: remote repositories
974 @rtype: C{list} of C{str}
976 out = self._git_getoutput('remote')[0]
977 return [ remote.strip() for remote in out ]
979 def has_remote_repo(self, name):
981 Do we know about a remote named I{name}?
983 @param name: name of the remote repository
985 @return: C{True} if the remote repositore is known, C{False} otherwise
988 if name in self.get_remote_repos():
993 def add_remote_repo(self, name, url, tags=True, fetch=False):
995 Add a tracked remote repository
997 @param name: the name to use for the remote
999 @param url: the url to add
1001 @param tags: whether to fetch tags
1003 @param fetch: whether to fetch immediately from the remote side
1004 @type fetch: C{bool}
1006 args = GitArgs('add')
1007 args.add_false(tags, '--no-tags')
1008 args.add_true(fetch, '--fetch')
1010 self._git_command("remote", args.args)
1012 def remove_remote_repo(self, name):
1013 args = GitArgs('rm', name)
1014 self._git_command("remote", args.args)
1016 def fetch(self, repo=None, tags=False, depth=0, refspec=None,
1019 Download objects and refs from another repository.
1021 @param repo: repository to fetch from
1023 @param tags: whether to fetch all tag objects
1025 @param depth: deepen the history of (shallow) repository to depth I{depth}
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}
1032 args = GitArgs('--quiet')
1033 args.add_true(tags, '--tags')
1034 args.add_cond(depth, '--depth=%s' % depth)
1036 args.add_true(all_remotes, '--all')
1038 args.add_cond(repo, repo)
1039 args.add_cond(refspec, refspec)
1041 self._git_command("fetch", args.args)
1043 def pull(self, repo=None, ff_only=False):
1045 Fetch and merge from another repository
1047 @param repo: repository to fetch from
1049 @param ff_only: only merge if this results in a fast forward merge
1050 @type ff_only: C{bool}
1053 args += [ '--ff-only' ] if ff_only else []
1054 args += [ repo ] if repo else []
1055 self._git_command("pull", args)
1057 def push(self, repo=None, src=None, dst=None, ff_only=True):
1059 Push changes to the remote repo
1061 @param repo: repository to push to
1063 @param src: the source ref to push
1065 @param dst: the name of the destination ref to push to
1067 @param ff_only: only push if it's a fast forward update
1068 @type ff_only: C{bool}
1071 args.add_cond(repo, repo)
1073 # Allow for src == '' to delete dst on the remote
1077 refspec += ':%s' % dst
1079 refspec = '+%s' % refspec
1081 self._git_command("push", args.args)
1083 def push_tag(self, repo, tag):
1085 Push a tag to the remote repo
1087 @param repo: repository to push to
1089 @param tag: the name of the tag
1092 args = GitArgs(repo, 'tag', tag)
1093 self._git_command("push", args.args)
1097 def add_files(self, paths, force=False, index_file=None, work_tree=None):
1099 Add files to a the repository
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
1110 if isinstance(paths, basestring):
1113 args = [ '-f' ] if force else []
1116 extra_env['GIT_INDEX_FILE'] = index_file
1119 extra_env['GIT_WORK_TREE'] = work_tree
1121 self._git_command("add", args + paths, extra_env)
1123 def remove_files(self, paths, verbose=False):
1125 Remove files from the repository
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}
1132 if isinstance(paths, basestring):
1135 args = [] if verbose else ['--quiet']
1136 self._git_command("rm", args + paths)
1138 def list_files(self, types=['cached']):
1140 List files in index and working tree
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}
1147 all_types = [ 'cached', 'deleted', 'others', 'ignored', 'stage'
1148 'unmerged', 'killed', 'modified' ]
1153 args += [ '--%s' % t ]
1155 raise GitRepositoryError("Unknown type '%s'" % t)
1156 out, ret = self._git_getoutput('ls-files', args)
1158 raise GitRepositoryError("Error listing files: '%d'" % ret)
1160 return [ file for file in out[0].split('\0') if file ]
1165 def write_file(self, filename, filters=True):
1167 Hash a single file and write it into the object database
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
1176 args = GitArgs('-w', '-t', 'blob')
1177 args.add_false(filters, '--no-filters')
1180 sha1, stderr, ret = self._git_inout('hash-object',
1182 capture_stderr=True)
1184 return self.strip_sha1(sha1)
1186 raise GbpError("Failed to hash %s: %s" % (filename, stderr))
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)
1195 def commit_staged(self, msg, author_info=None, edit=False):
1197 Commit currently staged files to the repository
1199 @param msg: commit message
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
1207 args.add_true(edit, '--edit')
1208 self._commit(msg=msg, args=args.args, author_info=author_info)
1210 def commit_all(self, msg, author_info=None, edit=False):
1212 Commit all changes to the repository
1213 @param msg: commit message
1215 @param author_info: authorship information
1216 @type author_info: L{GitModifier}
1218 args = GitArgs('-a')
1219 args.add_true(edit, '--edit')
1220 self._commit(msg=msg, args=args.args, author_info=author_info)
1222 def commit_files(self, files, msg, author_info=None):
1224 Commit the given files to the repository
1226 @param files: file or files to commit
1227 @type files: C{str} or C{list}
1228 @param msg: commit message
1230 @param author_info: authorship information
1231 @type author_info: L{GitModifier}
1233 if isinstance(files, basestring):
1235 self._commit(msg=msg, args=files, author_info=author_info)
1237 def commit_dir(self, unpack_dir, msg, branch, other_parents=None,
1238 author={}, committer={}, create_missing_branch=False):
1240 Replace the current tip of branch I{branch} with the contents from I{unpack_dir}
1242 @param unpack_dir: content to add
1243 @type unpack_dir: C{str}
1244 @param msg: commit message to use
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}
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}
1260 git_index_file = os.path.join(self.path, self._git_dir, 'gbp_index')
1262 os.unlink(git_index_file)
1265 self.add_files('.', force=True, index_file=git_index_file,
1266 work_tree=unpack_dir)
1267 tree = self.write_tree(git_index_file)
1271 cur = self.rev_parse(branch)
1272 except GitRepositoryError:
1273 if create_missing_branch:
1274 log.debug("Will create missing branch '%s'..." % branch)
1282 # Build list of parents:
1287 for parent in other_parents:
1288 sha = self.rev_parse(parent)
1289 if sha not in parents:
1292 commit = self.commit_tree(tree=tree, msg=msg, parents=parents,
1293 author=author, committer=committer)
1295 raise GitRepositoryError("Failed to commit tree")
1296 self.update_ref("refs/heads/%s" % branch, commit, cur)
1299 def commit_tree(self, tree, msg, parents, author={}, committer={}):
1301 Commit a tree with commit msg I{msg} and parents I{parents}
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'
1312 for key, val in author.items():
1314 extra_env['GIT_AUTHOR_%s' % key.upper()] = val
1315 for key, val in committer.items():
1317 extra_env['GIT_COMMITTER_%s' % key.upper()] = val
1320 for parent in parents:
1321 args += [ '-p' , parent ]
1322 sha1, stderr, ret = self._git_inout('commit-tree',
1326 capture_stderr=True)
1328 return self.strip_sha1(sha1)
1330 raise GbpError("Failed to commit tree: %s" % stderr)
1332 #{ Commit Information
1334 def get_commits(self, since=None, until=None, paths=None, num=0,
1335 first_parent=False, options=None):
1337 Get commits from since to until touching paths
1339 @param since: commit to start from
1341 @param until: last commit to get
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
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
1351 @type first_parent: C{bool}
1353 args = GitArgs('--pretty=format:%H')
1354 args.add_true(num, '-%d' % num)
1355 args.add_true(first_parent, '--first-parent')
1357 args.add("%s..%s" % (since, until or 'HEAD'))
1360 args.add_cond(options, options)
1362 if isinstance(paths, basestring):
1364 args.add_cond(paths, paths)
1366 commits, ret = self._git_getoutput('log', args.args)
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 ]
1375 obj, stderr, ret = self._git_inout('show', ["--pretty=medium", id],
1376 capture_stderr=True)
1378 raise GitRepositoryError("can't get %s: %s" % (id, stderr.rstrip()))
1381 def grep_log(self, regex, since=None):
1383 Get commmits matching I{regex}
1385 @param regex: regular expression
1387 @param since: where to start grepping (e.g. a branch)
1390 args = ['--pretty=format:%H']
1391 args.append("--grep=%s" % regex)
1396 stdout, stderr, ret = self._git_inout('log', args,
1397 capture_stderr=True)
1399 raise GitRepositoryError("Error grepping log for %s: %s" %
1400 (regex, stderr[:-1]))
1402 return [ commit.strip() for commit in stdout.split('\n')[::-1] ]
1406 def get_subject(self, commit):
1408 Gets the subject of a commit.
1410 @deprecated: Use get_commit_info directly
1412 @param commit: the commit to get the subject from
1413 @return: the commit's subject
1416 return self.get_commit_info(commit)['subject']
1418 def get_commit_info(self, commitish):
1420 Look up data of a specific commit-ish. Dereferences given commit-ish
1421 to the commit it points to.
1423 @param commitish: the commit-ish to inspect
1424 @return: the commit's including id, author, email, subject and body
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)
1432 raise GitRepositoryError("Unable to retrieve commit info for %s"
1435 fields = out.split('\x00')
1437 author = GitModifier(fields[0].strip(),
1440 committer = GitModifier(fields[3].strip(),
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)
1453 return {'id' : commitish,
1455 'committer' : committer,
1456 'subject' : fields[6],
1457 'patchname' : fields[7],
1462 def format_patches(self, start, end, output_dir,
1467 Output the commits between start and end as patches in output_dir.
1469 This outputs the revisions I{start...end} by default. When using
1470 I{symmetric} to C{false} it uses I{start..end} instead.
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)
1479 options = GitArgs('-N', '-k',
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')
1485 output, ret = self._git_getoutput('format-patch', options.args)
1486 return [ line.strip() for line in output ]
1488 def apply_patch(self, patch, index=True, context=None, strip=None):
1489 """Apply a patch using git apply"""
1492 args += [ '-C', context ]
1494 args.append("--index")
1496 args += [ '-p', str(strip) ]
1498 self._git_command("apply", args)
1500 def diff(self, obj1, obj2=None, paths=None, stat=False, summary=False):
1502 Diff two git repository objects
1504 @param obj1: first object
1506 @param obj2: second object
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}
1517 options = GitArgs('-p')
1519 options.add('--stat')
1521 options.add('--stat=%s' % stat)
1522 options.add_true(summary, '--summary')
1524 options.add_true(obj2, obj2)
1526 options.add('--', paths)
1527 output, stderr, ret = self._git_inout('diff', options.args)
1529 raise GitRepositoryError("Git diff failed")
1533 def archive(self, format, prefix, output, treeish, **kwargs):
1535 Create an archive from a treeish
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
1547 args = [ '--format=%s' % format, '--prefix=%s' % prefix,
1548 '--output=%s' % output, treeish ]
1549 out, ret = self._git_getoutput('archive', args, **kwargs)
1551 raise GitRepositoryError("Unable to archive %s" % treeish)
1553 def collect_garbage(self, auto=False):
1555 Cleanup unnecessary files and optimize the local repository
1557 param auto: only cleanup if required
1560 args = [ '--auto' ] if auto else []
1561 self._git_command("gc", args)
1565 def has_submodules(self):
1567 Does the repo have any submodules?
1569 @return: C{True} if the repository has any submodules, C{False}
1573 if os.path.exists(os.path.join(self.path, '.gitmodules')):
1579 def add_submodule(self, repo_path):
1583 @param repo_path: path to submodule
1584 @type repo_path: C{str}
1586 self._git_command("submodule", [ "add", repo_path ])
1589 def update_submodules(self, init=True, recursive=True, fetch=False):
1591 Update all submodules
1593 @param init: whether to initialize the submodule if necessary
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}
1601 if not self.has_submodules():
1605 args.append("--recursive")
1607 args.append("--init")
1609 args.append("--no-fetch")
1611 self._git_command("submodule", args)
1614 def get_submodules(self, treeish, path=None, recursive=True):
1616 List the submodules of treeish
1618 @return: a list of submodule/commit-id tuples
1619 @rtype: list of tuples
1621 # Note that we is lstree instead of submodule commands because
1622 # there's no way to list the submodules of another branch with
1632 out, ret = self._git_getoutput('ls-tree', args, cwd=path)
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('/'),
1641 submodules += self.get_submodules(commit, path=nextpath,
1642 recursive=recursive)
1645 #{ Repository Creation
1648 def create(klass, path, description=None, bare=False):
1650 Create a repository at path
1652 @param path: where to create the repository
1654 @param bare: whether to create a bare repository
1656 @return: git repository object
1657 @rtype: L{GitRepository}
1660 abspath = os.path.abspath(path)
1662 args.add_true(bare, '--bare')
1663 git_dir = '' if bare else '.git'
1666 if not os.path.exists(abspath):
1667 os.makedirs(abspath)
1669 stdout, stderr, ret = klass.__git_inout(command='init',
1674 capture_stderr=True)
1675 except Exception as excobj:
1676 raise GitRepositoryError("Error running git init: %s" % excobj)
1678 raise GitRepositoryError("Error running git init: %s" % stderr)
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]))
1691 def clone(klass, path, remote, depth=0, recursive=False, mirror=False,
1692 bare=False, auto_name=True):
1694 Clone a git repository at I{remote} to I{path}.
1696 @param path: where to clone the repository to
1698 @param remote: URL to clone
1699 @type remote: C{str}
1700 @param depth: create a shallow clone of depth I{depth}
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
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}
1714 abspath = os.path.abspath(path)
1718 abspath, name = abspath.rsplit('/', 1)
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')
1726 args.add_true(name, name)
1728 if not os.path.exists(abspath):
1729 os.makedirs(abspath)
1732 stdout, stderr, ret = klass.__git_inout(command='clone',
1737 capture_stderr=True)
1738 except Exception as excobj:
1739 raise GitRepositoryError("Error running git clone: %s" % excobj)
1741 raise GitRepositoryError("Error running git clone: %s" % stderr)
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'):
1750 return klass(os.path.join(abspath, name))
1751 except OSError as err:
1752 raise GitRepositoryError("Cannot clone Git repository "
1754 % (remote, abspath, err[1]))