vagrant-git-hooks 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +19 -0
- data/LICENSE.md +21 -0
- data/README.md +84 -0
- data/lib/vagrant-git-hooks/VERSION +1 -0
- data/lib/vagrant-git-hooks/command.rb +227 -0
- data/lib/vagrant-git-hooks/config.rb +91 -0
- data/lib/vagrant-git-hooks/helpers.rb +133 -0
- data/lib/vagrant-git-hooks/hook_builder.rb +69 -0
- data/lib/vagrant-git-hooks/plugin.rb +112 -0
- data/lib/vagrant-git-hooks/service.rb +90 -0
- data/lib/vagrant-git-hooks/util/git.rb +58 -0
- data/lib/vagrant-git-hooks/util/installer.rb +159 -0
- data/lib/vagrant-git-hooks/util/manifest.rb +61 -0
- data/lib/vagrant-git-hooks/util/verbose.rb +22 -0
- data/lib/vagrant-git-hooks/version.rb +12 -0
- data/lib/vagrant-git-hooks.rb +4 -0
- data/locales/en.yml +76 -0
- data/locales/fr.yml +75 -0
- metadata +113 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 9bcb94570bf05bac8dccbbf2014d27040d72ce2c5c9f6488c4792780fe52f234
|
|
4
|
+
data.tar.gz: fe129e89c75782fec7817804b22cdf15c984281874c8ec27fca27d39f6c8378a
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: cdc518ee7647648c5f13ac7e902dedeffd843af6214d71d2674644b12ef0b9624a924d2b00747b2ec9c9a90a0f3ea38ba5bbcef1ad2c62e03024df997cc1ebd2
|
|
7
|
+
data.tar.gz: 2ad7b7a2f3f43053530e1461ea9c87b33179a051a1ba9ffb6d50687d7d25f1d4993abeaea3e578f8e231cb89637104cf87176e075e05194271a6de8b4a1f885a
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.2.0](https://github.com/julienpoirou/vagrant-git-hooks/compare/v0.1.0...v0.2.0) (2026-07-05)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Fonctionnalités ✨
|
|
7
|
+
|
|
8
|
+
* Add git hooks plugin implementation and RubyDoc publishing ([59218e3](https://github.com/julienpoirou/vagrant-git-hooks/commit/59218e36a20b0a439feaf2347bb071dfeb82585f))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Corrections 🐛
|
|
12
|
+
|
|
13
|
+
* Wire the workflow_dispatch tag input and make Publish release-safe ([f3b1a84](https://github.com/julienpoirou/vagrant-git-hooks/commit/f3b1a843c1abccef00814e6349e76d8f306455c3))
|
|
14
|
+
|
|
15
|
+
## Changelog
|
|
16
|
+
|
|
17
|
+
All notable changes to this project are documented in this file. The format is
|
|
18
|
+
managed automatically by [release-please](https://github.com/googleapis/release-please)
|
|
19
|
+
from [Conventional Commits](https://www.conventionalcommits.org/).
|
data/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Julien Poirou
|
|
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.
|
data/README.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# vagrant-git-hooks
|
|
2
|
+
|
|
3
|
+
🇬🇧 English · [🇫🇷 Français](./README.fr.md)
|
|
4
|
+
|
|
5
|
+
Manage your project's **git hooks from the Vagrantfile** — a [husky](https://github.com/typicode/husky)
|
|
6
|
+
replacement that needs **no Node/npm**, only Vagrant. Stop adding a `package.json`
|
|
7
|
+
and `node_modules` to a Go/Ruby/Python repo just to lint on commit.
|
|
8
|
+
|
|
9
|
+
## Why
|
|
10
|
+
|
|
11
|
+
Husky is great, but it *is* a Node package. `vagrant-git-hooks` moves hook
|
|
12
|
+
management into the tool you already use to describe your environment — Vagrant —
|
|
13
|
+
so the only dependency is Vagrant itself. Hooks are plain POSIX shell and run on
|
|
14
|
+
the host, exactly like husky's.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
vagrant plugin install vagrant-git-hooks
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Configure
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
Vagrant.configure("2") do |config|
|
|
26
|
+
config.git_hooks.hooks = {
|
|
27
|
+
"pre-commit" => "rubocop --parallel",
|
|
28
|
+
"commit-msg" => "commitlint --edit $1",
|
|
29
|
+
"pre-push" => ["bundle exec rake spec", "echo pushed"]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
# Optional: adopt one-file-per-hook scripts from a versioned directory.
|
|
33
|
+
config.git_hooks.hooks_source = "scripts/githooks"
|
|
34
|
+
|
|
35
|
+
# Defaults:
|
|
36
|
+
# config.git_hooks.install_on_up = true # install on `vagrant up`/provision
|
|
37
|
+
# config.git_hooks.remove_on_destroy = false # keep hooks on `vagrant destroy`
|
|
38
|
+
# config.git_hooks.fail_on_error = true # inject `set -e` (a failing hook blocks the commit)
|
|
39
|
+
end
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
A value can be a `String` (single line) or an `Array<String>` (several lines).
|
|
43
|
+
Your `$1` / `"$@"` are passed through — git feeds the hook its own arguments.
|
|
44
|
+
|
|
45
|
+
On `vagrant up` the plugin writes each hook into the repository hooks directory
|
|
46
|
+
(resolved via `git rev-parse --git-path hooks`, so `core.hooksPath` and worktrees
|
|
47
|
+
are honored) with a managed header:
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
#!/usr/bin/env sh
|
|
51
|
+
# >>> vagrant-git-hooks (managed) >>>
|
|
52
|
+
# Managed by Vagrant - do not edit manually (hook: pre-commit)
|
|
53
|
+
set -e
|
|
54
|
+
rubocop --parallel
|
|
55
|
+
# <<< vagrant-git-hooks (managed) <<<
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
If a non-managed hook already exists, it is backed up to `<hook>.bak` before being
|
|
59
|
+
replaced, and restored on uninstall.
|
|
60
|
+
|
|
61
|
+
## CLI
|
|
62
|
+
|
|
63
|
+
| Command | Description |
|
|
64
|
+
|---|---|
|
|
65
|
+
| `vagrant hooks install` | Install configured hooks (`--dry-run`, `--json`) |
|
|
66
|
+
| `vagrant hooks uninstall` | Remove managed hooks, restore backups (`--yes`) |
|
|
67
|
+
| `vagrant hooks list` | Show each hook's state: `installed` / `missing` / `modified` / `foreign` |
|
|
68
|
+
| `vagrant hooks run <hook> [args]` | Run a configured hook on the host (test without committing) |
|
|
69
|
+
| `vagrant hooks version` | Print the plugin version |
|
|
70
|
+
| `vagrant hooks help [topic]` | Help |
|
|
71
|
+
|
|
72
|
+
Global flags: `--json`, `--no-emoji`, `--lang en|fr`.
|
|
73
|
+
|
|
74
|
+
## husky → vagrant-git-hooks
|
|
75
|
+
|
|
76
|
+
| husky | vagrant-git-hooks |
|
|
77
|
+
|---|---|
|
|
78
|
+
| `npx husky init` + `.husky/<hook>` files | `config.git_hooks.hooks = { … }` in the Vagrantfile |
|
|
79
|
+
| installed on `npm install` (Node) | installed on `vagrant up` (Vagrant) |
|
|
80
|
+
| `package.json` + `node_modules` | nothing extra — just Vagrant |
|
|
81
|
+
|
|
82
|
+
## License
|
|
83
|
+
|
|
84
|
+
MIT — see [LICENSE.md](./LICENSE.md).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.2.0
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
require "json"
|
|
5
|
+
require "tempfile"
|
|
6
|
+
|
|
7
|
+
require_relative "helpers"
|
|
8
|
+
require_relative "config"
|
|
9
|
+
require_relative "service"
|
|
10
|
+
require_relative "hook_builder"
|
|
11
|
+
require_relative "util/git"
|
|
12
|
+
require_relative "util/verbose"
|
|
13
|
+
require_relative "version"
|
|
14
|
+
|
|
15
|
+
module VagrantGitHooks
|
|
16
|
+
BASE_CMD = if defined?(Vagrant) && Vagrant.respond_to?(:plugin)
|
|
17
|
+
Vagrant.plugin("2", :command)
|
|
18
|
+
else
|
|
19
|
+
Class.new do
|
|
20
|
+
def initialize(argv = [], env = {})
|
|
21
|
+
@argv = argv || []
|
|
22
|
+
@env = env || {}
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def parse_options(parser)
|
|
26
|
+
parser.permute!(@argv)
|
|
27
|
+
@argv
|
|
28
|
+
rescue OptionParser::InvalidOption
|
|
29
|
+
nil
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
class Command < BASE_CMD
|
|
35
|
+
def execute
|
|
36
|
+
UiHelpers.setup_i18n!
|
|
37
|
+
@opts = { json: false, no_emoji: false, lang: nil, yes: false, dry_run: false }
|
|
38
|
+
|
|
39
|
+
argv = parse_options(build_parser)
|
|
40
|
+
return 0 unless argv
|
|
41
|
+
|
|
42
|
+
apply_locale_and_flags
|
|
43
|
+
dispatch(argv.shift, argv)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def dispatch(sub, argv)
|
|
49
|
+
case sub
|
|
50
|
+
when "version" then cmd_version
|
|
51
|
+
when nil, "", "help" then UiHelpers.print_topic_help(argv.shift)
|
|
52
|
+
0
|
|
53
|
+
when "install" then with_project { |root, cfg| do_install(root, cfg) }
|
|
54
|
+
when "uninstall", "remove" then with_project { |root, cfg| do_uninstall(root, cfg) }
|
|
55
|
+
when "list", "status" then with_project { |root, cfg| do_list(root, cfg) }
|
|
56
|
+
when "run" then with_project { |root, cfg| do_run(root, cfg, argv) }
|
|
57
|
+
else
|
|
58
|
+
err("#{UiHelpers.e(:error, no_emoji: @opts[:no_emoji])} #{UiHelpers.t("errors.unknown_command")}")
|
|
59
|
+
1
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def build_parser
|
|
64
|
+
OptionParser.new do |o|
|
|
65
|
+
o.banner = UiHelpers.t("usage.banner")
|
|
66
|
+
o.on("--json", "Machine-readable JSON output") { @opts[:json] = true }
|
|
67
|
+
o.on("--no-emoji", "Disable emoji in output") { @opts[:no_emoji] = true }
|
|
68
|
+
o.on("--lang LANG", "Force language (en|fr)") { |v| @opts[:lang] = v }
|
|
69
|
+
o.on("--dry-run", "Show what would change") { @opts[:dry_run] = true }
|
|
70
|
+
o.on("-y", "--yes", "Auto-confirm") { @opts[:yes] = true }
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def apply_locale_and_flags
|
|
75
|
+
ENV["VGH_NO_EMOJI"] = "1" if @opts[:no_emoji]
|
|
76
|
+
return unless @opts[:lang]
|
|
77
|
+
|
|
78
|
+
begin
|
|
79
|
+
UiHelpers.set_locale!(@opts[:lang])
|
|
80
|
+
rescue UiHelpers::UnsupportedLocaleError
|
|
81
|
+
UiHelpers.set_locale!("en")
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def with_project
|
|
86
|
+
root = @env.respond_to?(:root_path) ? @env.root_path : nil
|
|
87
|
+
unless root
|
|
88
|
+
err("#{UiHelpers.e(:error, no_emoji: @opts[:no_emoji])} #{UiHelpers.t("errors.no_machine")}")
|
|
89
|
+
return 1
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
cfg = @env.vagrantfile.config.git_hooks
|
|
93
|
+
resolve_cli_locale!(cfg)
|
|
94
|
+
yield root.to_s, cfg
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def resolve_cli_locale!(cfg)
|
|
98
|
+
return if @opts[:lang]
|
|
99
|
+
return unless ENV["VGH_LANG"].to_s.strip.empty?
|
|
100
|
+
return unless cfg.respond_to?(:locale) && cfg.locale
|
|
101
|
+
|
|
102
|
+
begin
|
|
103
|
+
UiHelpers.set_locale!(cfg.locale)
|
|
104
|
+
rescue UiHelpers::UnsupportedLocaleError
|
|
105
|
+
UiHelpers.set_locale!("en")
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def do_install(root, cfg)
|
|
110
|
+
return fail_not_repo("install") unless git_repo?(root)
|
|
111
|
+
|
|
112
|
+
res = Service.install!(root: root, cfg: cfg, ui: ui, dry_run: @opts[:dry_run])
|
|
113
|
+
return emit("install", "success", res) if @opts[:json]
|
|
114
|
+
|
|
115
|
+
total = res[:installed].size + res[:updated].size
|
|
116
|
+
if total.zero?
|
|
117
|
+
say("#{mark(:info)} #{UiHelpers.t("messages.no_hooks")}")
|
|
118
|
+
else
|
|
119
|
+
say("#{mark(:success)} #{UiHelpers.t("messages.installed", count: total)}")
|
|
120
|
+
unless res[:backed_up].empty?
|
|
121
|
+
say("#{mark(:info)} #{UiHelpers.t("messages.backed_up", list: res[:backed_up].join(", "))}")
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
0
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def do_uninstall(root, _cfg)
|
|
128
|
+
return fail_not_repo("uninstall") unless git_repo?(root)
|
|
129
|
+
|
|
130
|
+
unless @opts[:yes]
|
|
131
|
+
return err_code("uninstall", UiHelpers.t("errors.confirmation_required")) if @opts[:json]
|
|
132
|
+
|
|
133
|
+
unless confirm?("#{mark(:question)} #{UiHelpers.t("prompts.uninstall")}")
|
|
134
|
+
return err_code("uninstall", UiHelpers.t("errors.cancelled"))
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
res = Service.uninstall!(root: root, ui: ui, dry_run: @opts[:dry_run])
|
|
139
|
+
return emit("uninstall", "success", res) if @opts[:json]
|
|
140
|
+
|
|
141
|
+
say("#{mark(:broom)} #{UiHelpers.t("messages.removed", count: res[:removed].size)}")
|
|
142
|
+
say("#{mark(:info)} #{UiHelpers.t("messages.restored", count: res[:restored].size)}") unless res[:restored].empty?
|
|
143
|
+
0
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def do_list(root, cfg)
|
|
147
|
+
rows = Service.status(root: root, cfg: cfg)
|
|
148
|
+
return emit("list", "success", { hooks: rows }) if @opts[:json]
|
|
149
|
+
|
|
150
|
+
if rows.empty?
|
|
151
|
+
say("#{mark(:info)} #{UiHelpers.t("messages.no_hooks")}")
|
|
152
|
+
return 0
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
say("#{mark(:info)} #{UiHelpers.t("messages.list_header")}")
|
|
156
|
+
rows.each { |r| say(format(" • %-22s %s", r[:name], r[:state])) }
|
|
157
|
+
0
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def do_run(_root, cfg, argv)
|
|
161
|
+
name = argv.shift
|
|
162
|
+
return err_code("run", UiHelpers.t("usage.run")) if name.to_s.strip.empty?
|
|
163
|
+
|
|
164
|
+
cmd = (cfg.hooks || {})[name]
|
|
165
|
+
return err_code("run", UiHelpers.t("errors.hook_not_configured", name: name)) if cmd.nil?
|
|
166
|
+
|
|
167
|
+
script = HookBuilder.script_for(name: name, commands: cmd, fail_on_error: cfg.fail_on_error)
|
|
168
|
+
Tempfile.create(["vgh-", ""]) do |f|
|
|
169
|
+
f.write(script)
|
|
170
|
+
f.flush
|
|
171
|
+
File.chmod(0o755, f.path) unless Gem.win_platform?
|
|
172
|
+
Util::Verbose.log("sh", f.path, *argv)
|
|
173
|
+
system("sh", f.path, *argv) ? 0 : 1
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def cmd_version
|
|
178
|
+
return emit("version", "success", { version: VERSION }) if @opts[:json]
|
|
179
|
+
|
|
180
|
+
say("#{mark(:version)} #{UiHelpers.t("log.version_line", version: VERSION)}")
|
|
181
|
+
0
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def git_repo?(root)
|
|
185
|
+
Util::Git.available? && Util::Git.repo?(root)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def fail_not_repo(action)
|
|
189
|
+
err_code(action, UiHelpers.t("errors.not_a_repo"))
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def err_code(action, message)
|
|
193
|
+
return emit(action, "error", { error: message }) if @opts[:json]
|
|
194
|
+
|
|
195
|
+
err("#{mark(:error)} #{message}")
|
|
196
|
+
1
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def emit(action, status, data)
|
|
200
|
+
puts JSON.generate({ action: action, status: status }.merge(data))
|
|
201
|
+
status == "success" ? 0 : 1
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def mark(key)
|
|
205
|
+
UiHelpers.e(key, no_emoji: @opts[:no_emoji])
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def confirm?(prompt)
|
|
209
|
+
return true if @opts[:yes]
|
|
210
|
+
|
|
211
|
+
print "#{prompt} "
|
|
212
|
+
%w[y yes o oui].include?($stdin.gets.to_s.strip.downcase)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def ui
|
|
216
|
+
@env.respond_to?(:ui) ? @env.ui : nil
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def say(msg)
|
|
220
|
+
UiHelpers.say(ui, msg)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def err(msg)
|
|
224
|
+
UiHelpers.error(ui, msg)
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module VagrantGitHooks
|
|
4
|
+
# Vagrant configuration for managed Git hooks.
|
|
5
|
+
#
|
|
6
|
+
# @!attribute hooks
|
|
7
|
+
# @return [Hash{String=>String,Array<String>}] Hook commands keyed by hook name.
|
|
8
|
+
# @!attribute hooks_source
|
|
9
|
+
# @return [String, nil] Directory containing hook scripts to adopt.
|
|
10
|
+
# @!attribute install_on_up
|
|
11
|
+
# @return [Boolean] Whether hooks are installed during `vagrant up`.
|
|
12
|
+
# @!attribute remove_on_destroy
|
|
13
|
+
# @return [Boolean] Whether managed hooks are removed during `vagrant destroy`.
|
|
14
|
+
# @!attribute fail_on_error
|
|
15
|
+
# @return [Boolean] Whether generated hooks should stop on the first failing command.
|
|
16
|
+
class Config < Vagrant.plugin("2", :config)
|
|
17
|
+
KNOWN_HOOKS = %w[
|
|
18
|
+
applypatch-msg pre-applypatch post-applypatch
|
|
19
|
+
pre-commit pre-merge-commit prepare-commit-msg commit-msg post-commit
|
|
20
|
+
pre-rebase post-checkout post-merge pre-push pre-auto-gc
|
|
21
|
+
post-rewrite post-index-change reference-transaction fsmonitor-watchman
|
|
22
|
+
p4-changelist p4-prepare-changelist p4-post-changelist p4-pre-submit
|
|
23
|
+
].freeze
|
|
24
|
+
|
|
25
|
+
attr_accessor :hooks, :hooks_source, :install_on_up, :remove_on_destroy,
|
|
26
|
+
:fail_on_error, :locale, :verbose
|
|
27
|
+
|
|
28
|
+
def initialize
|
|
29
|
+
@hooks = UNSET_VALUE
|
|
30
|
+
@hooks_source = UNSET_VALUE
|
|
31
|
+
@install_on_up = UNSET_VALUE
|
|
32
|
+
@remove_on_destroy = UNSET_VALUE
|
|
33
|
+
@fail_on_error = UNSET_VALUE
|
|
34
|
+
@locale = UNSET_VALUE
|
|
35
|
+
@verbose = UNSET_VALUE
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def finalize!
|
|
39
|
+
@hooks = {} if @hooks == UNSET_VALUE
|
|
40
|
+
@hooks_source = nil if @hooks_source == UNSET_VALUE
|
|
41
|
+
@install_on_up = true if @install_on_up == UNSET_VALUE
|
|
42
|
+
@remove_on_destroy = false if @remove_on_destroy == UNSET_VALUE
|
|
43
|
+
@fail_on_error = true if @fail_on_error == UNSET_VALUE
|
|
44
|
+
@locale = "en" if @locale == UNSET_VALUE
|
|
45
|
+
@verbose = false if @verbose == UNSET_VALUE
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def validate(_machine)
|
|
49
|
+
errors = []
|
|
50
|
+
|
|
51
|
+
validate_hooks(errors)
|
|
52
|
+
|
|
53
|
+
errors << "hooks_source must be a String path" if @hooks_source && !@hooks_source.is_a?(String)
|
|
54
|
+
|
|
55
|
+
errors << "install_on_up must be boolean" unless boolean?(@install_on_up)
|
|
56
|
+
errors << "remove_on_destroy must be boolean" unless boolean?(@remove_on_destroy)
|
|
57
|
+
errors << "fail_on_error must be boolean" unless boolean?(@fail_on_error)
|
|
58
|
+
|
|
59
|
+
unless @locale.is_a?(String) && %w[en fr].include?(@locale.to_s[0, 2].downcase)
|
|
60
|
+
errors << "locale must be 'en' or 'fr'"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
{ "vagrant-git-hooks" => errors }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def validate_hooks(errors)
|
|
69
|
+
unless @hooks.is_a?(Hash)
|
|
70
|
+
errors << 'hooks must be a Hash of { "hook-name" => "command" | [commands] }'
|
|
71
|
+
return
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
@hooks.each do |name, cmd|
|
|
75
|
+
errors << "unknown git hook: #{name}" unless KNOWN_HOOKS.include?(name.to_s)
|
|
76
|
+
|
|
77
|
+
unless cmd.is_a?(String) || (cmd.is_a?(Array) && cmd.all?(String))
|
|
78
|
+
errors << "hook '#{name}' command must be a String or Array of Strings"
|
|
79
|
+
next
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
blank = cmd.is_a?(String) ? cmd.strip.empty? : Array(cmd).all? { |c| c.to_s.strip.empty? }
|
|
83
|
+
errors << "hook '#{name}' command is empty" if blank
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def boolean?(val)
|
|
88
|
+
[true, false].include?(val)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
require "i18n"
|
|
5
|
+
|
|
6
|
+
module VagrantGitHooks
|
|
7
|
+
module UiHelpers
|
|
8
|
+
class MissingTranslationError < StandardError; end
|
|
9
|
+
class UnsupportedLocaleError < StandardError; end
|
|
10
|
+
|
|
11
|
+
SUPPORTED = %i[en fr].freeze
|
|
12
|
+
OUR_NAMESPACES = %w[messages. errors. usage. help. log. emoji.].freeze
|
|
13
|
+
|
|
14
|
+
EMOJI = {
|
|
15
|
+
success: "✅",
|
|
16
|
+
info: "🔍",
|
|
17
|
+
ongoing: "🔁",
|
|
18
|
+
warning: "⚠️",
|
|
19
|
+
error: "❌",
|
|
20
|
+
version: "💾",
|
|
21
|
+
broom: "🧹",
|
|
22
|
+
question: "❓",
|
|
23
|
+
bug: "🐛"
|
|
24
|
+
}.freeze
|
|
25
|
+
|
|
26
|
+
module_function
|
|
27
|
+
|
|
28
|
+
def setup_i18n!
|
|
29
|
+
return if defined?(@i18n_setup) && @i18n_setup
|
|
30
|
+
|
|
31
|
+
::I18n.enforce_available_locales = false
|
|
32
|
+
|
|
33
|
+
base = File.expand_path("../../locales", __dir__)
|
|
34
|
+
paths = Dir[File.join(base, "*.yml")]
|
|
35
|
+
if paths.empty? && File.directory?(base)
|
|
36
|
+
paths = Dir.children(base).grep(/\.ya?ml\z/).map { |file| File.join(base, file) }
|
|
37
|
+
end
|
|
38
|
+
::I18n.load_path |= paths
|
|
39
|
+
::I18n.available_locales = SUPPORTED
|
|
40
|
+
|
|
41
|
+
default = ((ENV["VGH_LANG"] || ENV["LANG"] || "en")[0, 2] || "en").to_sym
|
|
42
|
+
::I18n.default_locale = SUPPORTED.include?(default) ? default : :en
|
|
43
|
+
|
|
44
|
+
::I18n.backend.load_translations
|
|
45
|
+
@i18n_setup = true
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def set_locale!(lang)
|
|
49
|
+
setup_i18n!
|
|
50
|
+
sym = lang.to_s[0, 2].downcase.to_sym
|
|
51
|
+
unless SUPPORTED.include?(sym)
|
|
52
|
+
raise UnsupportedLocaleError,
|
|
53
|
+
"#{EMOJI[:error]} Unsupported language: #{sym}. Available: #{SUPPORTED.join(", ")}"
|
|
54
|
+
end
|
|
55
|
+
::I18n.locale = sym
|
|
56
|
+
::I18n.backend.load_translations
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def setup_locale_from_config!(cfg)
|
|
60
|
+
lang = (cfg.respond_to?(:locale) ? cfg.locale : nil) || ENV.fetch("VGH_LANG", nil)
|
|
61
|
+
return unless lang
|
|
62
|
+
|
|
63
|
+
set_locale!(lang)
|
|
64
|
+
rescue UnsupportedLocaleError
|
|
65
|
+
set_locale!("en")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
NS = "vgh"
|
|
69
|
+
|
|
70
|
+
def namespaced(key)
|
|
71
|
+
k = key.to_s
|
|
72
|
+
k.start_with?("#{NS}.") ? k : "#{NS}.#{k}"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def t(key, **opts)
|
|
76
|
+
setup_i18n!
|
|
77
|
+
::I18n.t(namespaced(key), **opts)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def t_hash(key)
|
|
81
|
+
setup_i18n!
|
|
82
|
+
v = ::I18n.t(namespaced(key), default: {})
|
|
83
|
+
v.is_a?(Hash) ? v : {}
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def our_key?(key)
|
|
87
|
+
OUR_NAMESPACES.any? { |ns| key.to_s.start_with?(ns) }
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def e(key, no_emoji: false)
|
|
91
|
+
return "" if no_emoji || ENV["VGH_NO_EMOJI"] == "1"
|
|
92
|
+
|
|
93
|
+
EMOJI[key] || ""
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def say(ui, msg)
|
|
97
|
+
ui&.info(msg) || puts(msg)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def warn(ui, msg)
|
|
101
|
+
ui&.warn(msg) || puts(msg)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def error(ui, msg)
|
|
105
|
+
ui&.error(msg) || warn(ui, msg)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def debug_enabled?
|
|
109
|
+
ENV["VGH_DEBUG"].to_s == "1"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def debug(ui, msg)
|
|
113
|
+
return unless debug_enabled?
|
|
114
|
+
|
|
115
|
+
say(ui, "#{e(:bug)} #{msg}")
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def print_general_help
|
|
119
|
+
setup_i18n!
|
|
120
|
+
puts t("help.general_title")
|
|
121
|
+
t_hash("help.commands").each_value { |line| puts " #{line}" }
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def print_topic_help(topic)
|
|
125
|
+
setup_i18n!
|
|
126
|
+
topic = topic.to_s.downcase.strip
|
|
127
|
+
return print_general_help if topic.empty?
|
|
128
|
+
|
|
129
|
+
body = t("help.topic.#{topic}", default: nil)
|
|
130
|
+
body ? puts(body) : print_general_help
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module VagrantGitHooks
|
|
6
|
+
module HookBuilder
|
|
7
|
+
SHEBANG = "#!/usr/bin/env sh"
|
|
8
|
+
BLOCK_START = "# >>> vagrant-git-hooks (managed) >>>"
|
|
9
|
+
BLOCK_STOP = "# <<< vagrant-git-hooks (managed) <<<"
|
|
10
|
+
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
# Builds a managed Git hook script from configured shell commands.
|
|
14
|
+
#
|
|
15
|
+
# @param name [String] Git hook name.
|
|
16
|
+
# @param commands [String, Array<String>] Shell commands written to the hook body.
|
|
17
|
+
# @param fail_on_error [Boolean] Whether to include `set -e`.
|
|
18
|
+
# @param newline [String] Line separator used in the generated script.
|
|
19
|
+
# @return [String] Complete hook script.
|
|
20
|
+
def script_for(name:, commands:, fail_on_error: true, newline: "\n")
|
|
21
|
+
lines = header(name, "do not edit manually")
|
|
22
|
+
lines << "set -e" if fail_on_error
|
|
23
|
+
lines.concat(body_lines(commands))
|
|
24
|
+
lines << BLOCK_STOP
|
|
25
|
+
lines.join(newline) + newline
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Wraps an existing hook body in plugin markers.
|
|
29
|
+
#
|
|
30
|
+
# Preserves user-provided hook bodies when adopting from hooks_source, but
|
|
31
|
+
# wraps them in markers so uninstall and status can manage them later.
|
|
32
|
+
#
|
|
33
|
+
# @param name [String] Git hook name.
|
|
34
|
+
# @param content [String] Existing hook script content.
|
|
35
|
+
# @param newline [String] Line separator used in the generated script.
|
|
36
|
+
# @return [String] Managed hook script.
|
|
37
|
+
def adopt_script(name:, content:, newline: "\n")
|
|
38
|
+
lines = content.to_s.split(/\r?\n/, -1)
|
|
39
|
+
lines.pop if lines.last == ""
|
|
40
|
+
|
|
41
|
+
shebang = lines.first&.start_with?("#!") ? lines.shift : SHEBANG
|
|
42
|
+
head = [shebang, BLOCK_START, "# Managed by Vagrant - adopted from hooks_source (hook: #{name})"]
|
|
43
|
+
|
|
44
|
+
(head + lines + [BLOCK_STOP]).join(newline) + newline
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def managed?(content)
|
|
48
|
+
content.to_s.include?(BLOCK_START)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def header(name, note)
|
|
52
|
+
ts = begin
|
|
53
|
+
Time.now.utc.iso8601
|
|
54
|
+
rescue StandardError
|
|
55
|
+
Time.now.utc.to_s
|
|
56
|
+
end
|
|
57
|
+
[
|
|
58
|
+
SHEBANG,
|
|
59
|
+
BLOCK_START,
|
|
60
|
+
"# Managed by Vagrant - #{note} (hook: #{name})",
|
|
61
|
+
"# Generated: #{ts}"
|
|
62
|
+
]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def body_lines(commands)
|
|
66
|
+
Array(commands).flat_map { |c| c.to_s.split("\n") }
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|