Skip to content

Commit 6700011

Browse files
authored
Merge pull request #53 from BrainMaestro/add-global-hook-support
Add global hook support
2 parents 6665b7a + f29a3da commit 6700011

20 files changed

+598
-265
lines changed

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ WORKDIR /app
55
COPY ./composer.json ./composer.lock /app/
66

77
# Remove any scripts that have cghooks since it is not yet present in the container
8-
RUN sed -iE '/\.\/cghooks .*/d' composer.json
8+
RUN sed -i -E '/\.\/cghooks .*/d' composer.json
99

1010
RUN composer install
1111

Makefile

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
.PHONY: test
1+
.PHONY: build exec
22

3-
test:
3+
build:
44
docker build --rm -t cghooks .
55

66
exec:
7-
docker run --rm -it cghooks sh
7+
docker run --rm -it cghooks bash

README.md

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,33 +5,51 @@
55
[![Packagist][badge-packagist]][link-packagist]
66
[![Download][badge-downloads]][link-packagist]
77

8-
> Manage git hooks easily in your composer configuration. This package makes it easy to implement a consistent project-wide usage of git hooks. Specifying hooks in the composer file makes them available for every member of the project team. This provides a consistent environment and behavior for everyone which is great.
8+
> Manage git hooks easily in your composer configuration. This command line tool makes it easy to implement a consistent project-wide usage of git hooks. Specifying hooks in the composer file makes them available for every member of the project team. This provides a consistent environment and behavior for everyone which is great. It is also possible to use to manage git hooks globally for every repository on your computer. That way you have a reliable set of hooks crafted by yourself for every project you choose to work on.
99
1010
## Install
1111

1212
Add a `hooks` section to the `extra` section of your `composer.json` and add the hooks there. The previous way of adding hooks to the `scripts` section of your `composer.json` is still supported, but this way is cleaner if you have many scripts.
1313

14-
```json
14+
```javascript
1515
{
1616
"extra": {
1717
"hooks": {
18-
"pre-commit": "phpunit",
19-
"post-commit": "echo committed",
20-
"pre-push": ["phpunit", "echo pushing!"],
18+
"pre-commit": [
19+
"echo committing as $(git config user.name)",
20+
"php-cs-fixer fix ." // fix style
21+
],
22+
// verify commit message. ex: ABC-123: Fix everything
23+
"commit-msg": "grep -q '[A-Z]+-[0-9]+.*' $1",
24+
"pre-push": [
25+
"php-cs-fixer fix --dry-run ." // check style
26+
"phpunit"
27+
],
28+
"post-merge": "composer update"
2129
"...": "..."
2230
}
2331
}
2432
}
2533
```
2634

27-
Then install the library with
35+
Then install with
2836

2937
```sh
3038
composer require --dev brainmaestro/composer-git-hooks
3139
```
3240

3341
This installs the `cghooks` binary to your `vendor/bin` folder. If this folder is not in your path, you will need to preface every command with `vendor/bin/`.
3442

43+
### Global support
44+
45+
You can also install it globally. This feels much more natural when `cghooks` is used with the newly added support for managing global git hooks.
46+
47+
```sh
48+
composer global require --dev brainmaestro/composer-git-hooks
49+
```
50+
51+
All commands have global support (besides testing the hooks. Still requires being in the directory with the `composer.json` file).
52+
3553
### Optional Configuration
3654

3755
#### Shortcut
@@ -63,7 +81,7 @@ Add the following events to your `composer.json` file. The `cghooks` commands wi
6381

6482
## Usage
6583

66-
All the following commands have to be run in the same folder as your `composer.json` file.
84+
All the following commands have to be run either in the same folder as your `composer.json` file or by specifying the `--git-dir` option to point to a folder with a `composer.json` file.
6785

6886
### Adding Hooks
6987

@@ -78,10 +96,14 @@ to add all the valid git hooks that have been specified in the composer config.
7896

7997
The `lock` file contains a list of all added hooks.
8098

99+
If the `--global` flag is used, the hooks will be added globally, and the global git config will also be modified. If no directory is provided, there is a fallback to the current `core.hooksPath` in the global config. If that value is not set, it defaults to `$COMPOSER_HOME` (this specific fallback only happens for the `add` command). It will fail with an error if there is still no path after the fallbacks.
100+
81101
### Updating Hooks
82102

83103
The update command which is run with `cghooks update` basically ignores the lock file and tries to add hooks from the composer lock. This is similar to what the `--force` option for the `add` command did. This command is useful if the hooks in the `composer.json` file have changed since the first time the hooks were added.
84104

105+
This works similarly when used with `--global` except that there is no fallback to `$COMPOSER_HOME` if no directory is provided.
106+
85107
### Removing Hooks
86108

87109
Hooks can be easily removed with `cghooks remove`. This will remove all the hooks that were specified in the composer config.
@@ -94,6 +116,8 @@ Hooks can also be removed by passing them as arguments. The command `cghooks rem
94116

95117
**CAREFUL**: If the lock file was tampered with or the force option was used, hooks that already existed before using this package, but were specified in the composer scripts config will be removed as well. That is, if you had a previous `pre-commit` hook, but your current composer config also has a `pre-commit` hook, this option will cause the command to remove your initial hook.
96118

119+
This also does not have a fallback to `$COMPOSER_HOME` if no directory is provided when used with `--global`.
120+
97121
### Listing hooks
98122

99123
Hooks can be listed with the `cghooks list-hooks` command. This basically checks composer config and list the hooks that actually have files.
@@ -102,9 +126,12 @@ Hooks can be listed with the `cghooks list-hooks` command. This basically checks
102126

103127
The following options are common to all commands.
104128

105-
| Option | Description | Command |
106-
| --------- | --------------------- | ---------------------------------------------- |
107-
| `git-dir` | Path to git directory | `cghooks ${command} --git-dir='/path/to/.git'` |
129+
| Option | Description | Command |
130+
| --------- | ----------------------------------- | ---------------------------------------------- |
131+
| `git-dir` | Path to git directory | `cghooks ${command} --git-dir='/path/to/.git'` |
132+
| `global` | Runs the specified command globally | `cghooks ${command} --global` |
133+
134+
Each command also has a flag `-v` to control verbosity for more detailed logs. Currently, only one level is supported.
108135

109136
### Testing Hooks
110137

cghooks

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,14 @@ use BrainMaestro\GitHooks\Commands\ListCommand;
2222
use BrainMaestro\GitHooks\Commands\HookCommand;
2323
use Symfony\Component\Console\Application;
2424

25-
$application = new Application('Composer Git Hooks', '2.4.5');
25+
$application = new Application('Composer Git Hooks', '2.5.0');
2626

27-
$hooks = Hook::getValidHooks($dir);
28-
$application->add(new AddCommand($hooks));
29-
$application->add(new UpdateCommand($hooks));
30-
$application->add(new RemoveCommand($hooks));
31-
$application->add(new ListCommand($hooks));
27+
$application->add(new AddCommand());
28+
$application->add(new UpdateCommand());
29+
$application->add(new RemoveCommand());
30+
$application->add(new ListCommand());
3231

33-
foreach ($hooks as $hook => $script) {
32+
foreach (Hook::getValidHooks($dir) as $hook => $script) {
3433
$application->add(new HookCommand($hook, $script));
3534
}
3635

composer.json

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,16 @@
4949
"extra": {
5050
"hooks": {
5151
"pre-commit": "composer check-style",
52-
"pre-push": "composer test"
52+
"pre-push": [
53+
"composer test",
54+
"appver=$(grep -o -P '\\d.\\d.\\d' cghooks)",
55+
"tag=$(git tag --sort=-v:refname | head -n 1 | tr -d v)",
56+
"if [ \"$tag\" != \"$appver\" ]; then",
57+
"echo \"The most recent tag v$tag does not match the application version $appver\n\"",
58+
"sed -i -E \"s/$appver/$tag/\" cghooks",
59+
"exit 1",
60+
"fi"
61+
]
5362
}
5463
}
5564
}

src/Commands/AddCommand.php

Lines changed: 64 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,13 @@
1010

1111
class AddCommand extends Command
1212
{
13-
private $force;
14-
private $windows;
15-
private $noLock;
16-
private $ignoreLock;
1713
private $addedHooks = [];
1814

15+
protected $force;
16+
protected $noLock;
17+
protected $windows;
18+
protected $ignoreLock;
19+
1920
protected function configure()
2021
{
2122
$this
@@ -27,6 +28,7 @@ protected function configure()
2728
->addOption('ignore-lock', 'i', InputOption::VALUE_NONE, 'Add the lock file to .gitignore')
2829
->addOption('git-dir', 'g', InputOption::VALUE_REQUIRED, 'Path to git directory', '.git')
2930
->addOption('force-win', null, InputOption::VALUE_NONE, 'Force windows bash compatibility')
31+
->addOption('global', null, InputOption::VALUE_NONE, 'Add global git hooks')
3032
;
3133
}
3234

@@ -40,7 +42,12 @@ protected function init($input)
4042

4143
protected function command()
4244
{
43-
create_hooks_dir($this->gitDir);
45+
if (empty($this->dir)) {
46+
$this->error('You did not specify a git directory to use');
47+
return;
48+
}
49+
50+
create_hooks_dir($this->dir);
4451

4552
foreach ($this->hooks as $hook => $contents) {
4653
$this->addHook($hook, $contents);
@@ -53,11 +60,20 @@ protected function command()
5360

5461
$this->addLockFile();
5562
$this->ignoreLockFile();
63+
$this->setGlobalGitHooksPath();
64+
}
65+
66+
protected function global_dir_fallback()
67+
{
68+
if (!empty($this->dir = trim(getenv('COMPOSER_HOME')))) {
69+
$this->dir = realpath($this->dir);
70+
$this->debug("No global git hook path was provided. Falling back to COMPOSER_HOME [{$this->dir}]");
71+
}
5672
}
5773

5874
private function addHook($hook, $contents)
5975
{
60-
$filename = "{$this->gitDir}/hooks/{$hook}";
76+
$filename = "{$this->dir}/hooks/{$hook}";
6177
$exists = file_exists($filename);
6278

6379
// On windows, the shebang needs to point to bash
@@ -66,28 +82,28 @@ private function addHook($hook, $contents)
6682
$contents = is_array($contents) ? implode(PHP_EOL, $contents) : $contents;
6783

6884
if (! $this->force && $exists) {
69-
$this->comment("{$hook} already exists");
85+
$this->debug("[{$hook}] already exists");
7086
return;
7187
}
7288

73-
file_put_contents($filename, $shebang . $contents);
89+
file_put_contents($filename, $shebang . $contents . PHP_EOL);
7490
chmod($filename, 0755);
7591

76-
$operation = $exists ? 'Overwrote' : 'Added';
77-
$this->log("{$operation} <info>{$hook}</info> hook");
92+
$operation = $exists ? 'Updated' : 'Added';
93+
$this->info("{$operation} [{$hook}] hook");
7894

7995
$this->addedHooks[] = $hook;
8096
}
8197

8298
private function addLockFile()
8399
{
84100
if ($this->noLock) {
85-
$this->comment('Skipped creating a '. Hook::LOCK_FILE . ' file');
101+
$this->debug("Skipped creating a [{$this->lockFile}] file");
86102
return;
87103
}
88104

89105
file_put_contents(Hook::LOCK_FILE, json_encode($this->addedHooks));
90-
$this->comment('Created ' . Hook::LOCK_FILE . ' file');
106+
$this->debug("Created [{$this->lockFile}] file");
91107
}
92108

93109
private function ignoreLockFile()
@@ -97,16 +113,48 @@ private function ignoreLockFile()
97113
}
98114

99115
if (! $this->ignoreLock) {
100-
$this->comment('Skipped adding '. Hook::LOCK_FILE . ' to .gitignore');
116+
$this->debug("Skipped adding [{$this->lockFile}] to .gitignore");
101117
return;
102118
}
103119

104120
$contents = file_get_contents('.gitignore');
105-
$return = strpos($contents, Hook::LOCK_FILE);
121+
$return = strpos($contents, $this->lockFile);
106122

107123
if ($return === false) {
108-
file_put_contents('.gitignore', Hook::LOCK_FILE . PHP_EOL, FILE_APPEND);
109-
$this->comment('Added ' . Hook::LOCK_FILE . ' to .gitignore');
124+
file_put_contents('.gitignore', $this->lockFile . PHP_EOL, FILE_APPEND);
125+
$this->debug("Added [{$this->lockFile}] to .gitignore");
110126
}
111127
}
128+
129+
private function setGlobalGitHooksPath()
130+
{
131+
if (! $this->global) {
132+
return;
133+
}
134+
135+
$previousGlobalHookDir = global_hook_dir();
136+
$globalHookDir = trim(realpath("{$this->dir}/hooks"));
137+
138+
if ($globalHookDir === $previousGlobalHookDir) {
139+
return;
140+
}
141+
142+
$this->info(
143+
'About to modify global git hook path. '
144+
. ($previousGlobalHookDir !== ''
145+
? "Previous value was [{$previousGlobalHookDir}]"
146+
: 'There was no previous value')
147+
);
148+
149+
$exitCode = 0;
150+
passthru("git config --global core.hooksPath {$globalHookDir}", $exitCode);
151+
152+
if ($exitCode !== 0) {
153+
$this->error("Could not set global git hook path.\n" .
154+
" Try running this manually 'git config --global core.hooksPath {$globalHookDir}'");
155+
return;
156+
}
157+
158+
$this->info("Global git hook path set to [{$globalHookDir}]");
159+
}
112160
}

src/Commands/Command.php

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace BrainMaestro\GitHooks\Commands;
44

5+
use BrainMaestro\GitHooks\Hook;
56
use Symfony\Component\Console\Command\Command as SymfonyCommand;
67
use Symfony\Component\Console\Input\InputInterface;
78
use Symfony\Component\Console\Input\InputOption;
@@ -11,14 +12,11 @@ abstract class Command extends SymfonyCommand
1112
{
1213
private $output;
1314

15+
protected $dir;
1416
protected $hooks;
1517
protected $gitDir;
16-
17-
public function __construct($hooks)
18-
{
19-
$this->hooks = $hooks;
20-
parent::__construct();
21-
}
18+
protected $global;
19+
protected $lockFile;
2220

2321
abstract protected function init($input);
2422
abstract protected function command();
@@ -27,22 +25,50 @@ final protected function execute(InputInterface $input, OutputInterface $output)
2725
{
2826
$this->output = $output;
2927
$this->gitDir = $input->getOption('git-dir');
28+
$this->global = $input->getOption('global');
29+
$this->lockFile = Hook::LOCK_FILE;
30+
$this->dir = trim(
31+
$this->global && $this->gitDir === '.git'
32+
? dirname(global_hook_dir())
33+
: $this->gitDir
34+
);
35+
36+
if ($this->global) {
37+
if (empty($this->dir)) {
38+
$this->global_dir_fallback();
39+
}
40+
41+
$this->lockFile = $this->dir . '/' . Hook::LOCK_FILE;
42+
}
43+
44+
$this->hooks = Hook::getValidHooks($this->global ? $this->dir : getcwd());
45+
3046
$this->init($input);
3147
$this->command();
3248
}
3349

34-
protected function log($log)
50+
protected function global_dir_fallback()
51+
{
52+
}
53+
54+
protected function info($info)
3555
{
36-
$this->output->writeln($log);
56+
$info = str_replace('[', '<info>', $info);
57+
$info = str_replace(']', '</info>', $info);
58+
59+
$this->output->writeln($info);
3760
}
3861

39-
protected function comment($comment)
62+
protected function debug($debug)
4063
{
41-
$this->output->writeln("<comment>{$comment}</comment>");
64+
$debug = str_replace('[', '<comment>', $debug);
65+
$debug = str_replace(']', '</comment>', $debug);
66+
67+
$this->output->writeln($debug, OutputInterface::VERBOSITY_VERBOSE);
4268
}
4369

4470
protected function error($error)
4571
{
46-
$this->output->writeln("<error>{$error}</error>");
72+
$this->output->writeln("<fg=red>{$error}</>");
4773
}
4874
}

0 commit comments

Comments
 (0)