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))
768 def clean(self, directories=False, force=False, dry_run=False):
770 Remove untracked files from the working tree.
772 @param directories: remove untracked directories, too
773 @type directories: C{bool}
774 @param force: satisfy git configuration variable clean.requireForce
776 @param dry_run: don’t actually remove anything
777 @type dry_run: C{bool}
780 options.add_true(directories, '-d')
781 options.add_true(force, '-f')
782 options.add_true(dry_run, '-n')
784 _out, err, ret = self._git_inout('clean', options.args,
785 extra_env={'LC_ALL': 'C'})
787 raise GitRepositoryError("Can't execute repository clean: %s" % err)
791 Is the repository empty?
793 @return: True if the repositorydoesn't have any commits,
797 # an empty repo has no branches:
798 return False if self.branch else True
800 def rev_parse(self, name, short=0):
802 Find the SHA1 of a given name
804 @param name: the name to look for
806 @param short: try to abbreviate SHA1 to given length
808 @return: the name's sha1
811 args = GitArgs("--quiet", "--verify")
812 args.add_cond(short, '--short=%d' % short)
814 sha, ret = self._git_getoutput('rev-parse', args.args)
816 raise GitRepositoryError("revision '%s' not found" % name)
817 return self.strip_sha1(sha[0], short)
820 def strip_sha1(sha1, length=0):
822 Strip a given sha1 and check if the resulting
823 hash has the expected length.
825 >>> GitRepository.strip_sha1(' 58ef37dbeb12c44b206b92f746385a6f61253c0a\\n')
826 '58ef37dbeb12c44b206b92f746385a6f61253c0a'
827 >>> GitRepository.strip_sha1('58ef37d', 10)
828 Traceback (most recent call last):
830 GitRepositoryError: '58ef37d' is not a valid sha1 of length 10
831 >>> GitRepository.strip_sha1('58ef37d', 7)
833 >>> GitRepository.strip_sha1('123456789', 7)
835 >>> GitRepository.strip_sha1('foobar')
836 Traceback (most recent call last):
838 GitRepositoryError: 'foobar' is not a valid sha1
845 if len(s) < l or len(s) > maxlen:
846 raise GitRepositoryError("'%s' is not a valid sha1%s" %
847 (s, " of length %d" % l if length else ""))
851 def checkout(self, treeish):
855 @param treeish: the treeish to check out
856 @type treeish: C{str}
858 self._git_command("checkout", ["--quiet", treeish])
860 def has_treeish(self, treeish):
862 Check if the repository has the treeish object I{treeish}.
864 @param treeish: treeish object to look for
865 @type treeish: C{str}
866 @return: C{True} if the repository has that tree, C{False} otherwise
869 _out, _err, ret = self._git_inout('ls-tree', [treeish],
871 return [ True, False ][ret != 0]
873 def write_tree(self, index_file=None):
875 Create a tree object from the current index
877 @param index_file: alternate index file to read changes from
878 @type index_file: C{str}
879 @return: the new tree object's sha1
883 extra_env = {'GIT_INDEX_FILE': index_file }
887 tree, stderr, ret = self._git_inout('write-tree', [],
891 raise GitRepositoryError("Can't write out current index: %s" % stderr[:-1])
894 def make_tree(self, contents):
896 Create a tree based on contents. I{contents} has the same format than
897 the I{GitRepository.list_tree} output.
903 mode, type, sha1, name = obj
904 out += '%s %s %s\t%s\0' % (mode, type, sha1, name)
906 sha1, err, ret = self._git_inout('mktree',
911 raise GitRepositoryError("Failed to mktree: '%s'" % err)
912 return self.strip_sha1(sha1)
914 def get_obj_type(self, obj):
916 Get type of a git repository object
918 @param obj: repository object
920 @return: type of the repository object
923 out, ret = self._git_getoutput('cat-file', args=['-t', obj])
925 raise GitRepositoryError("Not a Git repository object: '%s'" % obj)
926 return out[0].strip()
928 def list_tree(self, treeish, recurse=False):
930 Get a trees content. It returns a list of objects that match the
931 'ls-tree' output: [ mode, type, sha1, path ].
933 @param treeish: the treeish object to list
934 @type treeish: C{str}
935 @param recurse: whether to list the tree recursively
936 @type recurse: C{bool}
938 @rtype: C{list} of objects. See above.
941 args.add_true(recurse, '-r')
944 out, err, ret = self._git_inout('ls-tree', args.args, capture_stderr=True)
946 raise GitRepositoryError("Failed to ls-tree '%s': '%s'" % (treeish, err))
949 for line in out.split('\0'):
951 tree.append(line.split(None, 3))
956 def get_config(self, name):
958 Gets the config value associated with I{name}
960 @param name: config value to get
961 @return: fetched config value
964 value, ret = self._git_getoutput('config', [ name ])
965 if ret: raise KeyError
966 return value[0][:-1] # first line with \n ending removed
968 def get_author_info(self):
970 Determine a sane values for author name and author email from git's
971 config and environment variables.
973 @return: name and email
974 @rtype: L{GitModifier}
977 name = self.get_config("user.name")
979 name = os.getenv("USER")
981 email = self.get_config("user.email")
983 email = os.getenv("EMAIL")
984 email = os.getenv("GIT_AUTHOR_EMAIL", email)
985 name = os.getenv("GIT_AUTHOR_NAME", name)
986 return GitModifier(name, email)
988 #{ Remote Repositories
990 def get_remote_repos(self):
992 Get all remote repositories
994 @return: remote repositories
995 @rtype: C{list} of C{str}
997 out = self._git_getoutput('remote')[0]
998 return [ remote.strip() for remote in out ]
1000 def has_remote_repo(self, name):
1002 Do we know about a remote named I{name}?
1004 @param name: name of the remote repository
1006 @return: C{True} if the remote repositore is known, C{False} otherwise
1009 if name in self.get_remote_repos():
1014 def add_remote_repo(self, name, url, tags=True, fetch=False):
1016 Add a tracked remote repository
1018 @param name: the name to use for the remote
1020 @param url: the url to add
1022 @param tags: whether to fetch tags
1024 @param fetch: whether to fetch immediately from the remote side
1025 @type fetch: C{bool}
1027 args = GitArgs('add')
1028 args.add_false(tags, '--no-tags')
1029 args.add_true(fetch, '--fetch')
1031 self._git_command("remote", args.args)
1033 def remove_remote_repo(self, name):
1034 args = GitArgs('rm', name)
1035 self._git_command("remote", args.args)
1037 def fetch(self, repo=None, tags=False, depth=0, refspec=None,
1040 Download objects and refs from another repository.
1042 @param repo: repository to fetch from
1044 @param tags: whether to fetch all tag objects
1046 @param depth: deepen the history of (shallow) repository to depth I{depth}
1048 @param refspec: refspec to use instead of the default from git config
1049 @type refspec: C{str}
1050 @param all_remotes: fetch all remotes
1051 @type all_remotes: C{bool}
1053 args = GitArgs('--quiet')
1054 args.add_true(tags, '--tags')
1055 args.add_cond(depth, '--depth=%s' % depth)
1057 args.add_true(all_remotes, '--all')
1059 args.add_cond(repo, repo)
1060 args.add_cond(refspec, refspec)
1062 self._git_command("fetch", args.args)
1064 def pull(self, repo=None, ff_only=False):
1066 Fetch and merge from another repository
1068 @param repo: repository to fetch from
1070 @param ff_only: only merge if this results in a fast forward merge
1071 @type ff_only: C{bool}
1074 args += [ '--ff-only' ] if ff_only else []
1075 args += [ repo ] if repo else []
1076 self._git_command("pull", args)
1078 def push(self, repo=None, src=None, dst=None, ff_only=True):
1080 Push changes to the remote repo
1082 @param repo: repository to push to
1084 @param src: the source ref to push
1086 @param dst: the name of the destination ref to push to
1088 @param ff_only: only push if it's a fast forward update
1089 @type ff_only: C{bool}
1092 args.add_cond(repo, repo)
1094 # Allow for src == '' to delete dst on the remote
1098 refspec += ':%s' % dst
1100 refspec = '+%s' % refspec
1102 self._git_command("push", args.args)
1104 def push_tag(self, repo, tag):
1106 Push a tag to the remote repo
1108 @param repo: repository to push to
1110 @param tag: the name of the tag
1113 args = GitArgs(repo, 'tag', tag)
1114 self._git_command("push", args.args)
1118 def add_files(self, paths, force=False, index_file=None, work_tree=None):
1120 Add files to a the repository
1122 @param paths: list of files to add
1123 @type paths: list or C{str}
1124 @param force: add files even if they would be ignored by .gitignore
1125 @type force: C{bool}
1126 @param index_file: alternative index file to use
1127 @param work_tree: alternative working tree to use
1131 if isinstance(paths, basestring):
1134 args = [ '-f' ] if force else []
1137 extra_env['GIT_INDEX_FILE'] = index_file
1140 extra_env['GIT_WORK_TREE'] = work_tree
1142 self._git_command("add", args + paths, extra_env)
1144 def remove_files(self, paths, verbose=False):
1146 Remove files from the repository
1148 @param paths: list of files to remove
1149 @param paths: C{list} or C{str}
1150 @param verbose: be verbose
1151 @type verbose: C{bool}
1153 if isinstance(paths, basestring):
1156 args = [] if verbose else ['--quiet']
1157 self._git_command("rm", args + paths)
1159 def list_files(self, types=['cached']):
1161 List files in index and working tree
1163 @param types: list of types to show
1164 @type types: C{list}
1165 @return: list of files
1166 @rtype: C{list} of C{str}
1168 all_types = [ 'cached', 'deleted', 'others', 'ignored', 'stage'
1169 'unmerged', 'killed', 'modified' ]
1174 args += [ '--%s' % t ]
1176 raise GitRepositoryError("Unknown type '%s'" % t)
1177 out, ret = self._git_getoutput('ls-files', args)
1179 raise GitRepositoryError("Error listing files: '%d'" % ret)
1181 return [ file for file in out[0].split('\0') if file ]
1186 def write_file(self, filename, filters=True):
1188 Hash a single file and write it into the object database
1190 @param filename: the filename to the content of the file to hash
1191 @type filename: C{str}
1192 @param filters: whether to run filters
1193 @type filters: C{bool}
1194 @return: the hash of the file
1197 args = GitArgs('-w', '-t', 'blob')
1198 args.add_false(filters, '--no-filters')
1201 sha1, stderr, ret = self._git_inout('hash-object',
1203 capture_stderr=True)
1205 return self.strip_sha1(sha1)
1207 raise GbpError("Failed to hash %s: %s" % (filename, stderr))
1212 def _commit(self, msg, args=[], author_info=None):
1213 extra_env = author_info.get_author_env() if author_info else None
1214 self._git_command("commit", ['-q', '-m', msg] + args, extra_env=extra_env)
1216 def commit_staged(self, msg, author_info=None, edit=False):
1218 Commit currently staged files to the repository
1220 @param msg: commit message
1222 @param author_info: authorship information
1223 @type author_info: L{GitModifier}
1224 @param edit: whether to spawn an editor to edit the commit info
1228 args.add_true(edit, '--edit')
1229 self._commit(msg=msg, args=args.args, author_info=author_info)
1231 def commit_all(self, msg, author_info=None, edit=False):
1233 Commit all changes to the repository
1234 @param msg: commit message
1236 @param author_info: authorship information
1237 @type author_info: L{GitModifier}
1239 args = GitArgs('-a')
1240 args.add_true(edit, '--edit')
1241 self._commit(msg=msg, args=args.args, author_info=author_info)
1243 def commit_files(self, files, msg, author_info=None):
1245 Commit the given files to the repository
1247 @param files: file or files to commit
1248 @type files: C{str} or C{list}
1249 @param msg: commit message
1251 @param author_info: authorship information
1252 @type author_info: L{GitModifier}
1254 if isinstance(files, basestring):
1256 self._commit(msg=msg, args=files, author_info=author_info)
1258 def commit_dir(self, unpack_dir, msg, branch, other_parents=None,
1259 author={}, committer={}, create_missing_branch=False):
1261 Replace the current tip of branch I{branch} with the contents from I{unpack_dir}
1263 @param unpack_dir: content to add
1264 @type unpack_dir: C{str}
1265 @param msg: commit message to use
1267 @param branch: branch to add the contents of unpack_dir to
1268 @type branch: C{str}
1269 @param other_parents: additional parents of this commit
1270 @type other_parents: C{list} of C{str}
1271 @param author: author information to use for commit
1272 @type author: C{dict} with keys I{name}, I{email}, I{date}
1273 @param committer: committer information to use for commit
1274 @type committer: C{dict} with keys I{name}, I{email}, I{date}
1276 @param create_missing_branch: create I{branch} as detached branch if it
1277 doesn't already exist.
1278 @type create_missing_branch: C{bool}
1281 git_index_file = os.path.join(self.path, self._git_dir, 'gbp_index')
1283 os.unlink(git_index_file)
1286 self.add_files('.', force=True, index_file=git_index_file,
1287 work_tree=unpack_dir)
1288 tree = self.write_tree(git_index_file)
1292 cur = self.rev_parse(branch)
1293 except GitRepositoryError:
1294 if create_missing_branch:
1295 log.debug("Will create missing branch '%s'..." % branch)
1303 # Build list of parents:
1308 for parent in other_parents:
1309 sha = self.rev_parse(parent)
1310 if sha not in parents:
1313 commit = self.commit_tree(tree=tree, msg=msg, parents=parents,
1314 author=author, committer=committer)
1316 raise GitRepositoryError("Failed to commit tree")
1317 self.update_ref("refs/heads/%s" % branch, commit, cur)
1320 def commit_tree(self, tree, msg, parents, author={}, committer={}):
1322 Commit a tree with commit msg I{msg} and parents I{parents}
1324 @param tree: tree to commit
1325 @param msg: commit message
1326 @param parents: parents of this commit
1327 @param author: authorship information
1328 @type author: C{dict} with keys 'name' and 'email' or L{GitModifier}
1329 @param committer: comitter information
1330 @type committer: C{dict} with keys 'name' and 'email'
1333 for key, val in author.items():
1335 extra_env['GIT_AUTHOR_%s' % key.upper()] = val
1336 for key, val in committer.items():
1338 extra_env['GIT_COMMITTER_%s' % key.upper()] = val
1341 for parent in parents:
1342 args += [ '-p' , parent ]
1343 sha1, stderr, ret = self._git_inout('commit-tree',
1347 capture_stderr=True)
1349 return self.strip_sha1(sha1)
1351 raise GbpError("Failed to commit tree: %s" % stderr)
1353 #{ Commit Information
1355 def get_commits(self, since=None, until=None, paths=None, num=0,
1356 first_parent=False, options=None):
1358 Get commits from since to until touching paths
1360 @param since: commit to start from
1362 @param until: last commit to get
1364 @param paths: only list commits touching paths
1365 @type paths: C{list} of C{str}
1366 @param num: maximum number of commits to fetch
1368 @param options: list of additional options passed to git log
1369 @type options: C{list} of C{str}ings
1370 @param first_parent: only follow first parent when seeing a
1372 @type first_parent: C{bool}
1374 args = GitArgs('--pretty=format:%H')
1375 args.add_true(num, '-%d' % num)
1376 args.add_true(first_parent, '--first-parent')
1378 args.add("%s..%s" % (since, until or 'HEAD'))
1381 args.add_cond(options, options)
1383 if isinstance(paths, basestring):
1385 args.add_cond(paths, paths)
1387 commits, ret = self._git_getoutput('log', args.args)
1389 where = " on %s" % paths if paths else ""
1390 raise GitRepositoryError("Error getting commits %s..%s%s" %
1391 (since, until, where))
1392 return [ commit.strip() for commit in commits ]
1396 obj, stderr, ret = self._git_inout('show', ["--pretty=medium", id],
1397 capture_stderr=True)
1399 raise GitRepositoryError("can't get %s: %s" % (id, stderr.rstrip()))
1402 def grep_log(self, regex, since=None):
1404 Get commmits matching I{regex}
1406 @param regex: regular expression
1408 @param since: where to start grepping (e.g. a branch)
1411 args = ['--pretty=format:%H']
1412 args.append("--grep=%s" % regex)
1417 stdout, stderr, ret = self._git_inout('log', args,
1418 capture_stderr=True)
1420 raise GitRepositoryError("Error grepping log for %s: %s" %
1421 (regex, stderr[:-1]))
1423 return [ commit.strip() for commit in stdout.split('\n')[::-1] ]
1427 def get_subject(self, commit):
1429 Gets the subject of a commit.
1431 @deprecated: Use get_commit_info directly
1433 @param commit: the commit to get the subject from
1434 @return: the commit's subject
1437 return self.get_commit_info(commit)['subject']
1439 def get_commit_info(self, commitish):
1441 Look up data of a specific commit-ish. Dereferences given commit-ish
1442 to the commit it points to.
1444 @param commitish: the commit-ish to inspect
1445 @return: the commit's including id, author, email, subject and body
1448 commit_sha1 = self.rev_parse("%s^0" % commitish)
1449 args = GitArgs('--pretty=format:%an%x00%ae%x00%ad%x00%cn%x00%ce%x00%cd%x00%s%x00%f%x00%b%x00',
1450 '-z', '--date=raw', '--name-status', commit_sha1)
1451 out, err, ret = self._git_inout('show', args.args)
1453 raise GitRepositoryError("Unable to retrieve commit info for %s"
1456 fields = out.split('\x00')
1458 author = GitModifier(fields[0].strip(),
1461 committer = GitModifier(fields[3].strip(),
1465 files = defaultdict(list)
1466 file_fields = fields[9:]
1467 # For some reason git returns one extra empty field for merge commits
1468 if file_fields[0] == '': file_fields.pop(0)
1469 while len(file_fields) and file_fields[0] != '':
1470 status = file_fields.pop(0).strip()
1471 path = file_fields.pop(0)
1472 files[status].append(path)
1474 return {'id' : commitish,
1476 'committer' : committer,
1477 'subject' : fields[6],
1478 'patchname' : fields[7],
1483 def format_patches(self, start, end, output_dir,
1488 Output the commits between start and end as patches in output_dir.
1490 This outputs the revisions I{start...end} by default. When using
1491 I{symmetric} to C{false} it uses I{start..end} instead.
1493 @param start: the commit on the left side of the revision range
1494 @param end: the commit on the right hand side of the revisino range
1495 @param output_dir: directory to write the patches to
1496 @param signature: whether to output a signature
1497 @param thread: whether to include In-Reply-To references
1498 @param symmetric: whether to use the symmetric difference (see above)
1500 options = GitArgs('-N', '-k',
1502 options.add_cond(not signature, '--no-signature')
1503 options.add('%s%s%s' % (start, '...' if symmetric else '..', end))
1504 options.add_cond(thread, '--thread=%s' % thread, '--no-thread')
1506 output, ret = self._git_getoutput('format-patch', options.args)
1507 return [ line.strip() for line in output ]
1509 def apply_patch(self, patch, index=True, context=None, strip=None):
1510 """Apply a patch using git apply"""
1513 args += [ '-C', context ]
1515 args.append("--index")
1517 args += [ '-p', str(strip) ]
1519 self._git_command("apply", args)
1521 def diff(self, obj1, obj2=None, paths=None, stat=False, summary=False):
1523 Diff two git repository objects
1525 @param obj1: first object
1527 @param obj2: second object
1529 @param paths: List of paths to diff
1530 @type paths: C{list}
1531 @param stat: Show diffstat
1532 @type stat: C{bool} or C{int} or C{str}
1533 @param summary: Show diffstat
1534 @type summary: C{bool}
1538 options = GitArgs('-p')
1540 options.add('--stat')
1542 options.add('--stat=%s' % stat)
1543 options.add_true(summary, '--summary')
1545 options.add_true(obj2, obj2)
1547 options.add('--', paths)
1548 output, stderr, ret = self._git_inout('diff', options.args)
1550 raise GitRepositoryError("Git diff failed")
1554 def archive(self, format, prefix, output, treeish, **kwargs):
1556 Create an archive from a treeish
1558 @param format: the type of archive to create, e.g. 'tar.gz'
1559 @type format: C{str}
1560 @param prefix: prefix to prepend to each filename in the archive
1561 @type prefix: C{str}
1562 @param output: the name of the archive to create
1563 @type output: C{str}
1564 @param treeish: the treeish to create the archive from
1565 @type treeish: C{str}
1566 @param kwargs: additional commandline options passed to git-archive
1568 args = [ '--format=%s' % format, '--prefix=%s' % prefix,
1569 '--output=%s' % output, treeish ]
1570 out, ret = self._git_getoutput('archive', args, **kwargs)
1572 raise GitRepositoryError("Unable to archive %s" % treeish)
1574 def collect_garbage(self, auto=False):
1576 Cleanup unnecessary files and optimize the local repository
1578 param auto: only cleanup if required
1581 args = [ '--auto' ] if auto else []
1582 self._git_command("gc", args)
1586 def has_submodules(self):
1588 Does the repo have any submodules?
1590 @return: C{True} if the repository has any submodules, C{False}
1594 if os.path.exists(os.path.join(self.path, '.gitmodules')):
1600 def add_submodule(self, repo_path):
1604 @param repo_path: path to submodule
1605 @type repo_path: C{str}
1607 self._git_command("submodule", [ "add", repo_path ])
1610 def update_submodules(self, init=True, recursive=True, fetch=False):
1612 Update all submodules
1614 @param init: whether to initialize the submodule if necessary
1616 @param recursive: whether to update submodules recursively
1617 @type recursive: C{bool}
1618 @param fetch: whether to fetch new objects
1619 @type fetch: C{bool}
1622 if not self.has_submodules():
1626 args.append("--recursive")
1628 args.append("--init")
1630 args.append("--no-fetch")
1632 self._git_command("submodule", args)
1635 def get_submodules(self, treeish, path=None, recursive=True):
1637 List the submodules of treeish
1639 @return: a list of submodule/commit-id tuples
1640 @rtype: list of tuples
1642 # Note that we is lstree instead of submodule commands because
1643 # there's no way to list the submodules of another branch with
1653 out, ret = self._git_getoutput('ls-tree', args, cwd=path)
1655 mode, objtype, commit, name = line[:-1].split(None, 3)
1656 # A submodules is shown as "commit" object in ls-tree:
1657 if objtype == "commit":
1658 nextpath = os.path.join(path, name)
1659 submodules.append( (nextpath.replace(self.path,'').lstrip('/'),
1662 submodules += self.get_submodules(commit, path=nextpath,
1663 recursive=recursive)
1666 #{ Repository Creation
1669 def create(klass, path, description=None, bare=False):
1671 Create a repository at path
1673 @param path: where to create the repository
1675 @param bare: whether to create a bare repository
1677 @return: git repository object
1678 @rtype: L{GitRepository}
1681 abspath = os.path.abspath(path)
1683 args.add_true(bare, '--bare')
1684 git_dir = '' if bare else '.git'
1687 if not os.path.exists(abspath):
1688 os.makedirs(abspath)
1690 stdout, stderr, ret = klass.__git_inout(command='init',
1695 capture_stderr=True)
1696 except Exception as excobj:
1697 raise GitRepositoryError("Error running git init: %s" % excobj)
1699 raise GitRepositoryError("Error running git init: %s" % stderr)
1702 with open(os.path.join(abspath, git_dir, "description"), 'w') as f:
1703 description += '\n' if description[-1] != '\n' else ''
1704 f.write(description)
1705 return klass(abspath)
1706 except OSError as err:
1707 raise GitRepositoryError("Cannot create Git repository at '%s': %s"
1708 % (abspath, err[1]))
1712 def clone(klass, path, remote, depth=0, recursive=False, mirror=False,
1713 bare=False, auto_name=True):
1715 Clone a git repository at I{remote} to I{path}.
1717 @param path: where to clone the repository to
1719 @param remote: URL to clone
1720 @type remote: C{str}
1721 @param depth: create a shallow clone of depth I{depth}
1723 @param recursive: whether to clone submodules
1724 @type recursive: C{bool}
1725 @param mirror: whether to pass --mirror to git-clone
1726 @type mirror: C{bool}
1727 @param bare: whether to create a bare repository
1729 @param auto_name: If I{True} create a directory below I{path} based on
1730 the I{remote}s name. Otherwise create the repo directly at I{path}.
1731 @type auto_name: C{bool}
1732 @return: git repository object
1733 @rtype: L{GitRepository}
1735 abspath = os.path.abspath(path)
1739 abspath, name = abspath.rsplit('/', 1)
1741 args = GitArgs('--quiet')
1742 args.add_true(depth, '--depth', depth)
1743 args.add_true(recursive, '--recursive')
1744 args.add_true(mirror, '--mirror')
1745 args.add_true(bare, '--bare')
1747 args.add_true(name, name)
1749 if not os.path.exists(abspath):
1750 os.makedirs(abspath)
1753 stdout, stderr, ret = klass.__git_inout(command='clone',
1758 capture_stderr=True)
1759 except Exception as excobj:
1760 raise GitRepositoryError("Error running git clone: %s" % excobj)
1762 raise GitRepositoryError("Error running git clone: %s" % stderr)
1765 name = remote.rstrip('/').rsplit('/',1)[1]
1766 if (mirror or bare):
1767 if not name.endswith('.git'):
1768 name = "%s.git" % name
1769 elif name.endswith('.git'):
1771 return klass(os.path.join(abspath, name))
1772 except OSError as err:
1773 raise GitRepositoryError("Cannot clone Git repository "
1775 % (remote, abspath, err[1]))