# vim: set fileencoding=utf-8 : # # (C) 2006,2007,2008,2011 Guido Guenther <agx@sigxcpu.org> # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
"""Exception thrown by L{GitRepository}"""
""" Represents a git repository at I{path}. It's currently assumed that the git repository is stored in a directory named I{.git/} below I{path}.
@ivar _path: The path to the working tree @type _path: C{str} @ivar _bare: Whether this is a bare repository @type _bare: C{bool} @raises GitRepositoryError: on git errors GitRepositoryError is raised by all methods. """
"""Check whether this is a bare repository""" capture_stderr=True) raise GitRepositoryError( "Failed to get repository state at '%s'" % self.path)
capture_stderr=True) raise GitRepositoryError("No Git repository at '%s': '%s'" % (self.path, out)) except GitRepositoryError: raise # We already have a useful error message except: raise GitRepositoryError("No Git repository at '%s'" % self.path)
"""Prepare environment for subprocess calls""" env = os.environ.copy() env.update(extra_env)
""" Run a git command and return the output
@param command: git command to run @type command: C{str} @param args: list of arguments @type args: C{list} @param extra_env: extra environment variables to pass @type extra_env: C{dict} @param cwd: directory to swith to when running the command, defaults to I{self.path} @type cwd: C{str} @return: stdout, return code @rtype: C{tuple} of C{list} of C{str} and C{int}
@deprecated: use L{gbp.git.repository.GitRepository._git_inout} instead. """ output = []
if not cwd: cwd = self.path
env = self.__build_env(extra_env) cmd = ['git', command] + args log.debug(cmd) popen = subprocess.Popen(cmd, stdout=subprocess.PIPE, env=env, cwd=cwd) while popen.poll() == None: output += popen.stdout.readlines() output += popen.stdout.readlines() return output, popen.returncode
capture_stderr=False): """ Run a git command with input and return output
@param command: git command to run @type command: C{str} @param input: input to pipe to command @type input: C{str} @param args: list of arguments @type args: C{list} @param extra_env: extra environment variables to pass @type extra_env: C{dict} @param capture_stderr: whether to capture stderr @type capture_stderr: C{bool} @return: stdout, stderr, return code @rtype: C{tuple} of C{str}, C{str}, C{int} """
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=stderr_arg, env=env, cwd=cwd)
""" Execute git command with arguments args and environment env at path.
@param command: git command @type command: C{str} @param args: command line arguments @type args: C{list} @param extra_env: extra environment variables to set when running command @type extra_env: C{dict} """ try: GitCommand(command, args, extra_env=extra_env, cwd=self.path)() except CommandExecFailed as excobj: raise GitRepositoryError("Error running git %s: %s" % (command, excobj))
""" Check if the git command has certain feature enabled.
@param command: git command @type command: C{str} @param feature: feature / command option to check @type feature: C{str} @return: True if feature is supported @rtype: C{bool} """ args = GitArgs(command, '-m') help, foo, ret = self._git_inout('help', args.args) if ret: raise GitRepositoryError("Invalid git command: %s" % command)
# Parse git command man page section_re = re.compile(r'^(?P<section>[A-Z].*)') option_re = re.compile(r'--?(?P<name>[a-zA-Z\-]+).*') man_section = None for line in help.splitlines(): if man_section == "OPTIONS" and line.startswith(' -'): opts = line.split(',') for opt in opts: match = option_re.match(opt.strip()) if match and match.group('name') == feature: return True # Check man section match = section_re.match(line) if match: man_section = match.group('section') return False
def path(self): """The absolute path to the repository"""
def git_dir(self): """The absolute path to git's metadata""" return os.path.join(self.path, self._git_dir)
def bare(self): """Wheter this is a bare repository""" return self._bare
def tags(self): """List of all tags in the repository""" return self.get_tags()
def branch(self): """The currently checked out branch""" try: return self.get_branch() except GitRepositoryError: return None
def head(self): """return the SHA1 of the current HEAD""" return self.rev_parse('HEAD')
#{ Branches and Merging """ Rename branch
@param branch: name of the branch to be renamed @param newbranch: new name of the branch """ args = GitArgs("-m", branch, newbranch) self._git_command("branch", args.args)
""" Create a new branch
@param branch: the branch's name @param rev: where to start the branch from
If rev is None the branch starts form the current HEAD. """ args = GitArgs(branch) args.add_true(rev, rev) self._git_command("branch", args.args)
""" Delete branch I{branch}
@param branch: name of the branch to delete @type branch: C{str} @param remote: delete a remote branch @param remote: C{bool} """ args = GitArgs('-D') args.add_true(remote, '-r') args.add(branch)
if self.branch != branch: self._git_command("branch", args.args) else: raise GitRepositoryError("Can't delete the branch you're on")
""" On what branch is the current working copy
@return: current branch @rtype: C{str} """ out, ret = self._git_getoutput('symbolic-ref', [ 'HEAD' ]) if ret: raise GitRepositoryError("Currently not on a branch")
ref = out[0][:-1] # Check if ref really exists failed = self._git_getoutput('show-ref', [ ref ])[1] if not failed: return ref[11:] # strip /refs/heads
""" Check if the repository has branch named I{branch}.
@param branch: branch to look for @param remote: only look for remote branches @type remote: C{bool} @return: C{True} if the repository has this branch, C{False} otherwise @rtype: C{bool} """ if remote: ref = 'refs/remotes/%s' % branch else: ref = 'refs/heads/%s' % branch failed = self._git_getoutput('show-ref', [ ref ])[1] if failed: return False return True
""" Switch to branch I{branch}
@param branch: name of the branch to switch to @type branch: C{str} """ if self.branch == branch: return
if self.bare: self._git_command("symbolic-ref", [ 'HEAD', 'refs/heads/%s' % branch ]) else: self._git_command("checkout", [ branch ])
""" Get the branch we'd merge from
@return: repo and branch we would merge from @rtype: C{str} """ try: remote = self.get_config("branch.%s.remote" % branch) merge = self.get_config("branch.%s.merge" % branch) except KeyError: return None remote += merge.replace("refs/heads","", 1) return remote
""" Get the common ancestor between two commits
@param commit1: commit SHA1 or name of a branch or tag @type commit1: C{str} @param commit2: commit SHA1 or name of a branch or tag @type commit2: C{str} @return: SHA1 of the common ancestor @rtype: C{str} """ args = GitArgs() args.add(commit1) args.add(commit2) sha1, stderr, ret = self._git_inout('merge-base', args.args, capture_stderr=True) if not ret: return self.strip_sha1(sha1) else: raise GitRepositoryError("Failed to get common ancestor: %s" % stderr.strip())
""" Merge changes from the named commit into the current branch
@param commit: the commit to merge from (usually a branch name or tag) @type commit: C{str} @param verbose: whether to print a summary after the merge @type verbose: C{bool} @param edit: wheter to invoke an editor to edit the merge message @type edit: C{bool} """ args = GitArgs() args.add_cond(verbose, '--summary', '--no-summary') if (self._cmd_has_feature('merge', 'edit')): args.add_cond(edit, '--edit', '--no-edit') else: log.debug("Your git suite doesn't support --edit/--no-edit " "option for git-merge ") args.add(commit) self._git_command("merge", args.args)
""" Check if an update I{from from_branch} to I{to_branch} would be a fast forward or if the branch is up to date already.
@return: can_fast_forward, up_to_date @rtype: C{tuple} """ has_local = False # local repo has new commits has_remote = False # remote repo has new commits out = self._git_getoutput('rev-list', ["--left-right", "%s...%s" % (from_branch, to_branch), "--"])[0]
if not out: # both branches have the same commits return True, True
for line in out: if line.startswith("<"): has_local = True elif line.startswith(">"): has_remote = True
if has_local and has_remote: return False, False elif has_local: return False, True elif has_remote: return True, False
""" Get a list of branches
@param remote: whether to list local or remote branches @type remote: C{bool} @return: local or remote branches @rtype: C{list} """ args = [ '--format=%(refname:short)' ] args += [ 'refs/remotes/' ] if remote else [ 'refs/heads/' ] out = self._git_getoutput('for-each-ref', args)[0] return [ ref.strip() for ref in out ]
""" Get a list of local branches
@return: local branches @rtype: C{list} """ return self._get_branches(remote=False)
""" Get a list of remote branches
@return: remote branches @rtype: C{list} """ return self._get_branches(remote=True)
""" Update ref I{ref} to commit I{new} if I{ref} currently points to I{old}
@param ref: the ref to update @type ref: C{str} @param new: the new value for ref @type new: C{str} @param old: the old value of ref @type old: C{str} @param msg: the reason for the update @type msg: C{str} """ args = [ ref, new ] if old: args += [ old ] if msg: args = [ '-m', msg ] + args self._git_command("update-ref", args)
""" Check if branch I{branch} contains commit I{commit}
@param branch: the branch the commit should be on @type branch: C{str} @param commit: the C{str} commit to check @type commit: C{str} @param remote: whether to check remote instead of local branches @type remote: C{bool} """ args = GitArgs() args.add_true(remote, '-r') args.add('--contains') args.add(commit)
out, ret = self._git_getoutput('branch', args.args) for line in out: # remove prefix '*' for current branch before comparing line = line.replace('*', '') if line.strip() == branch: return True return False
""" Set upstream branches for local branch
@param local_branch: name of the local branch @type local_branch: C{str} @param upstream: remote/branch, for example origin/master @type upstream: C{str} """
# check if both branches exist for branch, remote in [(local_branch, False), (upstream, True)]: if not self.has_branch(branch, remote=remote): raise GitRepositoryError("Branch %s doesn't exist!" % branch)
self._git_getoutput('branch', ["--set-upstream", local_branch, upstream])
""" Get upstream branch for the local branch
@param local_branch: name fo the local branch @type local_branch: C{str} @return: upstream (remote/branch) or '' if no upstream found @rtype: C{str}
""" args = GitArgs('--format=%(upstream:short)') if self.has_branch(local_branch, remote=False): args.add('refs/heads/%s' % local_branch) else: raise GitRepositoryError("Branch %s doesn't exist!" % local_branch)
out = self._git_getoutput('for-each-ref', args.args)[0]
return out[0].strip()
#{ Tags
""" Create a new tag.
@param name: the tag's name @type name: C{str} @param msg: The tag message. @type msg: C{str} @param commit: the commit or object to create the tag at, default is I{HEAD} @type commit: C{str} @param sign: Whether to sing the tag @type sign: C{bool} @param keyid: the GPG keyid used to sign the tag @type keyid: C{str} """ args = [] args += [ '-m', msg ] if msg else [] if sign: args += [ '-s' ] args += [ '-u', keyid ] if keyid else [] args += [ name ] args += [ commit ] if commit else [] self._git_command("tag", args)
""" Delete a tag named I{tag}
@param tag: the tag to delete @type tag: C{str} """ if self.has_tag(tag): self._git_command("tag", [ "-d", tag ])
self._git_command("tag", [ new, old ]) self.delete_tag(old)
""" Check if the repository has a tag named I{tag}.
@param tag: tag to look for @type tag: C{str} @return: C{True} if the repository has that tag, C{False} otherwise @rtype: C{bool} """ out, ret = self._git_getoutput('tag', [ '-l', tag ]) return [ False, True ][len(out)]
""" Find the closest tag to a given commit
@param commit: the commit to describe @type commit: C{str} @param pattern: only look for tags matching I{pattern} @type pattern: C{str} @return: the found tag @rtype: C{str} """ args = [ '--abbrev=0' ] if pattern: args += [ '--match' , pattern ] args += [ commit ]
tag, err, ret = self._git_inout('describe', args, capture_stderr=True) if ret: raise GitRepositoryError("Can't find tag for %s. Git error: %s" % \ (commit, err.strip())) return tag.strip()
""" List tags
@param pattern: only list tags matching I{pattern} @type pattern: C{str} @return: tags @rtype: C{list} of C{str} """ args = [ '-l', pattern ] if pattern else [] return [ line.strip() for line in self._git_getoutput('tag', args)[0] ]
""" Verify a signed tag
@param tag: the tag's name @type tag: C{str} @return: Whether the signature on the tag could be verified @rtype: C{bool} """ args = GitArgs('-v', tag)
try: self._git_command('tag', args.args) except GitRepositoryError: return False return True
#} """ Force HEAD to a specific commit
@param commit: commit to move HEAD to @param hard: also update the working copy @type hard: C{bool} """ if not GitCommit.is_sha1(commit): commit = self.rev_parse(commit)
if self.bare: ref = "refs/heads/%s" % self.get_branch() self._git_command("update-ref", [ ref, commit ]) else: args = ['--quiet'] if hard: args += [ '--hard' ] args += [ commit, '--' ] self._git_command("reset", args)
""" Does the repository contain any uncommitted modifications?
@param ignore_untracked: whether to ignore untracked files when checking the repository status @type ignore_untracked: C{bool} @return: C{True} if the repository is clean, C{False} otherwise and Git's status message @rtype: C{tuple} """ if self.bare: return (True, '')
clean_msg = 'nothing to commit'
args = GitArgs() args.add_true(ignore_untracked, '-uno')
out, ret = self._git_getoutput('status', args.args, extra_env={'LC_ALL': 'C'}) if ret: raise GbpError("Can't get repository status") ret = False for line in out: if line.startswith('#'): continue if line.startswith(clean_msg): ret = True break return (ret, "".join(out))
""" Is the repository empty?
@return: True if the repositorydoesn't have any commits, False otherwise @rtype: C{bool} """ # an empty repo has no branches: return False if self.branch else True
""" Find the SHA1 of a given name
@param name: the name to look for @type name: C{str} @param short: try to abbreviate SHA1 to given length @type short: C{int} @return: the name's sha1 @rtype: C{str} """ args = GitArgs("--quiet", "--verify") args.add_cond(short, '--short=%d' % short) args.add(name) sha, ret = self._git_getoutput('rev-parse', args.args) if ret: raise GitRepositoryError("revision '%s' not found" % name) return self.strip_sha1(sha[0], short)
""" Strip a given sha1 and check if the resulting hash has the expected length.
>>> GitRepository.strip_sha1(' 58ef37dbeb12c44b206b92f746385a6f61253c0a\\n') '58ef37dbeb12c44b206b92f746385a6f61253c0a' >>> GitRepository.strip_sha1('58ef37d', 10) Traceback (most recent call last): ... GitRepositoryError: '58ef37d' is not a valid sha1 of length 10 >>> GitRepository.strip_sha1('58ef37d', 7) '58ef37d' >>> GitRepository.strip_sha1('123456789', 7) '123456789' >>> GitRepository.strip_sha1('foobar') Traceback (most recent call last): ... GitRepositoryError: 'foobar' is not a valid sha1 """ maxlen = 40 s = sha1.strip()
l = length or maxlen
if len(s) < l or len(s) > maxlen: raise GitRepositoryError("'%s' is not a valid sha1%s" % (s, " of length %d" % l if length else "")) return s
#{ Trees """ Checkout treeish
@param treeish: the treeish to check out @type treeish: C{str} """ self._git_command("checkout", ["--quiet", treeish])
""" Check if the repository has the treeish object I{treeish}.
@param treeish: treeish object to look for @type treeish: C{str} @return: C{True} if the repository has that tree, C{False} otherwise @rtype: C{bool} """ out, ret = self._git_getoutput('ls-tree', [ treeish ]) return [ True, False ][ret != 0]
""" Create a tree object from the current index
@param index_file: alternate index file to read changes from @type index_file: C{str} @return: the new tree object's sha1 @rtype: C{str} """ if index_file: extra_env = {'GIT_INDEX_FILE': index_file } else: extra_env = None
tree, ret = self._git_getoutput('write-tree', extra_env=extra_env) if ret: raise GitRepositoryError("Can't write out current index") return tree[0].strip()
""" Create a tree based on contents. I{contents} has the same format than the I{GitRepository.list_tree} output. """ out='' args = GitArgs('-z')
for obj in contents: mode, type, sha1, name = obj out += '%s %s %s\t%s\0' % (mode, type, sha1, name)
sha1, err, ret = self._git_inout('mktree', args.args, out, capture_stderr=True) if ret: raise GitRepositoryError("Failed to mktree: '%s'" % err) return self.strip_sha1(sha1)
""" Get type of a git repository object
@param obj: repository object @type obj: C{str} @return: type of the repository object @rtype: C{str} """ out, ret = self._git_getoutput('cat-file', args=['-t', obj]) if ret: raise GitRepositoryError("Not a Git repository object: '%s'" % obj) return out[0].strip()
""" Get a trees content. It returns a list of objects that match the 'ls-tree' output: [ mode, type, sha1, path ].
@param treeish: the treeish object to list @type treeish: C{str} @param recurse: whether to list the tree recursively @type recurse: C{bool} @return: the tree @rtype: C{list} of objects. See above. """ args = GitArgs('-z') args.add_true(recurse, '-r') args.add(treeish)
out, err, ret = self._git_inout('ls-tree', args.args, capture_stderr=True) if ret: raise GitRepositoryError("Failed to ls-tree '%s': '%s'" % (treeish, err))
tree = [] for line in out.split('\0'): if line: tree.append(line.split(None, 3)) return tree
#}
""" Gets the config value associated with I{name}
@param name: config value to get @return: fetched config value @rtype: C{str} """ value, ret = self._git_getoutput('config', [ name ]) if ret: raise KeyError return value[0][:-1] # first line with \n ending removed
""" Determine a sane values for author name and author email from git's config and environment variables.
@return: name and email @rtype: L{GitModifier} """ try: name = self.get_config("user.name") except KeyError: name = os.getenv("USER") try: email = self.get_config("user.email") except KeyError: email = os.getenv("EMAIL") email = os.getenv("GIT_AUTHOR_EMAIL", email) name = os.getenv("GIT_AUTHOR_NAME", name) return GitModifier(name, email)
#{ Remote Repositories
""" Get all remote repositories
@return: remote repositories @rtype: C{list} of C{str} """ out = self._git_getoutput('remote')[0] return [ remote.strip() for remote in out ]
""" Do we know about a remote named I{name}?
@param name: name of the remote repository @type name: C{str} @return: C{True} if the remote repositore is known, C{False} otherwise @rtype: C{bool} """ if name in self.get_remote_repos(): return True else: return False
""" Add a tracked remote repository
@param name: the name to use for the remote @type name: C{str} @param url: the url to add @type url: C{str} @param tags: whether to fetch tags @type tags: C{bool} @param fetch: whether to fetch immediately from the remote side @type fetch: C{bool} """ args = GitArgs('add') args.add_false(tags, '--no-tags') args.add_true(fetch, '--fetch') args.add(name, url) self._git_command("remote", args.args)
args = GitArgs('rm', name) self._git_command("remote", args.args)
""" Download objects and refs from another repository.
@param repo: repository to fetch from @type repo: C{str} @param tags: whether to fetch all tag objects @type tags: C{bool} @param depth: deepen the history of (shallow) repository to depth I{depth} @type depth: C{int} """ args = GitArgs('--quiet') args.add_true(tags, '--tags') args.add_cond(depth, '--depth=%s' % depth) args.add_cond(repo, repo)
self._git_command("fetch", args.args)
""" Fetch and merge from another repository
@param repo: repository to fetch from @type repo: C{str} @param ff_only: only merge if this results in a fast forward merge @type ff_only: C{bool} """ args = [] args += [ '--ff-only' ] if ff_only else [] args += [ repo ] if repo else [] self._git_command("pull", args)
""" Push changes to the remote repo
@param repo: repository to push to @type repo: C{str} @param src: the source ref to push @type src: C{str} @param dst: the name of the destination ref to push to @type dst: C{str} @param ff_only: only push if it's a fast forward update @type ff_only: C{bool} """ args = GitArgs() args.add_cond(repo, repo)
# Allow for src == '' to delete dst on the remote if src != None: refspec = src if dst: refspec += ':%s' % dst if not ff_only: refspec = '+%s' % refspec args.add(refspec) self._git_command("push", args.args)
""" Push a tag to the remote repo
@param repo: repository to push to @type repo: C{str} @param tag: the name of the tag @type tag: C{str} """ args = GitArgs(repo, 'tag', tag) self._git_command("push", args.args)
#{ Files
""" Add files to a the repository
@param paths: list of files to add @type paths: list or C{str} @param force: add files even if they would be ignored by .gitignore @type force: C{bool} @param index_file: alternative index file to use @param work_tree: alternative working tree to use """ extra_env = {}
if isinstance(paths, basestring): paths = [ paths ]
args = [ '-f' ] if force else []
if index_file: extra_env['GIT_INDEX_FILE'] = index_file
if work_tree: extra_env['GIT_WORK_TREE'] = work_tree
self._git_command("add", args + paths, extra_env)
""" Remove files from the repository
@param paths: list of files to remove @param paths: C{list} or C{str} @param verbose: be verbose @type verbose: C{bool} """ if isinstance(paths, basestring): paths = [ paths ]
args = [] if verbose else ['--quiet'] self._git_command("rm", args + paths)
""" List files in index and working tree
@param types: list of types to show @type types: C{list} @return: list of files @rtype: C{list} of C{str} """ all_types = [ 'cached', 'deleted', 'others', 'ignored', 'stage' 'unmerged', 'killed', 'modified' ] args = [ '-z' ]
for t in types: if t in all_types: args += [ '--%s' % t ] else: raise GitRepositoryError("Unknown type '%s'" % t) out, ret = self._git_getoutput('ls-files', args) if ret: raise GitRepositoryError("Error listing files: '%d'" % ret) if out: return [ file for file in out[0].split('\0') if file ] else: return []
""" Hash a single file and write it into the object database
@param filename: the filename to the content of the file to hash @type filename: C{str} @param filters: whether to run filters @type filters: C{bool} @return: the hash of the file @rtype: C{str} """ args = GitArgs('-w', '-t', 'blob') args.add_false(filters, '--no-filters') args.add(filename)
sha1, stderr, ret = self._git_inout('hash-object', args.args, capture_stderr=True) if not ret: return self.strip_sha1(sha1) else: raise GbpError("Failed to hash %s: %s" % (filename, stderr)) #}
#{ Comitting
extra_env = author_info.get_author_env() if author_info else None self._git_command("commit", ['-q', '-m', msg] + args, extra_env=extra_env)
""" Commit currently staged files to the repository
@param msg: commit message @type msg: C{str} @param author_info: authorship information @type author_info: L{GitModifier} @param edit: whether to spawn an editor to edit the commit info @type edit: C{bool} """ args = GitArgs() args.add_true(edit, '--edit') self._commit(msg=msg, args=args.args, author_info=author_info)
""" Commit all changes to the repository @param msg: commit message @type msg: C{str} @param author_info: authorship information @type author_info: L{GitModifier} """ args = GitArgs('-a') args.add_true(edit, '--edit') self._commit(msg=msg, args=args.args, author_info=author_info)
""" Commit the given files to the repository
@param files: file or files to commit @type files: C{str} or C{list} @param msg: commit message @type msg: C{str} @param author_info: authorship information @type author_info: L{GitModifier} """ if isinstance(files, basestring): files = [ files ] self._commit(msg=msg, args=files, author_info=author_info)
author={}, committer={}, create_missing_branch=False): """ Replace the current tip of branch I{branch} with the contents from I{unpack_dir}
@param unpack_dir: content to add @type unpack_dir: C{str} @param msg: commit message to use @type msg: C{str} @param branch: branch to add the contents of unpack_dir to @type branch: C{str} @param other_parents: additional parents of this commit @type other_parents: C{list} of C{str} @param author: author information to use for commit @type author: C{dict} with keys I{name}, I{email}, I{date} @param committer: committer information to use for commit @type committer: C{dict} with keys I{name}, I{email}, I{date} or L{GitModifier} @param create_missing_branch: create I{branch} as detached branch if it doesn't already exist. @type create_missing_branch: C{bool} """
git_index_file = os.path.join(self.path, self._git_dir, 'gbp_index') try: os.unlink(git_index_file) except OSError: pass self.add_files('.', force=True, index_file=git_index_file, work_tree=unpack_dir) tree = self.write_tree(git_index_file)
if branch: try: cur = self.rev_parse(branch) except GitRepositoryError: if create_missing_branch: log.debug("Will create missing branch '%s'..." % branch) cur = None else: raise else: # emtpy repo cur = None branch = 'master'
# Build list of parents: parents = [] if cur: parents = [ cur ] if other_parents: for parent in other_parents: sha = self.rev_parse(parent) if sha not in parents: parents += [ sha ]
commit = self.commit_tree(tree=tree, msg=msg, parents=parents, author=author, committer=committer) if not commit: raise GbpError("Failed to commit tree") self.update_ref("refs/heads/%s" % branch, commit, cur) return commit
""" Commit a tree with commit msg I{msg} and parents I{parents}
@param tree: tree to commit @param msg: commit message @param parents: parents of this commit @param author: authorship information @type author: C{dict} with keys 'name' and 'email' or L{GitModifier} @param committer: comitter information @type committer: C{dict} with keys 'name' and 'email' """ extra_env = {} for key, val in author.items(): if val: extra_env['GIT_AUTHOR_%s' % key.upper()] = val for key, val in committer.items(): if val: extra_env['GIT_COMMITTER_%s' % key.upper()] = val
args = [ tree ] for parent in parents: args += [ '-p' , parent ] sha1, stderr, ret = self._git_inout('commit-tree', args, msg, extra_env, capture_stderr=True) if not ret: return self.strip_sha1(sha1) else: raise GbpError("Failed to commit tree: %s" % stderr)
#{ Commit Information
first_parent=False, options=None): """ Get commits from since to until touching paths
@param since: commit to start from @type since: C{str} @param until: last commit to get @type until: C{str} @param paths: only list commits touching paths @type paths: C{list} of C{str} @param num: maximum number of commits to fetch @type num: C{int} @param options: list of additional options passed to git log @type options: C{list} of C{str}ings @param first_parent: only follow first parent when seeing a merge commit @type first_parent: C{bool} """ args = GitArgs('--pretty=format:%H') args.add_true(num, '-%d' % num) args.add_true(first_parent, '--first-parent') if since: args.add("%s..%s" % (since, until or 'HEAD')) elif until: args.add(until) args.add_cond(options, options) args.add("--") if isinstance(paths, basestring): paths = [ paths ] args.add_cond(paths, paths)
commits, ret = self._git_getoutput('log', args.args) if ret: where = " on %s" % paths if paths else "" raise GitRepositoryError("Error getting commits %s..%s%s" % (since, until, where)) return [ commit.strip() for commit in commits ]
"""git-show id""" commit, ret = self._git_getoutput('show', [ "--pretty=medium", id ]) if ret: raise GitRepositoryError("can't get %s" % id) for line in commit: yield line
""" Get commmits matching I{regex}
@param regex: regular expression @type regex: C{str} @param since: where to start grepping (e.g. a branch) @type since: C{str} """ args = ['--pretty=format:%H'] args.append("--grep=%s" % regex) if since: args.append(since) args.append('--')
commits, ret = self._git_getoutput('log', args) if ret: raise GitRepositoryError("Error grepping log for %s" % regex) return [ commit.strip() for commit in commits[::-1] ]
""" Gets the subject of a commit.
@param commit: the commit to get the subject from @return: the commit's subject @rtype: C{str} """ out, ret = self._git_getoutput('log', ['-n1', '--pretty=format:%s', commit]) if ret: raise GitRepositoryError("Error getting subject of commit %s" % commit) return out[0].strip()
""" Look up data of a specific commit-ish. Dereferences given commit-ish to the commit it points to.
@param commitish: the commit-ish to inspect @return: the commit's including id, author, email, subject and body @rtype: dict """ commit_sha1 = self.rev_parse("%s^0" % commitish) args = GitArgs('--pretty=format:%an%x00%ae%x00%ad%x00%cn%x00%ce%x00%cd%x00%s%x00%b%x00', '-z', '--date=raw', '--name-status', commit_sha1) out, err, ret = self._git_inout('show', args.args) if ret: raise GitRepositoryError("Unable to retrieve commit info for %s" % commitish)
fields = out.split('\x00')
author = GitModifier(fields[0].strip(), fields[1].strip(), fields[2].strip()) committer = GitModifier(fields[3].strip(), fields[4].strip(), fields[5].strip())
files = defaultdict(list) file_fields = fields[8:] # For some reason git returns one extra empty field for merge commits if file_fields[0] == '': file_fields.pop(0) while len(file_fields) and file_fields[0] != '': status = file_fields.pop(0).strip() path = file_fields.pop(0) files[status].append(path)
return {'id' : commitish, 'author' : author, 'committer' : committer, 'subject' : fields[6], 'body' : fields[7], 'files' : files}
#{ Patches """ Output the commits between start and end as patches in output_dir """ options = GitArgs('-N', '-k', '-o', output_dir) options.add_cond(not signature, '--no-signature') options.add('%s...%s' % (start, end)) options.add_cond(thread, '--thread=%s' % thread, '--no-thread')
output, ret = self._git_getoutput('format-patch', options.args) return [ line.strip() for line in output ]
"""Apply a patch using git apply""" args = [] if context: args += [ '-C', context ] if index: args.append("--index") if strip != None: args += [ '-p', str(strip) ] args.append(patch) self._git_command("apply", args)
""" Diff two git repository objects
@param obj1: first object @type obj1: C{str} @param obj2: second object @type obj2: C{str} @param paths: List of paths to diff @type paths: C{list} @return: diff @rtype: C{str} """ options = GitArgs(obj1, obj2) if paths: options.add('--', paths) output, stderr, ret = self._git_inout('diff', options.args) if ret: raise GitRepositoryError("Git diff failed") return output #}
""" Create an archive from a treeish
@param format: the type of archive to create, e.g. 'tar.gz' @type format: C{str} @param prefix: prefix to prepend to each filename in the archive @type prefix: C{str} @param output: the name of the archive to create @type output: C{str} @param treeish: the treeish to create the archive from @type treeish: C{str} @param kwargs: additional commandline options passed to git-archive """ args = [ '--format=%s' % format, '--prefix=%s' % prefix, '--output=%s' % output, treeish ] out, ret = self._git_getoutput('archive', args, **kwargs) if ret: raise GitRepositoryError("Unable to archive %s" % treeish)
""" Cleanup unnecessary files and optimize the local repository
param auto: only cleanup if required param auto: C{bool} """ args = [ '--auto' ] if auto else [] self._git_command("gc", args)
#{ Submodules
""" Does the repo have any submodules?
@return: C{True} if the repository has any submodules, C{False} otherwise @rtype: C{bool} """ if os.path.exists(os.path.join(self.path, '.gitmodules')): return True else: return False
""" Add a submodule
@param repo_path: path to submodule @type repo_path: C{str} """ self._git_command("submodule", [ "add", repo_path ])
""" Update all submodules
@param init: whether to initialize the submodule if necessary @type init: C{bool} @param recursive: whether to update submodules recursively @type recursive: C{bool} @param fetch: whether to fetch new objects @type fetch: C{bool} """
if not self.has_submodules(): return args = [ "update" ] if recursive: args.append("--recursive") if init: args.append("--init") if not fetch: args.append("--no-fetch")
self._git_command("submodule", args)
""" List the submodules of treeish
@return: a list of submodule/commit-id tuples @rtype: list of tuples """ # Note that we is lstree instead of submodule commands because # there's no way to list the submodules of another branch with # the latter. submodules = [] if path is None: path = self.path
args = [ treeish ] if recursive: args += ['-r']
out, ret = self._git_getoutput('ls-tree', args, cwd=path) for line in out: mode, objtype, commit, name = line[:-1].split(None, 3) # A submodules is shown as "commit" object in ls-tree: if objtype == "commit": nextpath = os.path.join(path, name) submodules.append( (nextpath.replace(self.path,'').lstrip('/'), commit) ) if recursive: submodules += self.get_submodules(commit, path=nextpath, recursive=recursive) return submodules
#{ Repository Creation
""" Create a repository at path
@param path: where to create the repository @type path: C{str} @param bare: whether to create a bare repository @type bare: C{bool} @return: git repository object @rtype: L{GitRepository} """ args = GitArgs() abspath = os.path.abspath(path)
args.add_true(bare, '--bare') git_dir = '' if bare else '.git'
try: if not os.path.exists(abspath): os.makedirs(abspath) try: GitCommand("init", args.args, cwd=abspath)() except CommandExecFailed as excobj: raise GitRepositoryError("Error running git init: %s" % excobj)
if description: with file(os.path.join(abspath, git_dir, "description"), 'w') as f: description += '\n' if description[-1] != '\n' else '' f.write(description) return klass(abspath) except OSError as err: raise GitRepositoryError("Cannot create Git repository at '%s': %s" % (abspath, err[1])) return None
bare=False, auto_name=True): """ Clone a git repository at I{remote} to I{path}.
@param path: where to clone the repository to @type path: C{str} @param remote: URL to clone @type remote: C{str} @param depth: create a shallow clone of depth I{depth} @type depth: C{int} @param recursive: whether to clone submodules @type recursive: C{bool} @param mirror: whether to pass --mirror to git-clone @type mirror: C{bool} @param bare: whether to create a bare repository @type bare: C{bool} @param auto_name: If I{True} create a directory below I{path} based on the I{remote}s name. Otherwise create the repo directly at I{path}. @type auto_name: C{bool} @return: git repository object @rtype: L{GitRepository} """ abspath = os.path.abspath(path) if auto_name: name = None else: abspath, name = abspath.rsplit('/', 1)
args = GitArgs('--quiet') args.add_true(depth, '--depth', depth) args.add_true(recursive, '--recursive') args.add_true(mirror, '--mirror') args.add_true(bare, '--bare') args.add(remote) args.add_true(name, name) try: if not os.path.exists(abspath): os.makedirs(abspath)
try: GitCommand("clone", args.args, cwd=abspath)() except CommandExecFailed as excobj: raise GitRepositoryError("Error running git clone: %s" % excobj)
if not name: name = remote.rstrip('/').rsplit('/',1)[1] if (mirror or bare): if not name.endswith('.git'): name = "%s.git" % name elif name.endswith('.git'): name = name[:-4] return klass(os.path.join(abspath, name)) except OSError as err: raise GitRepositoryError("Cannot clone Git repository " "'%s' to '%s': %s" % (remote, abspath, err[1])) return None #}
|