v / cmd / tools / vgit-fmt-hook.v
153 lines · 142 sloc · 4.63 KB · dd1ad2b6abfa62a9695e4e04d4827bff137ff94e
Raw
1import os
2import crypto.sha256
3
4const vexe = os.getenv_opt('VEXE') or { panic('missing VEXE env variable') }
5const vroot = os.to_slash(os.real_path(os.dir(vexe)))
6const horiginal = os.to_slash(os.join_path(vroot, 'cmd/tools/git_pre_commit_hook.vsh'))
7
8fn get_hook_target(git_folder string) string {
9 return os.to_slash(os.join_path(git_folder, 'hooks/pre-commit'))
10}
11
12fn main() {
13 // On OS without support for 'env -S', generate shell script
14 // to run cmd/tools/git_pre_commit_hook.vsh
15 // TODO: detect other OS (BusyBox) without support for 'env -S'
16 $if openbsd {
17 shoriginal := os.join_path(os.vtmp_dir(), 'git_pre_commit_hook.sh')
18 os.write_file(shoriginal, '#!/bin/sh\nv run ${horiginal}') or {
19 eprintln('unable to write shell script ${shoriginal}')
20 exit(1)
21 }
22 os.chmod(shoriginal, 0o755)!
23 }
24 git_folder := find_nearest_top_level_folder_with_a_git_subfolder(os.getwd()) or {
25 eprintln('This command has to be run inside a Git repository.')
26 exit(0)
27 }
28 os.chdir(git_folder)!
29 htarget := get_hook_target(git_folder)
30 cmd := os.args[2] or { 'status' }
31 match cmd {
32 'status' {
33 cmd_status(htarget)
34 }
35 'install' {
36 cmd_install(htarget)
37 }
38 'remove' {
39 cmd_remove(htarget)
40 }
41 else {
42 eprintln('Unknown command `${cmd}`. Known commands are: `status`, `install` or `remove`')
43 exit(1)
44 }
45 }
46}
47
48fn cmd_status(htarget string) {
49 report_status(htarget, true)
50}
51
52fn cmd_install(htarget string) {
53 if report_status(htarget, false) {
54 return
55 }
56 println('> Installing the newest version of ${horiginal} over ${htarget} ...')
57 $if openbsd {
58 shoriginal := os.join_path(os.vtmp_dir(), 'git_pre_commit_hook.sh')
59 os.cp(shoriginal, htarget) or { err_exit('failed to copy to ${htarget}') }
60 } $else {
61 os.cp(horiginal, htarget) or { err_exit('failed to copy to ${htarget}') }
62 }
63 println('> Done.')
64}
65
66fn cmd_remove(htarget string) {
67 report_status(htarget, false)
68 if !os.exists(htarget) {
69 err_exit('file ${htarget} has been removed already')
70 }
71 println('> Removing ${htarget} ...')
72 os.rm(htarget) or { err_exit('failed to remove ${htarget}') }
73 println('> Done.')
74}
75
76// Returns true if pre-commit Git hook already exists and identical to VSH script
77fn report_status(htarget string, show_instructions bool) bool {
78 mut original := ''
79 $if openbsd {
80 shoriginal := os.join_path(os.vtmp_dir(), 'git_pre_commit_hook.sh')
81 original = shoriginal
82 } $else {
83 original = horiginal
84 }
85 ostat := os.stat(original) or { os.Stat{} }
86 tstat := os.stat(htarget) or { os.Stat{} }
87 ohash := hash_file(original) or { '' }
88 thash := hash_file(htarget) or { '' }
89 if os.exists(htarget) && os.is_file(htarget) {
90 println('> CURRENT git repo pre-commit hook: size: ${tstat.size:6} bytes, sha256: ${thash}, ${htarget}')
91 } else {
92 println('> CURRENT git repo pre-commit hook: missing ${htarget}')
93 }
94 if os.exists(original) && os.is_file(original) {
95 println('> Main V repo pre-commit hook script: size: ${ostat.size:6} bytes, sha256: ${ohash}, ${original}')
96 }
97 if ohash == thash {
98 println('> Both files are exactly the same.')
99 if show_instructions {
100 show_msg_about_removing(htarget)
101 }
102 return true
103 }
104 println('> Files have different hashes.')
105 if ohash != '' && thash != '' {
106 existing_content := os.read_file(htarget) or { '' }
107 if !existing_content.contains('hooks.stopCommitOfNonVfmtedVFiles') {
108 // both files do exist, but the current git repo hook, is not compatible (an older version of git_pre_commit_hook.vsh):
109 err_exit('the existing file ${htarget} , does not appear to be a compatible V formatting hook\nYou have to remove it manually')
110 }
111 }
112 if show_instructions {
113 println("> Use `v git-fmt-hook install` to update the CURRENT repository's pre-commit hook,")
114 println('> with the newest pre-commit formatting script from the main V repo.')
115 show_msg_about_removing(htarget)
116 }
117 return false
118}
119
120fn show_msg_about_removing(htarget string) {
121 if os.exists(htarget) {
122 println("> Use `v git-fmt-hook remove` to remove the CURRENT repository's pre-commit hook.")
123 }
124}
125
126fn find_nearest_top_level_folder_with_a_git_subfolder(current string) ?string {
127 mut cfolder := os.to_slash(os.real_path(current))
128 for level := 0; level < 255; level++ {
129 if cfolder == '/' || cfolder == '' {
130 break
131 }
132 git_folder := os.join_path(cfolder, '.git')
133 if os.is_dir(git_folder) {
134 return git_folder
135 }
136 cfolder = os.dir(cfolder)
137 }
138 return none
139}
140
141fn hash_file(path string) !string {
142 fbytes := os.read_bytes(path)!
143 mut digest256 := sha256.new()
144 digest256.write(fbytes)!
145 mut sum256 := digest256.sum([])
146 return sum256.hex()
147}
148
149@[noreturn]
150fn err_exit(msg string) {
151 eprintln('> error: ${msg} .')
152 exit(0) // note: this is important, since the command is ran in `v up` and during `make`
153}
154