X-Git-Url: https://git.distorted.org.uk/~mdw/stgit/blobdiff_plain/117ed129a470c5cdae3addb43ff6f2d04f3e5409..1de97e5f5e62a46d69515052a860138e417f149b:/stgit/lib/git.py diff --git a/stgit/lib/git.py b/stgit/lib/git.py index ac3e9d0..9c530c7 100644 --- a/stgit/lib/git.py +++ b/stgit/lib/git.py @@ -511,6 +511,14 @@ class RunWithEnvCwd(RunWithEnv): @type env: dict @param env: Extra environment""" return RunWithEnv.run(self, args, env).cwd(self.cwd) + def run_in_cwd(self, args): + """Run the given command with an environment given by self.env and + self.env_in_cwd, without changing the current working + directory. + + @type args: list of strings + @param args: Command and argument vector""" + return RunWithEnv.run(self, args, self.env_in_cwd) class Repository(RunWithEnv): """Represents a git repository.""" @@ -637,12 +645,41 @@ class Repository(RunWithEnv): assert isinstance(t2, Tree) return self.run(['git', 'diff-tree', '-p'] + list(diff_opts) + [t1.sha1, t2.sha1]).raw_output() + def diff_tree_files(self, t1, t2): + """Given two L{Tree}s C{t1} and C{t2}, iterate over all files for + which they differ. For each file, yield a tuple with the old + file mode, the new file mode, the old blob, the new blob, the + status, the old filename, and the new filename. Except in case + of a copy or a rename, the old and new filenames are + identical.""" + assert isinstance(t1, Tree) + assert isinstance(t2, Tree) + i = iter(self.run(['git', 'diff-tree', '-r', '-z'] + [t1.sha1, t2.sha1] + ).raw_output().split('\0')) + try: + while True: + x = i.next() + if not x: + continue + omode, nmode, osha1, nsha1, status = x[1:].split(' ') + fn1 = i.next() + if status[0] in ['C', 'R']: + fn2 = i.next() + else: + fn2 = fn1 + yield (omode, nmode, self.get_blob(osha1), + self.get_blob(nsha1), status, fn1, fn2) + except StopIteration: + pass class MergeException(exception.StgException): """Exception raised when a merge fails for some reason.""" class MergeConflictException(MergeException): """Exception raised when a merge fails due to conflicts.""" + def __init__(self, conflicts): + MergeException.__init__(self) + self.conflicts = conflicts class Index(RunWithEnv): """Represents a git index file.""" @@ -660,15 +697,20 @@ class Index(RunWithEnv): def read_tree(self, tree): self.run(['git', 'read-tree', tree.sha1]).no_output() def write_tree(self): + """Write the index contents to the repository. + @return: The resulting L{Tree} + @rtype: L{Tree}""" try: return self.__repository.get_tree( self.run(['git', 'write-tree']).discard_stderr( ).output_one_line()) except run.RunException: raise MergeException('Conflicting merge') - def is_clean(self): + def is_clean(self, tree): + """Check whether the index is clean relative to the given treeish.""" try: - self.run(['git', 'update-index', '--refresh']).discard_output() + self.run(['git', 'diff-index', '--quiet', '--cached', tree.sha1] + ).discard_output() except run.RunException: return False else: @@ -747,6 +789,7 @@ class Worktree(object): def __init__(self, directory): self.__directory = directory env = property(lambda self: { 'GIT_WORK_TREE': '.' }) + env_in_cwd = property(lambda self: { 'GIT_WORK_TREE': self.directory }) directory = property(lambda self: self.__directory) class CheckoutException(exception.StgException): @@ -763,7 +806,12 @@ class IndexAndWorktree(RunWithEnvCwd): index = property(lambda self: self.__index) env = property(lambda self: utils.add_dict(self.__index.env, self.__worktree.env)) + env_in_cwd = property(lambda self: self.__worktree.env_in_cwd) cwd = property(lambda self: self.__worktree.directory) + def checkout_hard(self, tree): + assert isinstance(tree, Tree) + self.run(['git', 'read-tree', '--reset', '-u', tree.sha1] + ).discard_output() def checkout(self, old_tree, new_tree): # TODO: Optionally do a 3-way instead of doing nothing when we # have a problem. Or maybe we should stash changes in a patch? @@ -776,7 +824,7 @@ class IndexAndWorktree(RunWithEnvCwd): ).discard_output() except run.RunException: raise CheckoutException('Index/workdir dirty') - def merge(self, base, ours, theirs): + def merge(self, base, ours, theirs, interactive = False): assert isinstance(base, Tree) assert isinstance(ours, Tree) assert isinstance(theirs, Tree) @@ -786,17 +834,53 @@ class IndexAndWorktree(RunWithEnvCwd): env = { 'GITHEAD_%s' % base.sha1: 'ancestor', 'GITHEAD_%s' % ours.sha1: 'current', 'GITHEAD_%s' % theirs.sha1: 'patched'}) - r.discard_output() + r.returns([0, 1]) + output = r.output_lines() + if r.exitcode: + # There were conflicts + if interactive: + self.mergetool() + else: + conflicts = [l for l in output if l.startswith('CONFLICT')] + raise MergeConflictException(conflicts) except run.RunException, e: - if r.exitcode == 1: - raise MergeConflictException() - else: - raise MergeException('Index/worktree dirty') - def changed_files(self): - return self.run(['git', 'diff-files', '--name-only']).output_lines() - def update_index(self, files): - self.run(['git', 'update-index', '--remove', '-z', '--stdin'] - ).input_nulterm(files).discard_output() + raise MergeException('Index/worktree dirty') + def mergetool(self, files = ()): + """Invoke 'git mergetool' on the current IndexAndWorktree to resolve + any outstanding conflicts. If 'not files', all the files in an + unmerged state will be processed.""" + self.run(['git', 'mergetool'] + list(files)).returns([0, 1]).run() + # check for unmerged entries (prepend 'CONFLICT ' for consistency with + # merge()) + conflicts = ['CONFLICT ' + f for f in self.index.conflicts()] + if conflicts: + raise MergeConflictException(conflicts) + def changed_files(self, tree, pathlimits = []): + """Return the set of files in the worktree that have changed with + respect to C{tree}. The listing is optionally restricted to + those files that match any of the path limiters given. + + The path limiters are relative to the current working + directory; the returned file names are relative to the + repository root.""" + assert isinstance(tree, Tree) + return set(self.run_in_cwd( + ['git', 'diff-index', tree.sha1, '--name-only', '-z', '--'] + + list(pathlimits)).raw_output().split('\0')[:-1]) + def update_index(self, paths): + """Update the index with files from the worktree. C{paths} is an + iterable of paths relative to the root of the repository.""" + cmd = ['git', 'update-index', '--remove'] + self.run(cmd + ['-z', '--stdin'] + ).input_nulterm(paths).discard_output() + def worktree_clean(self): + """Check whether the worktree is clean relative to index.""" + try: + self.run(['git', 'update-index', '--refresh']).discard_output() + except run.RunException: + return False + else: + return True class Branch(object): """Represents a Git branch.""" @@ -844,3 +928,8 @@ class Branch(object): repository.run(['git', 'branch', create_at.sha1]).discard_output() return cls(repository, name) + +def diffstat(diff): + """Return the diffstat of the supplied diff.""" + return run.Run('git', 'apply', '--stat', '--summary' + ).raw_input(diff).raw_output()