├── .editorconfig
├── .github
└── workflows
│ └── php.yml
├── LICENSE
├── Makefile
├── README.md
├── cghooks
├── composer.json
└── src
├── Commands
├── AddCommand.php
├── Command.php
├── HookCommand.php
├── ListCommand.php
├── RemoveCommand.php
└── UpdateCommand.php
├── Hook.php
└── helpers.php
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | end_of_line = lf
6 | insert_final_newline = true
7 | trim_trailing_whitespace = true
8 | indent_style = space
9 | indent_size = 4
10 |
11 | [Makefile]
12 | indent_style = tab
13 | indent_size = 4
14 |
--------------------------------------------------------------------------------
/.github/workflows/php.yml:
--------------------------------------------------------------------------------
1 | name: PHP Composer
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 | pull_request:
7 | branches: [ master ]
8 |
9 | jobs:
10 | build:
11 |
12 | runs-on: ubuntu-latest
13 |
14 | strategy:
15 | fail-fast: false
16 | matrix:
17 | php: ['8.0', '8.1', '8.2', '8.3']
18 |
19 | steps:
20 | - name: Checkout
21 | uses: actions/checkout@v2
22 |
23 | - name: Validate composer.json
24 | run: composer validate
25 |
26 | - name: Setup PHP
27 | uses: shivammathur/setup-php@v2
28 | with:
29 | php-version: ${{ matrix.php }}
30 |
31 | - name: Install dependencies
32 | run: composer install --prefer-dist --no-progress
33 |
34 | - name: Check style
35 | run: composer run-script check-style
36 |
37 | - name: Run test suite
38 | run: composer run-script test
39 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) Ezinwa Okpoechi
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: build exec
2 |
3 | build:
4 | docker build --rm -t cghooks .
5 |
6 | exec:
7 | docker run --rm -it cghooks bash
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # composer-git-hooks
2 |
3 | [![Software License][badge-license]](LICENSE)
4 | [![Travis][badge-travis]][link-travis]
5 | [![Packagist][badge-packagist]][link-packagist]
6 | [![Download][badge-downloads]][link-packagist]
7 |
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.
9 |
10 | ## Install
11 |
12 | Add a `hooks` section to the `extra` section of your `composer.json` and add the hooks there.
13 |
14 | ```javascript
15 | {
16 | "extra": {
17 | "hooks": {
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 install"
29 | "...": "..."
30 | }
31 | }
32 | }
33 | ```
34 |
35 | Then install with
36 |
37 | ```sh
38 | composer require --dev brainmaestro/composer-git-hooks
39 | ```
40 |
41 | 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/`.
42 |
43 | Note: hooks declared in the `scripts` or `hooks` root sections of `composer.json` are no longer supported in v3.
44 |
45 | ### Global support
46 |
47 | 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.
48 |
49 | ```sh
50 | composer global require --dev brainmaestro/composer-git-hooks
51 | ```
52 |
53 | All commands have global support (besides testing the hooks. Still requires being in the directory with the `composer.json` file).
54 |
55 | ### Optional Configuration
56 |
57 | #### Stop on failure
58 |
59 | When a hook is a sequence of commands, it can be useful to stop the execution when a command fails.
60 |
61 | Specify the impacted hooks in the `stop-on-failure` config section.
62 |
63 | ```json
64 | {
65 | "extra": {
66 | "hooks": {
67 | "config": {
68 | "stop-on-failure": ["pre-push"]
69 | },
70 | "pre-push": [
71 | "php-cs-fixer fix --dry-run --stop-on-violation .",
72 | "phpunit"
73 | ],
74 | }
75 | }
76 | }
77 | ```
78 |
79 | Always be sure to run the [update command](#updating-hooks) after changing the `stop-on-failure` config section.
80 |
81 | #### Custom hooks
82 |
83 | Custom hooks can be added to the `custom-hooks` array of the `config section.
84 |
85 | ```json
86 | {
87 | "extra": {
88 | "hooks": {
89 | "config": {
90 | "custom-hooks": ["pre-flow-feature-start"]
91 | },
92 | "pre-flow-feature-start": [
93 | "echo 'Starting a new feature...'"
94 | ]
95 | }
96 | }
97 | }
98 | ```
99 |
100 | Always be sure to run the [update command](#updating-hooks) after changing the `custom-hooks` config section.
101 | Note: `config` is not valid custom hook value.
102 |
103 | #### Shortcut
104 |
105 | Add a `cghooks` script to the `scripts` section of your `composer.json` file. That way, commands can be run with `composer cghooks ${command}`. This is ideal if you would rather not edit your system path.
106 |
107 | ```json
108 | {
109 | "scripts": {
110 | "cghooks": "vendor/bin/cghooks",
111 | "...": "..."
112 | }
113 | }
114 | ```
115 |
116 | #### Composer Events
117 |
118 | Add the following events to your `composer.json` file. The `cghooks` commands will be run every time the events occur. Go to [Composer Command Events][link-composer-events] for more details about composer's event system.
119 |
120 | ```json
121 | {
122 | "scripts": {
123 | "post-install-cmd": "cghooks add --ignore-lock",
124 | "post-update-cmd": "cghooks update",
125 | "...": "..."
126 | }
127 | }
128 | ```
129 |
130 | ## Usage
131 |
132 | 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.
133 |
134 | ### Adding Hooks
135 |
136 | After installation is complete, run `cghooks add`
137 | to add all the valid git hooks that have been specified in the composer config.
138 |
139 | | Option | Description | Command |
140 | | ------------- | -------------------------------- | --------------------------- |
141 | | `no-lock` | Do not create a lock file | `cghooks add --no-lock` |
142 | | `ignore-lock` | Add the lock file to .gitignore | `cghooks add --ignore-lock` |
143 | | `force-win` | Force windows bash compatibility | `cghooks add --force-win` |
144 |
145 | The `lock` file contains a list of all added hooks.
146 |
147 | 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.
148 |
149 | ### Updating Hooks
150 |
151 | The update command which is run with `cghooks update` basically ignores the lock file and tries to add hooks from the composer file. 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.
152 |
153 | This works similarly when used with `--global` except that there is no fallback to `$COMPOSER_HOME` if no directory is provided.
154 |
155 | ### Removing Hooks
156 |
157 | Hooks can be easily removed with `cghooks remove`. This will remove all the hooks that were specified in the composer config.
158 |
159 | Hooks can also be removed by passing them as arguments. The command `cghooks remove pre-commit post-commit` which will remove the `pre-commit` and `post-commit` hooks.
160 |
161 | | Option | Description | Command |
162 | | ------- | ------------------------------------------- | ------------------------ |
163 | | `force` | Delete hooks without checking the lock file | `cghooks remove --force` |
164 |
165 | **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.
166 |
167 | This also does not have a fallback to `$COMPOSER_HOME` if no directory is provided when used with `--global`.
168 |
169 | ### Listing hooks
170 |
171 | Hooks can be listed with the `cghooks list-hooks` command. This basically checks composer config and list the hooks that actually have files.
172 |
173 | #### Common Options
174 |
175 | The following options are common to all commands.
176 |
177 | | Option | Description | Command |
178 | | -------------------- | ----------------------------------- | ----------------------------------------------- |
179 | | `git-dir` | Path to git directory | `cghooks ${command} --git-dir='/path/to/.git'` |
180 | | `lock-dir` | Path to lock file directory | `cghooks ${command} --lock-dir='/path/to/lock'` |
181 | | `global` | Runs the specified command globally | `cghooks ${command} --global` |
182 |
183 | Each command also has a flag `-v` to control verbosity for more detailed logs. Currently, only one level is supported.
184 |
185 | ### Testing Hooks
186 |
187 | Hooks can be tested with `cghooks ${hook}` before adding them. Example `cghooks pre-commit` runs the `pre-commit` hook.
188 |
189 | ## Contributing
190 |
191 | Please see [CONTRIBUTING](CONTRIBUTING.md) for details.
192 |
193 | ## Credits
194 |
195 | - [Ezinwa Okpoechi][link-author]
196 | - [All Contributors][link-contributors]
197 |
198 | ## Related
199 |
200 | - [husky][link-husky]
201 |
202 | ## License
203 |
204 | The MIT License (MIT). Please see [License File](LICENSE) for more information.
205 |
206 | [badge-downloads]: https://img.shields.io/packagist/dt/brainmaestro/composer-git-hooks.svg?style=flat-square
207 | [badge-license]: https://img.shields.io/badge/license-MIT-brightgreen.svg
208 | [badge-packagist]: https://img.shields.io/packagist/v/brainmaestro/composer-git-hooks.svg?style=flat-square
209 | [badge-stable]: https://poser.pugx.org/your-app-rocks/eloquent-uuid/v/stable
210 | [badge-travis]: https://img.shields.io/travis/BrainMaestro/composer-git-hooks.svg?style=flat-square
211 | [link-author]: https://github.com/BrainMaestro
212 | [link-composer-events]: https://getcomposer.org/doc/articles/scripts.md#command-events
213 | [link-contributors]: ../../contributors
214 | [link-husky]: https://github.com/typicode/husky
215 | [link-packagist]: https://packagist.org/packages/brainmaestro/composer-git-hooks
216 | [link-travis]: https://travis-ci.org/BrainMaestro/composer-git-hooks
217 |
--------------------------------------------------------------------------------
/cghooks:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 | add(new AddCommand());
28 | $application->add(new UpdateCommand());
29 | $application->add(new RemoveCommand());
30 | $application->add(new ListCommand());
31 |
32 | foreach (Hook::getValidHooks($dir) as $hook => $script) {
33 | $application->add(new HookCommand($hook, $script, $dir));
34 | }
35 |
36 | $application->run();
37 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "brainmaestro/composer-git-hooks",
3 | "description": "Easily manage git hooks in your composer config",
4 | "type": "library",
5 | "minimum-stability": "stable",
6 | "license": "MIT",
7 | "keywords": [
8 | "git",
9 | "hook",
10 | "composer"
11 | ],
12 | "authors": [
13 | {
14 | "name": "Ezinwa Okpoechi",
15 | "email": "brainmaestro@outlook.com"
16 | }
17 | ],
18 | "require": {
19 | "php": "^8.0",
20 | "symfony/console": "^5.0|^6.0|^7.0"
21 | },
22 | "require-dev": {
23 | "phpunit/phpunit": "^9|^10|^11",
24 | "friendsofphp/php-cs-fixer": "^3.0",
25 | "ext-json": "*"
26 | },
27 | "autoload": {
28 | "psr-4": {
29 | "BrainMaestro\\GitHooks\\": "src/"
30 | },
31 | "files": [
32 | "src/helpers.php"
33 | ]
34 | },
35 | "autoload-dev": {
36 | "psr-4": {
37 | "BrainMaestro\\GitHooks\\Tests\\": "tests/"
38 | }
39 | },
40 | "bin": [
41 | "cghooks"
42 | ],
43 | "scripts": {
44 | "test": "vendor/bin/phpunit",
45 | "post-install-cmd": "./cghooks add --ignore-lock",
46 | "post-update-cmd": "./cghooks update",
47 | "check-style": "php-cs-fixer fix --using-cache=no --diff --dry-run .",
48 | "fix-style": "php-cs-fixer fix --using-cache=no ."
49 | },
50 | "scripts-descriptions": {
51 | "test": "Run all tests.",
52 | "check-style": "Run style checks (only dry run - no fixing!).",
53 | "fix-style": "Run style checks and fix violations."
54 | },
55 | "extra": {
56 | "hooks": {
57 | "pre-commit": "composer check-style",
58 | "pre-push": [
59 | "composer test",
60 | "appver=$(grep -o -E '[0-9]+\\.[0-9]+\\.[0-9]+(-alpha\\.[0-9]+)?' cghooks)",
61 | "tag=$(git tag | tail -n 1)",
62 | "tag=${tag#v}",
63 | "if [ \"$tag\" != \"$appver\" ]; then",
64 | "echo \"The most recent tag $tag does not match the application version $appver\\n\"",
65 | "sed -i -E \"s/$appver/$tag/\" cghooks",
66 | "exit 1",
67 | "fi"
68 | ]
69 | }
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/Commands/AddCommand.php:
--------------------------------------------------------------------------------
1 | setName('add')
23 | ->setDescription('Adds git hooks from the composer config')
24 | ->setHelp('This command allows you to add git hooks')
25 | ->addOption('force', 'f', InputOption::VALUE_NONE, 'Override existing git hooks')
26 | ->addOption('no-lock', 'l', InputOption::VALUE_NONE, 'Do not create a lock file')
27 | ->addOption('ignore-lock', 'i', InputOption::VALUE_NONE, 'Add the lock file to .gitignore')
28 | ->addOption('git-dir', 'g', InputOption::VALUE_REQUIRED, 'Path to git directory')
29 | ->addOption('lock-dir', null, InputOption::VALUE_REQUIRED, 'Path to lock file directory', getcwd())
30 | ->addOption('force-win', null, InputOption::VALUE_NONE, 'Force windows bash compatibility')
31 | ->addOption('global', null, InputOption::VALUE_NONE, 'Add global git hooks')
32 | ;
33 | }
34 |
35 | protected function init(InputInterface $input)
36 | {
37 | $this->force = $input->getOption('force');
38 | $this->windows = $input->getOption('force-win') || is_windows();
39 | $this->noLock = $input->getOption('no-lock');
40 | $this->ignoreLock = $input->getOption('ignore-lock');
41 | }
42 |
43 | protected function command()
44 | {
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);
51 |
52 | foreach ($this->hooks as $hook => $contents) {
53 | $this->addHook($hook, $contents);
54 | }
55 |
56 | if (! empty($this->hooks) && count($this->upToDateHooks) === count($this->hooks)) {
57 | $this->info('All hooks are up to date');
58 | return;
59 | } elseif (! count($this->addedHooks)) {
60 | $this->error('No hooks were added. Try updating');
61 | return;
62 | }
63 |
64 | $this->addLockFile();
65 | $this->ignoreLockFile();
66 | $this->setGlobalGitHooksPath();
67 | }
68 |
69 | protected function global_dir_fallback()
70 | {
71 | if (!empty($this->dir = trim(getenv('COMPOSER_HOME')))) {
72 | $this->dir = realpath($this->dir);
73 | $this->debug("No global git hook path was provided. Falling back to COMPOSER_HOME [{$this->dir}]");
74 | }
75 | }
76 |
77 | private static function startsWithShebang($contents)
78 | {
79 | return substr_compare(trim($contents), "#!", 0) == 0;
80 | }
81 |
82 | private function addHook($hook, $contents)
83 | {
84 | $filename = "{$this->dir}/hooks/{$hook}";
85 | $exists = file_exists($filename);
86 |
87 | // On windows, the shebang needs to point to bash
88 | // See: https://github.com/BrainMaestro/composer-git-hooks/issues/7
89 | $shebang = ($this->windows ? '#!/bin/bash' : '#!/bin/sh') . PHP_EOL . PHP_EOL;
90 | $composerDir = $this->global ? $this->dir : getcwd();
91 | $contents = Hook::getHookContents($composerDir, $contents, $hook);
92 | if (AddCommand::startsWithShebang($contents)) {
93 | // Hook already starts with a shebang, do not add the default.
94 | // Many developers use bash in hooks, but sh is guaranteed to
95 | // be bash compatible. Especially in docker images with minimal
96 | // program set is sh often dash.
97 | $shebang = "";
98 | }
99 | $hookContents = $shebang . $contents . PHP_EOL;
100 |
101 | if (! $this->force && $exists) {
102 | $actualContents = file_get_contents($filename);
103 |
104 | if ($actualContents === $hookContents) {
105 | $this->debug("[{$hook}] is up to date");
106 | $this->upToDateHooks[] = $hook;
107 | return;
108 | }
109 |
110 | $this->debug("[{$hook}] already exists");
111 | return;
112 | }
113 |
114 | file_put_contents($filename, $hookContents);
115 | chmod($filename, 0755);
116 |
117 | $operation = $exists ? 'Updated' : 'Added';
118 | $this->info("{$operation} [{$hook}] hook");
119 |
120 | $this->addedHooks[] = $hook;
121 | }
122 |
123 | private function addLockFile()
124 | {
125 | if ($this->noLock) {
126 | $this->debug('Skipped creating a [' . Hook::LOCK_FILE . '] file');
127 | return;
128 | }
129 |
130 | file_put_contents($this->lockFile, json_encode($this->addedHooks));
131 | $this->debug("Created [{$this->lockFile}] file");
132 | }
133 |
134 | private function ignoreLockFile()
135 | {
136 | if ($this->noLock) {
137 | return;
138 | }
139 |
140 | if (! $this->ignoreLock) {
141 | $this->debug('Skipped adding [' . Hook::LOCK_FILE . '] to .gitignore');
142 | return;
143 | }
144 |
145 | $contents = file_get_contents('.gitignore');
146 | $return = strpos($contents, Hook::LOCK_FILE);
147 |
148 | if ($return === false) {
149 | file_put_contents('.gitignore', Hook::LOCK_FILE . PHP_EOL . PHP_EOL, FILE_APPEND);
150 | $this->debug(sprintf('Added [%s] to .gitignore', Hook::LOCK_FILE));
151 | }
152 | }
153 |
154 | private function setGlobalGitHooksPath()
155 | {
156 | if (! $this->global) {
157 | return;
158 | }
159 |
160 | $previousGlobalHookDir = global_hook_dir();
161 | $globalHookDir = trim(realpath("{$this->dir}/hooks"));
162 |
163 | if ($globalHookDir === $previousGlobalHookDir) {
164 | return;
165 | }
166 |
167 | $this->info(
168 | 'About to modify global git hook path. '
169 | . ($previousGlobalHookDir !== ''
170 | ? "Previous value was [{$previousGlobalHookDir}]"
171 | : 'There was no previous value')
172 | );
173 |
174 | $exitCode = 0;
175 | passthru("git config --global core.hooksPath {$globalHookDir}", $exitCode);
176 |
177 | if ($exitCode !== 0) {
178 | $this->error("Could not set global git hook path.\n" .
179 | " Try running this manually 'git config --global core.hooksPath {$globalHookDir}'");
180 | return;
181 | }
182 |
183 | $this->info("Global git hook path set to [{$globalHookDir}]");
184 | }
185 | }
186 |
--------------------------------------------------------------------------------
/src/Commands/Command.php:
--------------------------------------------------------------------------------
1 | output = $output;
29 | $this->gitDir = $input->getOption('git-dir') ?: git_dir();
30 | $this->lockDir = $input->getOption('lock-dir');
31 | $this->global = $input->getOption('global');
32 | $this->dir = trim(
33 | $this->global && $this->gitDir === git_dir()
34 | ? dirname(global_hook_dir())
35 | : $this->gitDir
36 | );
37 | if ($this->global) {
38 | if (empty($this->dir)) {
39 | $this->global_dir_fallback();
40 | }
41 | }
42 | if ($this->gitDir === false) {
43 | $output->writeln('Git is not initialized. Skip setting hooks...');
44 | return SymfonyCommand::SUCCESS;
45 | }
46 | $this->lockFile = (null !== $this->lockDir ? ($this->lockDir . '/') : '') . Hook::LOCK_FILE;
47 |
48 | $dir = $this->global ? $this->dir : getcwd();
49 |
50 | $this->hooks = Hook::getValidHooks($dir);
51 |
52 | $this->init($input);
53 | $this->command();
54 |
55 | return SymfonyCommand::SUCCESS;
56 | }
57 |
58 | protected function global_dir_fallback()
59 | {
60 | }
61 |
62 | protected function info($info)
63 | {
64 | $info = str_replace('[', '', $info);
65 | $info = str_replace(']', '', $info);
66 |
67 | $this->output->writeln($info);
68 | }
69 |
70 | protected function debug($debug)
71 | {
72 | $debug = str_replace('[', '', $debug);
73 | $debug = str_replace(']', '', $debug);
74 |
75 | $this->output->writeln($debug, OutputInterface::VERBOSITY_VERBOSE);
76 | }
77 |
78 | protected function error($error)
79 | {
80 | $this->output->writeln("{$error}>");
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/Commands/HookCommand.php:
--------------------------------------------------------------------------------
1 | hook = $hook;
19 | $this->contents = $contents;
20 | $this->composerDir = $composerDir;
21 | parent::__construct();
22 | }
23 |
24 | protected function configure()
25 | {
26 | $this
27 | ->setName($this->hook)
28 | ->setDescription("Test your {$this->hook} hook")
29 | ->setHelp("This command allows you to test your {$this->hook} hook")
30 | ;
31 | }
32 |
33 | protected function execute(InputInterface $input, OutputInterface $output): int
34 | {
35 | $contents = Hook::getHookContents($this->composerDir, $this->contents, $this->hook);
36 | $outputMessage = [];
37 | $returnCode = SymfonyCommand::SUCCESS;
38 | exec($contents, $outputMessage, $returnCode);
39 |
40 | $output->writeln(implode(PHP_EOL, $outputMessage));
41 |
42 | return $returnCode;
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/Commands/ListCommand.php:
--------------------------------------------------------------------------------
1 | setName('list-hooks')
14 | ->setDescription('List added hooks')
15 | ->setHelp('This command allows you to list your git hooks')
16 | ->addOption('git-dir', 'g', InputOption::VALUE_REQUIRED, 'Path to git directory')
17 | ->addOption('lock-dir', null, InputOption::VALUE_REQUIRED, 'Path to lock file directory', getcwd())
18 | ->addOption('global', null, InputOption::VALUE_NONE, 'Perform hook command globally for every git repository')
19 | ;
20 | }
21 |
22 | protected function init(InputInterface $input)
23 | {
24 | }
25 |
26 | protected function command()
27 | {
28 | foreach (array_keys($this->hooks) as $hook) {
29 | $filename = "{$this->dir}/hooks/{$hook}";
30 |
31 | if (is_file($filename)) {
32 | $this->info("[{$hook}]");
33 | }
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/Commands/RemoveCommand.php:
--------------------------------------------------------------------------------
1 | setName('remove')
19 | ->setDescription('Remove git hooks specified in the composer config')
20 | ->setHelp('This command allows you to remove git hooks')
21 | ->addArgument(
22 | 'hooks',
23 | InputArgument::IS_ARRAY,
24 | 'Hooks to be removed'
25 | )
26 | ->addOption(
27 | 'force',
28 | 'f',
29 | InputOption::VALUE_NONE,
30 | 'Delete hooks without checking the lock file'
31 | )
32 | ->addOption('git-dir', 'g', InputOption::VALUE_REQUIRED, 'Path to git directory')
33 | ->addOption('lock-dir', null, InputOption::VALUE_REQUIRED, 'Path to lock file directory', getcwd())
34 | ->addOption('global', null, InputOption::VALUE_NONE, 'Remove global git hooks')
35 | ;
36 | }
37 |
38 | protected function init(InputInterface $input)
39 | {
40 | $this->force = $input->getOption('force');
41 | $this->lockFileHooks = file_exists($this->lockFile)
42 | ? array_flip(json_decode(file_get_contents($this->lockFile)))
43 | : [];
44 | $hooks = $input->getArgument('hooks');
45 | $this->hooksToRemove = empty($hooks) ? array_keys($this->hooks) : $hooks;
46 | }
47 |
48 | protected function command()
49 | {
50 | foreach ($this->hooksToRemove as $hook) {
51 | $filename = "{$this->dir}/hooks/{$hook}";
52 |
53 | if (! array_key_exists($hook, $this->lockFileHooks) && ! $this->force) {
54 | $this->info("Skipped [{$hook}] hook - not present in lock file");
55 | $this->lockFileHooks = file_exists($this->lockFile)
56 | ? array_flip(json_decode(file_get_contents($this->lockFile)))
57 | : [];
58 | continue;
59 | }
60 |
61 | if (array_key_exists($hook, $this->hooks) && is_file($filename)) {
62 | unlink($filename);
63 | $this->info("Removed [{$hook}] hook");
64 | unset($this->lockFileHooks[$hook]);
65 | continue;
66 | }
67 |
68 | $this->error("{$hook} hook does not exist");
69 | }
70 |
71 | file_put_contents($this->lockFile, json_encode(array_keys($this->lockFileHooks)));
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/Commands/UpdateCommand.php:
--------------------------------------------------------------------------------
1 | setName('update')
14 | ->setDescription('Update git hooks specified in the composer config')
15 | ->setHelp('This command allows you to update git hooks')
16 | ->addOption('git-dir', 'g', InputOption::VALUE_REQUIRED, 'Path to git directory')
17 | ->addOption('lock-dir', null, InputOption::VALUE_REQUIRED, 'Path to lock file directory', getcwd())
18 | ->addOption('force-win', null, InputOption::VALUE_NONE, 'Force windows bash compatibility')
19 | ->addOption('global', null, InputOption::VALUE_NONE, 'Update global git hooks')
20 | ;
21 | }
22 |
23 | protected function init(InputInterface $input)
24 | {
25 | $this->windows = $input->getOption('force-win') || is_windows();
26 | $this->force = true;
27 | $this->noLock = true;
28 | $this->ignoreLock = false;
29 | }
30 |
31 | protected function command()
32 | {
33 | if (empty($this->dir)) {
34 | if ($this->global) {
35 | $this->error('You need to run the add command globally first before you try to update');
36 | } else {
37 | $this->error('You did not specify a git directory to use');
38 | }
39 |
40 | return;
41 | }
42 |
43 | parent::command();
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/Hook.php:
--------------------------------------------------------------------------------
1 | /dev/null';
53 |
54 | $gitDir = trim((string) shell_exec('git rev-parse --git-common-dir'.$errorToDevNull));
55 | if ($gitDir === '' || $gitDir === '--git-common-dir') {
56 | // the version of git does not support `--git-common-dir`
57 | // we fallback to `--git-dir` which and lose worktree support
58 | $gitDir = trim((string) shell_exec('git rev-parse --git-dir'.$errorToDevNull));
59 | }
60 |
61 | return $gitDir === '' ? false : realpath($gitDir);
62 | }
63 | }
64 |
--------------------------------------------------------------------------------