From: Guido Günther Date: Thu, 27 Oct 2011 18:00:37 +0000 (+0200) Subject: GitRepository: Reorder into groups X-Git-Tag: debian/0.6.0_git20111202~55 X-Git-Url: http://review.tizen.org/git/?a=commitdiff_plain;h=c4f9f23dace4b3ea87955f9ebe093f302b75f810;p=tools%2Fgit-buildpackage.git GitRepository: Reorder into groups --- diff --git a/gbp/git.py b/gbp/git.py index a14bc86..01cb060 100644 --- a/gbp/git.py +++ b/gbp/git.py @@ -59,7 +59,7 @@ class GitModifier(object): {'GIT_AUTHOR_EMAIL': 'bar', 'GIT_AUTHOR_NAME': 'foo'} @return: Author information suitable to use as environment variables - @rtype: dict + @rtype: C{dict} """ return self._get_env('author') @@ -72,7 +72,7 @@ class GitModifier(object): {'GIT_COMMITTER_NAME': 'foo', 'GIT_COMMITTER_EMAIL': 'bar'} @return: Commiter information suitable to use as environment variables - @rtype: dict + @rtype: C{dict} """ return self._get_env('committer') @@ -82,22 +82,10 @@ class GitRepository(object): 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}. - Bare repository aren't currently supported. - - @ivar path: The path to the working tree. - @type path: string - - @group Repository Creation: create clone - @group Branches and Merging: create_branch delete_branch get_branch - _get_branches get_local_branches get_merge_branch get_remote_branches - has_branch is_fast_forward merge set_branch - @group Tags: _build_legacy_tag create_tag delete_tag find_tag get_tags - has_tag move_tag find_version - @group Submodules: add_submodule get_submodules has_submodules - update_submodules - @group Patches: apply_patch format_patches - @group Remote Repositories: add_remote_repo get_remote_repos - has_remote_repo + @ivar _path: The path to the working tree + @type _path: C{str} + @ivar _bare: Whether this is a bare repository + @type _bare: C{bool} """ def _check_bare(self): @@ -134,15 +122,15 @@ class GitRepository(object): Run a git command and return the output @param command: git command to run - @type command: string + @type command: C{str} @param args: list of arguments - @type args: list + @type args: C{list} @param extra_env: extra environment variables to pass - @type extra_env: dict + @type extra_env: C{dict} @param cwd: directory to swith to when running the command, defaults to I{self.path} - @type cwd: string + @type cwd: C{str} @return: stdout, return code - @rtype: tuple + @rtype: C{tuple} """ output = [] @@ -164,15 +152,15 @@ class GitRepository(object): Run a git command with input and return output @param command: git command to run - @type command: string + @type command: C{str} @param input: input to pipe to command - @type input: string + @type input: C{str} @param args: list of arguments - @type args: list + @type args: C{list} @param extra_env: extra environment variables to pass - @type extra_env: dict + @type extra_env: C{dict} @return: stdout, stderr, return code - @rtype: tuple + @rtype: C{tuple} """ env = self.__build_env(extra_env) cmd = ['git', command] + args @@ -191,23 +179,22 @@ class GitRepository(object): at path. @param command: git command - @type command: string + @type command: C{str} @param args: command line arguments - @type args: list + @type args: C{list} @param extra_env: extra environment variables to set when running command - @type extra_env: dict + @type extra_env: C{dict} """ GitCommand(command, args, extra_env=extra_env, cwd=self.path)() @property def path(self): + """The path to the repository""" return self._path @property def base_dir(self): - """ - Get the base of the repository. - """ + """Get the base of the repository""" return os.path.join(self.path, '.git') @property @@ -215,151 +202,54 @@ class GitRepository(object): """Wheter this is a bare repository""" return self._bare - def has_branch(self, branch, remote=False): - """ - Check if the repository has branch named I{branch}. - - @param branch: branch to look for - @param remote: only look for remote branches - @type remote: bool - @return: C{True} if the repository has this branch, C{False} otherwise - @rtype: 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 - - def has_treeish(self, treeish): - """ - Check if the repository has the treeish object I{treeish}. - - @param treeish: treeish object to look for - @type treeish: string - @return: C{True} if the repository has that tree, C{False} otherwise - @rtype: bool - """ - - out, ret = self.__git_getoutput('ls-tree', [ treeish ]) - return [ True, False ][ret != 0] - - def has_tag(self, tag): - """ - Check if the repository has a tag named I{tag}. - - @param tag: tag to look for - @type tag: string - @return: C{True} if the repository has that tag, C{False} otherwise - @rtype: bool - """ - out, ret = self.__git_getoutput('tag', [ '-l', tag ]) - return [ False, True ][len(out)] - - - def _build_legacy_tag(self, format, version): - """legacy version numbering""" - if ':' in version: # strip of any epochs - version = version.split(':', 1)[1] - version = version.replace('~', '.') - return format % dict(version=version) - - - def find_version(self, format, version): - """ - Check if a certain version is stored in this repo. Return it's SHA1 in - this case. For legacy tags Don't check only the tag but also the - message, since the former wasn't injective until recently. - You only need to use this funciton if you also need to check for legacy - tags. - - @param format: tag pattern - @param version: debian version number - @return: sha1 of the version tag - """ - tag = build_tag(format, version) - legacy_tag = self._build_legacy_tag(format, version) - if self.has_tag(tag): # new tags are injective - return self.rev_parse(tag) - elif self.has_tag(legacy_tag): - out, ret = self.__git_getoutput('cat-file', args=['-p', legacy_tag]) - if ret: - return None - for line in out: - if line.endswith(" %s\n" % version): - return self.rev_parse(legacy_tag) - elif line.startswith('---'): # GPG signature start - return None - return None - + @property + def tags(self): + """List of all tags in the repository""" + return self.get_tags() - def delete_tag(self, tag): - """ - Delete a tag named I{tag} + @property + def branch(self): + """The currently checked out branch""" + return self.get_branch() - @param tag: the tag to delete - @type tag: string +#{ Branches and Merging + def create_branch(self, branch, rev=None): """ - if self.has_tag(tag): - self._git_command("tag", [ "-d", tag ]) + Create a new branch - def move_tag(self, old, new): - self._git_command("tag", [ new, old ]) - self.delete_tag(old) + @param branch: the branch's name + @param rev: where to start the branch from - def create_tag(self, name, msg=None, commit=None, sign=False, keyid=None): + If rev is None the branch starts form the current HEAD. """ - Create a new tag. + args = [ branch ] + args += [ rev ] if rev else [] - @param name: the tag's name - @type name: string - @param msg: The tag message. - @type msg: string - @param commit: the commit or object to create the tag at, default - is I{HEAD} - @type commit: string - @param sign: Whether to sing the tag - @type sign: bool - @param keyid: the GPG keyid used to sign the tag - @type keyid: string - """ - args = [] - args += [ '-m', msg ] if msg else [] - if sign: - args += [ '-u', keyid ] if keyid else [ '-s' ] - args += [ name ] - args += [ commit ] if commit else [] - self._git_command("tag", args) + self._git_command("branch", args) - def get_tags(self, pattern=None): + def delete_branch(self, branch, remote=False): """ - List tags + Delete branch I{branch} - @param pattern: only list tags matching I{pattern} - @type pattern: string + @param branch: name of the branch to delete + @type branch: C{str} + @param remote: delete a remote branch + @param remote: C{bool} """ - args = [ '-l', pattern ] if pattern else [] - return [ line.strip() for line in self.__git_getoutput('tag', args)[0] ] - - @property - def tags(self): - """List of all tags in the repository""" - return self.get_tags() + args = [ "-D" ] + args += [ "-r" ] if remote else [] - @property - def branch(self): - """The currently checked out branch""" - return self.get_branch() + if self.branch != branch: + self._git_command("branch", args + [branch]) + else: + raise GitRepositoryError, "Can't delete the branch you're on" def get_branch(self): """ On what branch is the current working copy @return: current branch - @rtype: string + @rtype: C{str} """ out, dummy = self.__git_getoutput('symbolic-ref', [ 'HEAD' ]) ref = out[0][:-1] @@ -368,12 +258,47 @@ class GitRepository(object): if not failed: return ref[11:] # strip /refs/heads + def has_branch(self, branch, remote=False): + """ + 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 + + def set_branch(self, branch): + """ + 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 ]) + def get_merge_branch(self, branch): """ Get the branch we'd merge from @return: repo and branch we would merge from - @rtype: string + @rtype: C{str} """ try: remote = self.get_config("branch.%s.remote" % branch) @@ -388,7 +313,7 @@ class GitRepository(object): Merge changes from the named commit into the current branch @param commit: the commit to merge from (usually a branch name) - @type commit: string + @type commit: C{str} """ args = [ "--summary" ] if verbose else [ "--no-summary" ] self._git_command("merge", args + [ commit ]) @@ -399,7 +324,7 @@ class GitRepository(object): forward or if the branch is up to date already. @return: can_fast_forward, up_to_date - @rtype: tuple + @rtype: C{tuple} """ has_local = False # local repo has new commits has_remote = False # remote repo has new commits @@ -423,231 +348,235 @@ class GitRepository(object): elif has_remote: return True, False - def set_branch(self, branch): + def _get_branches(self, remote=False): """ - Switch to branch 'branch' + Get a list of branches - @param branch: name of the branch to switch to + @param remote: whether to list local or remote branches + @type remote: C{bool} + @return: local or remote branches + @rtype: C{list} """ - if self.branch == branch: - return - - if self.bare: - self._git_command("symbolic-ref", - [ 'HEAD', 'refs/heads/%s' % branch ]) - else: - self._git_command("checkout", [ branch ]) + 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 ] - def create_branch(self, branch, rev=None): + def get_local_branches(self): """ - Create a new branch - - @param branch: the branch's name - @param rev: where to start the branch from + Get a list of local branches - If rev is None the branch starts form the current HEAD. + @return: local branches + @rtype: C{list} """ - args = [ branch ] - args += [ rev ] if rev else [] + return self._get_branches(remote=False) - self._git_command("branch", args) - def delete_branch(self, branch, remote=False): + def get_remote_branches(self): """ - Delete branch I{branch} + Get a list of remote branches - @param branch: name of the branch to delete - @type branch: string - @param remote: delete a remote branch - @param remote: bool + @return: remote branches + @rtype: C{list} """ - args = [ "-D" ] - args += [ "-r" ] if remote else [] - - if self.branch != branch: - self._git_command("branch", args + [branch]) - else: - raise GitRepositoryError, "Can't delete the branch you're on" + return self._get_branches(remote=True) - def force_head(self, commit, hard=False): + def update_ref(self, ref, new, old=None, msg=None): """ - Force HEAD to a specific commit + Update ref I{ref} to commit I{new} if I{ref} currently points to + I{old} - @param commit: commit to move HEAD to - @param hard: also update the working copy - @type hard: bool + @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 = ['--quiet'] - if hard: - args += [ '--hard' ] - args += [ commit, '--' ] - self._git_command("reset", args) + args = [ ref, new ] + if old: + args += [ old ] + if msg: + args = [ '-m', msg ] + args + self._git_command("update-ref", args) - def is_clean(self): +#{ Tags + + def create_tag(self, name, msg=None, commit=None, sign=False, keyid=None): """ - Does the repository contain any uncommitted modifications? + Create a new tag. - @return: C{True} if the repository is clean, C{False} otherwise - and Git's status message - @rtype: tuple + @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} """ - clean_msg = 'nothing to commit' - out = self.__git_getoutput('status')[0] - ret = False - for line in out: - if line.startswith('#'): - continue - if line.startswith(clean_msg): - ret = True - break - return (ret, "".join(out)) + args = [] + args += [ '-m', msg ] if msg else [] + if sign: + args += [ '-u', keyid ] if keyid else [ '-s' ] + args += [ name ] + args += [ commit ] if commit else [] + self._git_command("tag", args) - def is_empty(self): + def delete_tag(self, tag): """ - Is the repository empty? + Delete a tag named I{tag} - @return: True if the repositorydoesn't have any commits, - False otherwise - @rtype: bool + @param tag: the tag to delete + @type tag: C{str} """ - # an empty repo has no branches: - if self.branch: - return False - else: - return True + if self.has_tag(tag): + self._git_command("tag", [ "-d", tag ]) - def list_files(self, types=['cached']): + def move_tag(self, old, new): + self._git_command("tag", [ new, old ]) + self.delete_tag(old) + + def has_tag(self, tag): """ - List files in index and working tree + Check if the repository has a tag named I{tag}. - @param types: list of types to show - @type types: list + @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} """ - all_types = [ 'cached', 'deleted', 'others', 'ignored', 'stage' - 'unmerged', 'killed', 'modified' ] - args = [ '-z' ] + out, ret = self.__git_getoutput('tag', [ '-l', tag ]) + return [ False, True ][len(out)] - 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 [] + def _build_legacy_tag(self, format, version): + """legacy version numbering""" + if ':' in version: # strip of any epochs + version = version.split(':', 1)[1] + version = version.replace('~', '.') + return format % dict(version=version) - def get_commits(self, since=None, until=None, paths=None, options=None, - first_parent=False): + def find_version(self, format, version): """ - Get commits from since to until touching paths + Check if a certain version is stored in this repo. Return it's SHA1 in + this case. For legacy tags Don't check only the tag but also the + message, since the former wasn't injective until recently. + You only need to use this funciton if you also need to check for legacy + tags. - @param since: commit to start from - @param until: last commit to get - @param paths: only list commits touching paths - @param options: list of options passed to git log - @type options: list of strings - @param first_parent: only follow first parent when seeing a - merge commit - @type first_parent: bool + @param format: tag pattern + @param version: debian version number + @return: sha1 of the version tag """ + tag = build_tag(format, version) + legacy_tag = self._build_legacy_tag(format, version) + if self.has_tag(tag): # new tags are injective + return self.rev_parse(tag) + elif self.has_tag(legacy_tag): + out, ret = self.__git_getoutput('cat-file', args=['-p', legacy_tag]) + if ret: + return None + for line in out: + if line.endswith(" %s\n" % version): + return self.rev_parse(legacy_tag) + elif line.startswith('---'): # GPG signature start + return None + return None - args = ['--pretty=format:%H'] - - if options: - args += options - - if first_parent: - args += [ "--first-parent" ] - - if since and until: - args += ['%s..%s' % (since, until)] - - if paths: - args += [ "--", paths ] + def find_tag(self, commit, pattern=None): + """ + Find the closest tag to a given commit - commits, ret = self.__git_getoutput('log', 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 ] + @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 ] - def show(self, id): - """git-show id""" - commit, ret = self.__git_getoutput('show', [ "--pretty=medium", id ]) + tag, ret = self.__git_getoutput('describe', args) if ret: - raise GitRepositoryError, "can't get %s" % id - for line in commit: - yield line + raise GitRepositoryError, "can't find tag for %s" % commit + return tag[0].strip() - def grep_log(self, regex, where=None): - args = ['--pretty=format:%H'] - args.append("--grep=%s" % regex) - if where: - args.append(where) - args.append('--') + def get_tags(self, pattern=None): + """ + List tags - 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] ] + @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] ] +#} - def get_subject(self, commit): + def force_head(self, commit, hard=False): """ - Gets the subject of a commit. + Force HEAD to a specific commit - @param commit: the commit to get the subject from - @return: the commit's subject + @param commit: commit to move HEAD to + @param hard: also update the working copy + @type hard: C{bool} """ - 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() + args = ['--quiet'] + if hard: + args += [ '--hard' ] + args += [ commit, '--' ] + self._git_command("reset", args) - def get_commit_info(self, commit): + def is_clean(self): """ - Look up data of a specific commit + Does the repository contain any uncommitted modifications? - @param commit: the commit to inspect - @return: the commit's including id, author, email, subject and body - @rtype: dict + @return: C{True} if the repository is clean, C{False} otherwise + and Git's status message + @rtype: C{tuple} """ - out, ret = self.__git_getoutput('log', - ['--pretty=format:%an%n%ae%n%s%n%b%n', - '-n1', commit]) - if ret: - raise GitRepositoryError, "Unable to retrieve log entry for %s" \ - % commit - return {'id' : commit, - 'author' : out[0].strip(), - 'email' : out[1].strip(), - 'subject' : out[2].rstrip(), - 'body' : [line.rstrip() for line in out[3:]]} + clean_msg = 'nothing to commit' + out = self.__git_getoutput('status')[0] + ret = False + for line in out: + if line.startswith('#'): + continue + if line.startswith(clean_msg): + ret = True + break + return (ret, "".join(out)) - def find_tag(self, commit, pattern=None): - "Find the closest tag to a branch's head" - args = [ '--abbrev=0' ] - if pattern: - args += [ '--match' , pattern ] - args += [ commit ] + def is_empty(self): + """ + Is the repository empty? - tag, ret = self.__git_getoutput('describe', args) - if ret: - raise GitRepositoryError, "can't find tag for %s" % commit - return tag[0].strip() + @return: True if the repositorydoesn't have any commits, + False otherwise + @rtype: C{bool} + """ + # an empty repo has no branches: + if self.branch: + return False + else: + return True def rev_parse(self, name): """ Find the SHA1 of a given name @param name: the name to look for - @type name: string + @type name: C{str} @return: the name's sha1 - @rtype: string + @rtype: C{str} """ args = [ "--quiet", "--verify", name ] sha, ret = self.__git_getoutput('rev-parse', args) @@ -655,130 +584,47 @@ class GitRepository(object): raise GitRepositoryError, "revision '%s' not found" % name return sha[0].strip() +#{ Trees + def has_treeish(self, 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] + def write_tree(self, index_file=None): """ Create a tree object from the current index @param index_file: alternate index file to write the current index to - @type index_file: string + @type index_file: C{str} @return: the new tree object's sha1 - @rtype: string + @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() - - def update_ref(self, ref, new, old=None, msg=None): - """ - Update ref I{ref} to commit I{new} if I{ref} currently points to - I{old} - - @param ref: the ref to update - @type ref: string - @param new: the new value for ref - @type new: string - @param old: the old value of ref - @type old: string - @param msg: the reason for the update - @type msg: string - """ - args = [ ref, new ] - if old: - args += [ old ] - if msg: - args = [ '-m', msg ] + args - self._git_command("update-ref", args) - - def commit_tree(self, tree, msg, parents, author={}, committer={}): - """ - Commit a tree with commit msg 'msg' and parents 'parents' - - @param tree: tree to commit - @param msg: commit message - @param parents: parents of this commit - @param author: authorship information - @type author: dict with keys 'name' and 'email' - @param committer: comitter information - @type committer: 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) - if not ret: - return sha1.strip() - else: - raise GbpError, "Failed to commit tree: %s" % stderr - - def commit_dir(self, unpack_dir, msg, branch, other_parents=None, - author={}, committer={}): - """Replace the current tip of branch 'branch' with the contents from 'unpack_dir' - @param unpack_dir: content to add - @type unpack_dir: string - @param msg: commit message to use - @type msg: string - @param branch: branch to add the contents of unpack_dir to - @type branch: string - @param other_parents: additional parents of this commit - @type other_parents: list string - @param author: author information to use for commit - @type author: dict with keys 'name', 'email', 'date' - @param committer: committer information to use for commit - @type committer: dict with keys 'name', 'email', 'date'""" - - git_index_file = os.path.join(self.path, '.git', '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: - cur = self.rev_parse(branch) - 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 + 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() +#} def get_config(self, name): """ - Gets the config value associated with name + Gets the config value associated with I{name} @param name: config value to get @return: fetched config value - @rtype: string + @rtype: C{str} """ value, ret = self.__git_getoutput('config', [ name ]) if ret: raise KeyError @@ -790,7 +636,7 @@ class GitRepository(object): config and environment variables. @return: name and email - @rtype: typle + @rtype: C{tuple} """ try: name = self.get_config("user.email") @@ -804,44 +650,14 @@ class GitRepository(object): name = os.getenv("GIT_AUTHOR_NAME", name) return (name, email) - def _get_branches(self, remote=False): - """ - Get a list of branches - - @param remote: whether to list local or remote branches - @type remote: bool - @return: local or remote branches - @rtype: 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 ] - - def get_remote_branches(self): - """ - Get a list of remote branches - - @return: remote branches - @rtype: list - """ - return self._get_branches(remote=True) - - def get_local_branches(self): - """ - Get a list of local branches - - @return: local branches - @rtype: list - """ - return self._get_branches(remote=False) +#{ Remote Repositories def get_remote_repos(self): """ Get all remote repositories @return: remote repositories - @rtype: list of strings + @rtype: C{list} of C{str} """ out = self.__git_getoutput('remote')[0] return [ remote.strip() for remote in out ] @@ -851,9 +667,9 @@ class GitRepository(object): Do we know about a remote named I{name}? @param name: name of the remote repository - @type name: string + @type name: C{str} @return: C{True} if the remote repositore is known, C{False} otherwise - @rtype: bool + @rtype: C{bool} """ if name in self.get_remote_repos(): return True @@ -865,13 +681,13 @@ class GitRepository(object): Add a tracked remote repository @param name: the name to use for the remote - @type name: string + @type name: C{str} @param url: the url to add - @type url: string + @type url: C{str} @param tags: whether to fetch tags - @type tags: bool + @type tags: C{bool} @param fetch: whether to fetch immediately from the remote side - @type fetch: bool + @type fetch: C{bool} """ args = [ "add" ] args += [ '--tags' ] if tags else [ '--no-tags'] @@ -879,14 +695,42 @@ class GitRepository(object): args += [ name, url ] self._git_command("remote", args) + def fetch(self, repo=None): + """ + Download objects and refs from another repository. + + @param repo: repository to fetch from + @type repo: C{str} + """ + if repo: + args = [repo] + + self._git_command("fetch", args) + + def pull(self, repo=None, ff_only=False): + """ + 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) + +#{ Files + def add_files(self, paths, force=False, index_file=None, work_tree=None): """ - Add files to a git repository + Add files to a the repository @param paths: list of files to add - @type paths: list or string + @type paths: list or C{str} @param force: add files even if they would be ignored by .gitignore - @type force: bool + @type force: C{bool} @param index_file: alternative index file to use @param work_tree: alternative working tree to use """ @@ -905,15 +749,14 @@ class GitRepository(object): self._git_command("add", args + paths, extra_env) - def remove_files(self, paths, verbose=False): """ Remove files from the repository @param paths: list of files to remove - @param paths: list or string + @param paths: C{list} or C{str} @param verbose: be verbose - @type verbose: bool + @type verbose: C{bool} """ if type(paths) in [type(''), type(u'')]: paths = [ paths ] @@ -921,18 +764,44 @@ class GitRepository(object): args = [] if verbose else ['--quiet'] self._git_command("rm", args + paths) + def list_files(self, types=['cached']): + """ + 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 [] + +#{ Comitting def _commit(self, msg, args=[], author_info=None): extra_env = author_info.get_author_env() if author_info else None self._git_command("commit", ['-q', '-m', msg] + args, extra_env=extra_env) - def commit_staged(self, msg, author_info=None): """ Commit currently staged files to the repository @param msg: commit message - @type msg: string + @type msg: C{str} @param author_info: authorship information @type author_info: L{GitModifier} """ @@ -942,7 +811,7 @@ class GitRepository(object): """ Commit all changes to the repository @param msg: commit message - @type msg: string + @type msg: C{str} @param author_info: authorship information @type author_info: L{GitModifier} """ @@ -953,9 +822,9 @@ class GitRepository(object): Commit the given files to the repository @param files: file or files to commit - @type files: string or list + @type files: C{str} or C{list} @param msg: commit message - @type msg: string + @type msg: C{str} @param author_info: authorship information @type author_info: L{GitModifier} """ @@ -963,6 +832,179 @@ class GitRepository(object): files = [ files ] self._commit(msg=msg, args=files, author_info=author_info) + def commit_dir(self, unpack_dir, msg, branch, other_parents=None, + author={}, committer={}): + """ + Replace the current tip of branch 'branch' with the contents from '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}, ${date} + @param committer: committer information to use for commit + @type committer: C{dict} with keys I{name}, I{email}, I{date} + """ + + git_index_file = os.path.join(self.path, '.git', '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: + cur = self.rev_parse(branch) + 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 + + def commit_tree(self, tree, msg, parents, author={}, committer={}): + """ + 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' + @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) + if not ret: + return sha1.strip() + else: + raise GbpError, "Failed to commit tree: %s" % stderr + +#{ Commit Information + + def get_commits(self, since=None, until=None, paths=None, options=None, + first_parent=False): + """ + Get commits from since to until touching paths + + @param since: commit to start from + @param until: last commit to get + @param paths: only list commits touching paths + @param options: list of 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 = ['--pretty=format:%H'] + + if options: + args += options + + if first_parent: + args += [ "--first-parent" ] + + if since and until: + args += ['%s..%s' % (since, until)] + + if paths: + args += [ "--", paths ] + + commits, ret = self.__git_getoutput('log', 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 ] + + def show(self, id): + """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 + + def grep_log(self, regex, where=None): + args = ['--pretty=format:%H'] + args.append("--grep=%s" % regex) + if where: + args.append(where) + 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] ] + + def get_subject(self, commit): + """ + 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() + + def get_commit_info(self, commit): + """ + Look up data of a specific commit + + @param commit: the commit to inspect + @return: the commit's including id, author, email, subject and body + @rtype: dict + """ + out, ret = self.__git_getoutput('log', + ['--pretty=format:%an%n%ae%n%s%n%b%n', + '-n1', commit]) + if ret: + raise GitRepositoryError, "Unable to retrieve log entry for %s" \ + % commit + return {'id' : commit, + 'author' : out[0].strip(), + 'email' : out[1].strip(), + 'subject' : out[2].rstrip(), + 'body' : [line.rstrip() for line in out[3:]]} + + +#{ Patches def format_patches(self, start, end, output_dir): """ Output the commits between start and end as patches in output_dir @@ -982,6 +1024,7 @@ class GitRepository(object): args += [ '-p', strip ] args.append(patch) self._git_command("apply", args) +#} def archive(self, format, prefix, output, treeish, **kwargs): args = [ '--format=%s' % format, '--prefix=%s' % prefix, @@ -995,39 +1038,21 @@ class GitRepository(object): Cleanup unnecessary files and optimize the local repository param auto: only cleanup if required - param auto: bool + param auto: C{bool} """ args = [ '--auto' ] if auto else [] self._git_command("gc", args) - def fetch(self, repo=None): - """ - Download objects and refs from another repository. - - param repo: repository to fetch from - type repo: string - """ - if repo: - args = [repo] - - self._git_command("fetch", args) +#{ Submodules - def pull(self, repo=None, ff_only=False): + def has_submodules(self): """ - Fetch and merge from another repository + Does the repo have any submodules? - @param repo: repository to fetch from - @type repo: string - @param ff_only: only merge if this results in a fast forward merge - @type ff_only: bool + @return: C{True} if the repository has any submodules, C{False} + otherwise + @rtype: C{bool} """ - args = [] - args += [ '--ff-only' ] if ff_only else [] - args += [ repo ] if repo else [] - self._git_command("pull", args) - - def has_submodules(self): - """Does the repo have any submodules?""" if os.path.exists('.gitmodules'): return True else: @@ -1039,6 +1064,7 @@ class GitRepository(object): Add a submodule @param repo_path: path to submodule + @type repo_path: C{str} """ self._git_command("submodule", [ "add", repo_path ]) @@ -1048,8 +1074,11 @@ class GitRepository(object): 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(): @@ -1095,15 +1124,17 @@ class GitRepository(object): recursive=recursive) return submodules +#{ Repository Creation + @classmethod def create(klass, path, description=None, bare=False): """ Create a repository at path @param path: where to create the repository - @type path: string + @type path: C{str} @return: git repository object - @rtype:GitRepository + @rtype: L{GitRepository} """ abspath = os.path.abspath(path) @@ -1127,15 +1158,15 @@ class GitRepository(object): Clone a git repository at I{remote} to I{path} @param path: where to clone the repository to - @type path: string + @type path: C{str} @param remote: URL to clone - @type remote: string + @type remote: C{str} @param depth: create a shallow clone of depth I{depth} - @type depth: int + @type depth: C{int} @param recursive: whether to clone submodules - @type recursive: bool + @type recursive: C{bool} @return: git repository object - @rtype:GitRepository + @rtype: L{GitRepository} """ abspath = os.path.abspath(path) args = [ '--depth', depth ] if depth else [] @@ -1154,6 +1185,7 @@ class GitRepository(object): except OSError, err: raise GitRepositoryError, "Cannot clone Git repository %s to %s: %s " % (remote, abspath, err[1]) return None +#} class FastImport(object): @@ -1264,7 +1296,7 @@ def tag_to_version(tag, format): def rfc822_date_to_git(rfc822_date): - """Parse a date in RFC822 format, and convert to a 'seconds tz' string. + """Parse a date in RFC822 format, and convert to a 'seconds tz' C{str}ing. >>> rfc822_date_to_git('Thu, 1 Jan 1970 00:00:01 +0000') '1 +0000'