summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSebastian Thiel <byronimo@gmail.com>2015-01-12 14:55:31 +0100
committerSebastian Thiel <byronimo@gmail.com>2015-01-12 14:55:31 +0100
commite8eae18dcc360e6ab96c2291982bd4306adc01b9 (patch)
tree804cc52c1a9976a7274000d9a446bae2b4f032b3
parente7671110bc865786ffe61cf9b92bf43c03759229 (diff)
downloadgitpython-e8eae18dcc360e6ab96c2291982bd4306adc01b9.tar.gz
IndexFile.commit() now runs pre-commit and post-commit hooks.
However, it does so only on posix. The test-case will run on posix only as well. Please note that in theory, even on windows we will attempt to run hooks, even though I am not sure that this will actually work. Fixes #81
-rw-r--r--doc/source/changes.rst3
-rw-r--r--git/cmd.py2
-rw-r--r--git/exc.py20
-rw-r--r--git/index/base.py8
-rw-r--r--git/index/fun.py47
-rw-r--r--git/test/test_index.py21
6 files changed, 89 insertions, 12 deletions
diff --git a/doc/source/changes.rst b/doc/source/changes.rst
index 67384eb9..a64143c5 100644
--- a/doc/source/changes.rst
+++ b/doc/source/changes.rst
@@ -7,8 +7,9 @@ Changelog
* push/pull/fetch operations will not block anymore
* diff() can now properly detect renames, both in patch and raw format. Previously it only worked when create_patch was True.
* repo.odb.update_cache() is now called automatically after fetch and pull operations. In case you did that in your own code, you might want to remove your line to prevent a double-update that causes unnecessary IO.
-* A list of all fixed issues can be found here: https://github.com/gitpython-developers/GitPython/issues?q=milestone%3A%22v0.3.5+-+bugfixes%22+
* `Repo(path)` will not automatically search upstream anymore and find any git directory on its way up. If you need that behaviour, you can turn it back on using the new `search_parent_directories=True` flag when constructing a `Repo` object.
+* IndexFile.commit() now runs the `pre-commit` and `post-commit` hooks. Verified to be working on posix systems only.
+* A list of all fixed issues can be found here: https://github.com/gitpython-developers/GitPython/issues?q=milestone%3A%22v0.3.5+-+bugfixes%22+
0.3.4 - Python 3 Support
========================
diff --git a/git/cmd.py b/git/cmd.py
index 668d8f4a..2c76710c 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -502,7 +502,7 @@ class Git(LazyMixin):
# Prevent cmd prompt popups on windows by using a shell ... .
# See https://github.com/gitpython-developers/GitPython/pull/126
shell=sys.platform == 'win32',
- close_fds=(os.name == 'posix'), # unsupported on linux
+ close_fds=(os.name == 'posix'), # unsupported on windows
**subprocess_kwargs
)
if as_process:
diff --git a/git/exc.py b/git/exc.py
index 42191c62..d9b7cbd2 100644
--- a/git/exc.py
+++ b/git/exc.py
@@ -11,17 +11,14 @@ from git.compat import defenc
class InvalidGitRepositoryError(Exception):
-
""" Thrown if the given repository appears to have an invalid format. """
class NoSuchPathError(OSError):
-
""" Thrown if a path could not be access by the system. """
class GitCommandError(Exception):
-
""" Thrown if execution of the git command fails with non-zero status code. """
def __init__(self, command, status, stderr=None, stdout=None):
@@ -41,7 +38,6 @@ class GitCommandError(Exception):
class CheckoutError(Exception):
-
"""Thrown if a file could not be checked out from the index as it contained
changes.
@@ -71,6 +67,20 @@ class CacheError(Exception):
class UnmergedEntriesError(CacheError):
-
"""Thrown if an operation cannot proceed as there are still unmerged
entries in the cache"""
+
+
+class HookExecutionError(Exception):
+ """Thrown if a hook exits with a non-zero exit code. It provides access to the exit code and the string returned
+ via standard output"""
+
+ def __init__(self, command, status, stdout, stderr):
+ self.command = command
+ self.status = status
+ self.stdout = stdout
+ self.stderr = stderr
+
+ def __str__(self):
+ return ("'%s' hook returned with exit code %i\nstdout: '%s'\nstderr: '%s'"
+ % (self.command, self.status, self.stdout, self.stderr))
diff --git a/git/index/base.py b/git/index/base.py
index db0c3cda..7002385c 100644
--- a/git/index/base.py
+++ b/git/index/base.py
@@ -63,7 +63,8 @@ from .fun import (
aggressive_tree_merge,
write_tree_from_cache,
stat_mode_to_index_mode,
- S_IFGITLINK
+ S_IFGITLINK,
+ run_commit_hook
)
from gitdb.base import IStream
@@ -893,9 +894,12 @@ class IndexFile(LazyMixin, diff.Diffable, Serializable):
:note: If you have manually altered the .entries member of this instance,
don't forget to write() your changes to disk beforehand.
:return: Commit object representing the new commit"""
+ run_commit_hook('pre-commit', self)
tree = self.write_tree()
- return Commit.create_from_tree(self.repo, tree, message, parent_commits,
+ rval = Commit.create_from_tree(self.repo, tree, message, parent_commits,
head, author=author, committer=committer)
+ run_commit_hook('post-commit', self)
+ return rval
@classmethod
def _flush_stdin_and_wait(cls, proc, ignore_stdout=False):
diff --git a/git/index/fun.py b/git/index/fun.py
index f0dee961..38ad843b 100644
--- a/git/index/fun.py
+++ b/git/index/fun.py
@@ -13,9 +13,14 @@ from stat import (
S_IFGITLINK = S_IFLNK | S_IFDIR # a submodule
from io import BytesIO
+import os
+import subprocess
from git.util import IndexFileSHA1Writer
-from git.exc import UnmergedEntriesError
+from git.exc import (
+ UnmergedEntriesError,
+ HookExecutionError
+)
from git.objects.fun import (
tree_to_stream,
traverse_tree_recursive,
@@ -37,10 +42,46 @@ from .util import (
from gitdb.base import IStream
from gitdb.typ import str_tree_type
-from git.compat import defenc
+from git.compat import (
+ defenc,
+ force_text
+)
__all__ = ('write_cache', 'read_cache', 'write_tree_from_cache', 'entry_key',
- 'stat_mode_to_index_mode', 'S_IFGITLINK')
+ 'stat_mode_to_index_mode', 'S_IFGITLINK', 'run_commit_hook', 'hook_path')
+
+
+def hook_path(name, git_dir):
+ """:return: path to the given named hook in the given git repository directory"""
+ return os.path.join(git_dir, 'hooks', name)
+
+
+def run_commit_hook(name, index):
+ """Run the commit hook of the given name. Silently ignores hooks that do not exist.
+ :param name: name of hook, like 'pre-commit'
+ :param index: IndexFile instance
+ :raises HookExecutionError: """
+ hp = hook_path(name, index.repo.git_dir)
+ if not os.access(hp, os.X_OK):
+ return
+
+ env = os.environ.copy()
+ env['GIT_INDEX_FILE'] = index.path
+ env['GIT_EDITOR'] = ':'
+ cmd = subprocess.Popen(hp,
+ env=env,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ close_fds=(os.name == 'posix'))
+ stdout, stderr = cmd.communicate()
+ cmd.stdout.close()
+ cmd.stderr.close()
+
+ if cmd.returncode != 0:
+ stdout = force_text(stdout, defenc)
+ stderr = force_text(stderr, defenc)
+ raise HookExecutionError(hp, cmd.returncode, stdout, stderr)
+ # end handle return code
def stat_mode_to_index_mode(mode):
diff --git a/git/test/test_index.py b/git/test/test_index.py
index 021a8cd9..2be776cd 100644
--- a/git/test/test_index.py
+++ b/git/test/test_index.py
@@ -12,6 +12,7 @@ from git.test.lib import (
with_rw_repo
)
from git.util import Actor
+from git.exc import HookExecutionError
from git import (
IndexFile,
BlobFilter,
@@ -40,6 +41,7 @@ from git.index.typ import (
BaseIndexEntry,
IndexEntry
)
+from git.index.fun import hook_path
class TestIndex(TestBase):
@@ -665,6 +667,25 @@ class TestIndex(TestBase):
assert fkey not in index.entries
index.add(files, write=True)
+ if os.name != 'nt':
+ hp = hook_path('pre-commit', index.repo.git_dir)
+ with open(hp, "wt") as fp:
+ fp.write("#!/usr/bin/env sh\necho stdout; echo stderr 1>&2; exit 1")
+ # end
+ os.chmod(hp, 0o544)
+ try:
+ index.commit("This should fail")
+ except HookExecutionError as err:
+ assert err.status == 1
+ assert err.command == hp
+ assert err.stdout == 'stdout\n'
+ assert err.stderr == 'stderr\n'
+ assert str(err)
+ else:
+ raise AssertionError("Should have cought a HookExecutionError")
+ # end exception handling
+ os.remove(hp)
+ # end hook testing
nc = index.commit("2 files committed", head=False)
for fkey in keys: