Skip to content

Commit 1dfa4d2

Browse files
Add pre commit hooks (#2206)
1 parent d9d6d7e commit 1dfa4d2

11 files changed

+532
-0
lines changed
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
hooks:
2+
common:
3+
- repo: https://github.com/pre-commit/pre-commit-hooks
4+
rev: v4.4.0
5+
hooks:
6+
- id: end-of-file-fixer
7+
- id: trailing-whitespace
8+
- repo: https://github.com/gitleaks/gitleaks
9+
rev: v8.16.3
10+
hooks:
11+
- id: gitleaks
12+
Python:
13+
- repo: https://github.com/pylint-dev/pylint
14+
rev: v2.17.2
15+
hooks:
16+
- id: pylint
17+
JavaScript:
18+
- repo: https://github.com/pre-commit/mirrors-eslint
19+
rev: v8.38.0
20+
hooks:
21+
- id: eslint
22+
TypeScript:
23+
- repo: https://github.com/pre-commit/mirrors-eslint
24+
rev: v8.38.0
25+
hooks:
26+
- id: eslint
27+
Java:
28+
- repo: https://github.com/gherynos/pre-commit-java
29+
rev: v0.2.4
30+
hooks:
31+
- id: Checkstyle
32+
C:
33+
- repo: https://github.com/pocc/pre-commit-hooks
34+
rev: v1.3.5
35+
hooks:
36+
- id: cpplint
37+
C++:
38+
- repo: https://github.com/pocc/pre-commit-hooks
39+
rev: v1.3.5
40+
hooks:
41+
- id: cpplint
42+
PHP:
43+
- repo: https://github.com/digitalpulp/pre-commit-php
44+
rev: 1.4.0
45+
hooks:
46+
- id: php-lint-all
47+
Ruby:
48+
- repo: https://github.com/jumanjihouse/pre-commit-hooks
49+
rev: 3.0.0
50+
hooks:
51+
- id: RuboCop
52+
Go:
53+
- repo: https://github.com/golangci/golangci-lint
54+
rev: v1.52.2
55+
hooks:
56+
- id: golangci-lint
57+
Shell:
58+
- repo: https://github.com/jumanjihouse/pre-commit-hooks
59+
rev: 3.0.0
60+
hooks:
61+
- id: shellcheck
Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
package precommit
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"io/ioutil"
7+
"os"
8+
"sort"
9+
"strings"
10+
11+
"github.com/step-security/secure-repo/remediation/workflow/permissions"
12+
"gopkg.in/yaml.v3"
13+
)
14+
15+
type UpdatePrecommitConfigResponse struct {
16+
OriginalInput string
17+
FinalOutput string
18+
IsChanged bool
19+
ConfigfileFetchError bool
20+
}
21+
22+
type UpdatePrecommitConfigRequest struct {
23+
Content string
24+
Languages []string
25+
}
26+
27+
type PrecommitConfig struct {
28+
Repos []Repo `yaml:"repos"`
29+
}
30+
31+
type Repo struct {
32+
Repo string `yaml:"repo"`
33+
Rev string `yaml:"rev"`
34+
Hooks []Hook `yaml:"hooks"`
35+
}
36+
37+
type Hook struct {
38+
Id string `yaml:"id"`
39+
}
40+
41+
type FetchPrecommitConfig struct {
42+
Hooks Hooks `yaml:"hooks"`
43+
}
44+
45+
type Hooks map[string][]Repo
46+
47+
func getConfigFile() (string, error) {
48+
filePath := os.Getenv("PRECOMMIT_CONFIG")
49+
50+
if filePath == "" {
51+
filePath = "./precommit-config.yml"
52+
}
53+
54+
configFile, err := ioutil.ReadFile(filePath)
55+
if err != nil {
56+
return "", err
57+
}
58+
59+
return string(configFile), nil
60+
}
61+
62+
func GetHooks(precommitConfig string) ([]Repo, error) {
63+
var updatePrecommitConfigRequest UpdatePrecommitConfigRequest
64+
json.Unmarshal([]byte(precommitConfig), &updatePrecommitConfigRequest)
65+
inputConfigFile := []byte(updatePrecommitConfigRequest.Content)
66+
configMetadata := PrecommitConfig{}
67+
err := yaml.Unmarshal(inputConfigFile, &configMetadata)
68+
if err != nil {
69+
return nil, err
70+
}
71+
72+
alreadyPresentHooks := make(map[string]bool)
73+
for _, repos := range configMetadata.Repos {
74+
for _, hook := range repos.Hooks {
75+
alreadyPresentHooks[hook.Id] = true
76+
}
77+
}
78+
79+
configFile, err := getConfigFile()
80+
if err != nil {
81+
return nil, err
82+
}
83+
var fetchPrecommitConfig FetchPrecommitConfig
84+
yaml.Unmarshal([]byte(configFile), &fetchPrecommitConfig)
85+
newHooks := make(map[string]Repo)
86+
for _, lang := range updatePrecommitConfigRequest.Languages {
87+
if _, isSupported := fetchPrecommitConfig.Hooks[lang]; !isSupported {
88+
continue
89+
}
90+
if _, ok := alreadyPresentHooks[fetchPrecommitConfig.Hooks[lang][0].Hooks[0].Id]; !ok {
91+
if repo, ok := newHooks[fetchPrecommitConfig.Hooks[lang][0].Repo]; ok {
92+
repo.Hooks = append(repo.Hooks, fetchPrecommitConfig.Hooks[lang][0].Hooks...)
93+
newHooks[fetchPrecommitConfig.Hooks[lang][0].Repo] = repo
94+
} else {
95+
newHooks[fetchPrecommitConfig.Hooks[lang][0].Repo] = fetchPrecommitConfig.Hooks[lang][0]
96+
}
97+
alreadyPresentHooks[fetchPrecommitConfig.Hooks[lang][0].Hooks[0].Id] = true
98+
}
99+
}
100+
// Adding common hooks
101+
var repos []Repo
102+
for _, repo := range fetchPrecommitConfig.Hooks["common"] {
103+
tempRepo := repo
104+
tempRepo.Hooks = nil
105+
hookPresent := false
106+
for _, hook := range repo.Hooks {
107+
if _, ok := alreadyPresentHooks[hook.Id]; !ok {
108+
tempRepo.Hooks = append(tempRepo.Hooks, hook)
109+
hookPresent = true
110+
}
111+
}
112+
if hookPresent {
113+
repos = append(repos, tempRepo)
114+
}
115+
}
116+
for _, repo := range newHooks {
117+
repos = append(repos, repo)
118+
}
119+
sort.Slice(repos, func(i, j int) bool {
120+
return repos[i].Repo < repos[j].Repo
121+
})
122+
return repos, nil
123+
}
124+
125+
func UpdatePrecommitConfig(precommitConfig string, Hooks []Repo) (*UpdatePrecommitConfigResponse, error) {
126+
var updatePrecommitConfigRequest UpdatePrecommitConfigRequest
127+
json.Unmarshal([]byte(precommitConfig), &updatePrecommitConfigRequest)
128+
inputConfigFile := []byte(updatePrecommitConfigRequest.Content)
129+
configMetadata := PrecommitConfig{}
130+
err := yaml.Unmarshal(inputConfigFile, &configMetadata)
131+
if err != nil {
132+
return nil, err
133+
}
134+
135+
response := new(UpdatePrecommitConfigResponse)
136+
response.FinalOutput = updatePrecommitConfigRequest.Content
137+
response.OriginalInput = updatePrecommitConfigRequest.Content
138+
response.IsChanged = false
139+
140+
response.FinalOutput = strings.TrimSuffix(response.FinalOutput, "\n")
141+
repoIndent := 0
142+
repoGap := 1
143+
hooksIndent := 2
144+
hooksGap := 1
145+
if updatePrecommitConfigRequest.Content == "" {
146+
response.FinalOutput = "repos:"
147+
} else {
148+
repoIndent, repoGap, hooksIndent, hooksGap, err = getPrecommitIndentation(response.FinalOutput)
149+
if err != nil {
150+
return nil, err
151+
}
152+
}
153+
154+
for _, Update := range Hooks {
155+
repoAlreadyExist := false
156+
for _, update := range configMetadata.Repos {
157+
if update.Repo == Update.Repo {
158+
repoAlreadyExist = true
159+
}
160+
if repoAlreadyExist {
161+
break
162+
}
163+
}
164+
response.FinalOutput, err = addHook(Update, repoAlreadyExist, response.FinalOutput,
165+
repoIndent, repoGap, hooksIndent, hooksGap)
166+
if err != nil {
167+
return nil, err
168+
}
169+
response.IsChanged = true
170+
}
171+
172+
if !strings.HasSuffix(response.FinalOutput, "\n") {
173+
response.FinalOutput = response.FinalOutput + "\n"
174+
}
175+
176+
return response, nil
177+
}
178+
179+
func getPrecommitIndentation(content string) (int, int, int, int, error) {
180+
lines := strings.Split(content, "\n")
181+
182+
var repoIndent, repoGap, hooksIndent, hooksGap int
183+
repoFound, hooksFound := false, false
184+
for _, line := range lines {
185+
if strings.Contains(line, "repo:") && !repoFound {
186+
repoIndent = strings.Index(line, "-")
187+
repoGap = strings.Index(line, "repo:") - repoIndent - 1
188+
repoFound = true
189+
} else if strings.Contains(line, "id:") && !hooksFound {
190+
hooksIndent = strings.Index(line, "-")
191+
hooksGap = strings.Index(line, "id:") - hooksIndent - 1
192+
hooksFound = true
193+
}
194+
195+
if repoFound && hooksFound {
196+
break
197+
}
198+
}
199+
200+
return repoIndent, repoGap, hooksIndent, hooksGap, nil
201+
}
202+
203+
func addHook(Update Repo, repoAlreadyExist bool, inputYaml string, repoIndent, repoGap, hooksIndent, hooksGap int) (string, error) {
204+
t := yaml.Node{}
205+
206+
err := yaml.Unmarshal([]byte(inputYaml), &t)
207+
if err != nil {
208+
return "", fmt.Errorf("unable to parse yaml %v", err)
209+
}
210+
211+
if repoAlreadyExist {
212+
jobNode := permissions.IterateNode(&t, Update.Repo, "!!str", 0)
213+
if jobNode == nil {
214+
return "", fmt.Errorf("Repo Name %s not found in the input yaml", Update.Repo)
215+
}
216+
217+
// TODO: Also update rev version for already exist repo
218+
inputLines := strings.Split(inputYaml, "\n")
219+
var output []string
220+
for i := 0; i < jobNode.Line+1; i++ {
221+
output = append(output, inputLines[i])
222+
}
223+
224+
for _, hook := range Update.Hooks {
225+
hookIndentStr := strings.Repeat(" ", hooksIndent)
226+
hookGapStr := strings.Repeat(" ", hooksGap)
227+
output = append(output, fmt.Sprintf("%s-%sid: %s", hookIndentStr, hookGapStr, hook.Id))
228+
}
229+
230+
for i := jobNode.Line + 1; i < len(inputLines); i++ {
231+
output = append(output, inputLines[i])
232+
}
233+
return strings.Join(output, "\n"), nil
234+
} else {
235+
inputLines := strings.Split(inputYaml, "\n")
236+
237+
repoIndentStr := strings.Repeat(" ", repoIndent)
238+
repoGapStr := strings.Repeat(" ", repoGap)
239+
inputLines = append(inputLines, fmt.Sprintf("%s-%srepo: %s", repoIndentStr, repoGapStr, Update.Repo))
240+
241+
revIndentStr := strings.Repeat(" ", repoIndent+repoGap+1)
242+
inputLines = append(inputLines, fmt.Sprintf("%srev: %s", revIndentStr, Update.Rev))
243+
244+
inputLines = append(inputLines, fmt.Sprintf("%shooks:", revIndentStr))
245+
246+
hookIndentStr := strings.Repeat(" ", hooksIndent)
247+
hookGapStr := strings.Repeat(" ", hooksGap)
248+
for _, hook := range Update.Hooks {
249+
inputLines = append(inputLines, fmt.Sprintf("%s-%sid: %s", hookIndentStr, hookGapStr, hook.Id))
250+
}
251+
252+
return strings.Join(inputLines, "\n"), nil
253+
}
254+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package precommit
2+
3+
import (
4+
"encoding/json"
5+
"io/ioutil"
6+
"log"
7+
"path"
8+
"testing"
9+
)
10+
11+
func TestUpdatePrecommitConfig(t *testing.T) {
12+
13+
const inputDirectory = "../../testfiles/precommit/input"
14+
const outputDirectory = "../../testfiles/precommit/output"
15+
16+
tests := []struct {
17+
fileName string
18+
Languages []string
19+
isChanged bool
20+
}{
21+
{
22+
fileName: "basic.yml",
23+
Languages: []string{"JavaScript", "C++"},
24+
isChanged: true,
25+
},
26+
{
27+
fileName: "file-not-exit.yml",
28+
Languages: []string{"JavaScript", "C++"},
29+
isChanged: true,
30+
},
31+
{
32+
fileName: "same-repo-different-hooks.yml",
33+
Languages: []string{"Ruby", "Shell"},
34+
isChanged: true,
35+
},
36+
{
37+
fileName: "style1.yml",
38+
Languages: []string{"Ruby", "Shell"},
39+
isChanged: true,
40+
},
41+
}
42+
43+
for _, test := range tests {
44+
var updatePrecommitConfigRequest UpdatePrecommitConfigRequest
45+
input, err := ioutil.ReadFile(path.Join(inputDirectory, test.fileName))
46+
if err != nil {
47+
log.Fatal(err)
48+
}
49+
updatePrecommitConfigRequest.Content = string(input)
50+
updatePrecommitConfigRequest.Languages = test.Languages
51+
inputRequest, err := json.Marshal(updatePrecommitConfigRequest)
52+
if err != nil {
53+
log.Fatal(err)
54+
}
55+
56+
hooks, err := GetHooks(string(inputRequest))
57+
if err != nil {
58+
log.Fatal(err)
59+
}
60+
output, err := UpdatePrecommitConfig(string(inputRequest), hooks)
61+
if err != nil {
62+
t.Fatalf("Error not expected: %s", err)
63+
}
64+
65+
expectedOutput, err := ioutil.ReadFile(path.Join(outputDirectory, test.fileName))
66+
if err != nil {
67+
log.Fatal(err)
68+
}
69+
70+
if string(expectedOutput) != output.FinalOutput {
71+
t.Errorf("test failed %s did not match expected output\n%s", test.fileName, output.FinalOutput)
72+
}
73+
74+
if output.IsChanged != test.isChanged {
75+
t.Errorf("test failed %s did not match IsChanged, Expected: %v Got: %v", test.fileName, test.isChanged, output.IsChanged)
76+
77+
}
78+
79+
}
80+
81+
}

0 commit comments

Comments
 (0)