Skip to content

Commit e8eae18

Browse files
committed
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
1 parent e767111 commit e8eae18

File tree

6 files changed

+89
-12
lines changed

6 files changed

+89
-12
lines changed

Diff for: doc/source/changes.rst

+2-1
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ Changelog
77
* push/pull/fetch operations will not block anymore
88
* diff() can now properly detect renames, both in patch and raw format. Previously it only worked when create_patch was True.
99
* 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.
10-
* 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+
1110
* `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.
11+
* IndexFile.commit() now runs the `pre-commit` and `post-commit` hooks. Verified to be working on posix systems only.
12+
* 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+
1213

1314
0.3.4 - Python 3 Support
1415
========================

Diff for: git/cmd.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -502,7 +502,7 @@ def execute(self, command,
502502
# Prevent cmd prompt popups on windows by using a shell ... .
503503
# See https://github.com/gitpython-developers/GitPython/pull/126
504504
shell=sys.platform == 'win32',
505-
close_fds=(os.name == 'posix'), # unsupported on linux
505+
close_fds=(os.name == 'posix'), # unsupported on windows
506506
**subprocess_kwargs
507507
)
508508
if as_process:

Diff for: git/exc.py

+15-5
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,14 @@
1111

1212

1313
class InvalidGitRepositoryError(Exception):
14-
1514
""" Thrown if the given repository appears to have an invalid format. """
1615

1716

1817
class NoSuchPathError(OSError):
19-
2018
""" Thrown if a path could not be access by the system. """
2119

2220

2321
class GitCommandError(Exception):
24-
2522
""" Thrown if execution of the git command fails with non-zero status code. """
2623

2724
def __init__(self, command, status, stderr=None, stdout=None):
@@ -41,7 +38,6 @@ def __str__(self):
4138

4239

4340
class CheckoutError(Exception):
44-
4541
"""Thrown if a file could not be checked out from the index as it contained
4642
changes.
4743
@@ -71,6 +67,20 @@ class CacheError(Exception):
7167

7268

7369
class UnmergedEntriesError(CacheError):
74-
7570
"""Thrown if an operation cannot proceed as there are still unmerged
7671
entries in the cache"""
72+
73+
74+
class HookExecutionError(Exception):
75+
"""Thrown if a hook exits with a non-zero exit code. It provides access to the exit code and the string returned
76+
via standard output"""
77+
78+
def __init__(self, command, status, stdout, stderr):
79+
self.command = command
80+
self.status = status
81+
self.stdout = stdout
82+
self.stderr = stderr
83+
84+
def __str__(self):
85+
return ("'%s' hook returned with exit code %i\nstdout: '%s'\nstderr: '%s'"
86+
% (self.command, self.status, self.stdout, self.stderr))

Diff for: git/index/base.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@
6363
aggressive_tree_merge,
6464
write_tree_from_cache,
6565
stat_mode_to_index_mode,
66-
S_IFGITLINK
66+
S_IFGITLINK,
67+
run_commit_hook
6768
)
6869

6970
from gitdb.base import IStream
@@ -893,9 +894,12 @@ def commit(self, message, parent_commits=None, head=True, author=None, committer
893894
:note: If you have manually altered the .entries member of this instance,
894895
don't forget to write() your changes to disk beforehand.
895896
:return: Commit object representing the new commit"""
897+
run_commit_hook('pre-commit', self)
896898
tree = self.write_tree()
897-
return Commit.create_from_tree(self.repo, tree, message, parent_commits,
899+
rval = Commit.create_from_tree(self.repo, tree, message, parent_commits,
898900
head, author=author, committer=committer)
901+
run_commit_hook('post-commit', self)
902+
return rval
899903

900904
@classmethod
901905
def _flush_stdin_and_wait(cls, proc, ignore_stdout=False):

Diff for: git/index/fun.py

+44-3
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,14 @@
1313
S_IFGITLINK = S_IFLNK | S_IFDIR # a submodule
1414

1515
from io import BytesIO
16+
import os
17+
import subprocess
1618

1719
from git.util import IndexFileSHA1Writer
18-
from git.exc import UnmergedEntriesError
20+
from git.exc import (
21+
UnmergedEntriesError,
22+
HookExecutionError
23+
)
1924
from git.objects.fun import (
2025
tree_to_stream,
2126
traverse_tree_recursive,
@@ -37,10 +42,46 @@
3742

3843
from gitdb.base import IStream
3944
from gitdb.typ import str_tree_type
40-
from git.compat import defenc
45+
from git.compat import (
46+
defenc,
47+
force_text
48+
)
4149

4250
__all__ = ('write_cache', 'read_cache', 'write_tree_from_cache', 'entry_key',
43-
'stat_mode_to_index_mode', 'S_IFGITLINK')
51+
'stat_mode_to_index_mode', 'S_IFGITLINK', 'run_commit_hook', 'hook_path')
52+
53+
54+
def hook_path(name, git_dir):
55+
""":return: path to the given named hook in the given git repository directory"""
56+
return os.path.join(git_dir, 'hooks', name)
57+
58+
59+
def run_commit_hook(name, index):
60+
"""Run the commit hook of the given name. Silently ignores hooks that do not exist.
61+
:param name: name of hook, like 'pre-commit'
62+
:param index: IndexFile instance
63+
:raises HookExecutionError: """
64+
hp = hook_path(name, index.repo.git_dir)
65+
if not os.access(hp, os.X_OK):
66+
return
67+
68+
env = os.environ.copy()
69+
env['GIT_INDEX_FILE'] = index.path
70+
env['GIT_EDITOR'] = ':'
71+
cmd = subprocess.Popen(hp,
72+
env=env,
73+
stdout=subprocess.PIPE,
74+
stderr=subprocess.PIPE,
75+
close_fds=(os.name == 'posix'))
76+
stdout, stderr = cmd.communicate()
77+
cmd.stdout.close()
78+
cmd.stderr.close()
79+
80+
if cmd.returncode != 0:
81+
stdout = force_text(stdout, defenc)
82+
stderr = force_text(stderr, defenc)
83+
raise HookExecutionError(hp, cmd.returncode, stdout, stderr)
84+
# end handle return code
4485

4586

4687
def stat_mode_to_index_mode(mode):

Diff for: git/test/test_index.py

+21
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
with_rw_repo
1313
)
1414
from git.util import Actor
15+
from git.exc import HookExecutionError
1516
from git import (
1617
IndexFile,
1718
BlobFilter,
@@ -40,6 +41,7 @@
4041
BaseIndexEntry,
4142
IndexEntry
4243
)
44+
from git.index.fun import hook_path
4345

4446

4547
class TestIndex(TestBase):
@@ -665,6 +667,25 @@ def make_paths():
665667
assert fkey not in index.entries
666668

667669
index.add(files, write=True)
670+
if os.name != 'nt':
671+
hp = hook_path('pre-commit', index.repo.git_dir)
672+
with open(hp, "wt") as fp:
673+
fp.write("#!/usr/bin/env sh\necho stdout; echo stderr 1>&2; exit 1")
674+
# end
675+
os.chmod(hp, 0o544)
676+
try:
677+
index.commit("This should fail")
678+
except HookExecutionError as err:
679+
assert err.status == 1
680+
assert err.command == hp
681+
assert err.stdout == 'stdout\n'
682+
assert err.stderr == 'stderr\n'
683+
assert str(err)
684+
else:
685+
raise AssertionError("Should have cought a HookExecutionError")
686+
# end exception handling
687+
os.remove(hp)
688+
# end hook testing
668689
nc = index.commit("2 files committed", head=False)
669690

670691
for fkey in keys:

0 commit comments

Comments
 (0)