-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpush.go
More file actions
191 lines (173 loc) · 5.21 KB
/
Copy pathpush.go
File metadata and controls
191 lines (173 loc) · 5.21 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
package cmd
import (
"bufio"
"fmt"
"io"
"os"
"os/exec"
"strings"
"github.com/spf13/cobra"
"github.com/zcube/commit-checker/internal/checker"
"github.com/zcube/commit-checker/internal/config"
"github.com/zcube/commit-checker/internal/i18n"
)
var pushRange string
var pushCmd = &cobra.Command{
Use: "push",
RunE: func(cmd *cobra.Command, args []string) error {
// --require-config: 프로젝트 설정 파일이 없으면 아무 출력 없이 성공 종료 (전역 opt-in 설치)
if requireConfigSkip() {
return nil
}
cfg, err := config.Load(resolveConfigFilePath(configFile))
if err != nil {
return fmt.Errorf("failed to load config: %w", err)
}
// enabled: false — 리포 단위 opt-out: 모든 검사를 건너뛰고 성공 종료
if !cfg.IsEnabled() {
return nil
}
var commitRanges []string
if pushRange != "" {
commitRanges = []string{pushRange}
} else {
// stdin이 파이프/파일일 때만 읽기
stat, _ := os.Stdin.Stat()
if (stat.Mode() & os.ModeCharDevice) == 0 {
commitRanges = parsePushRanges(os.Stdin)
}
}
if len(commitRanges) == 0 {
return nil
}
var allErrs []string
for _, r := range commitRanges {
hashes, listErr := listPushCommitHashes(r)
if listErr != nil {
fmt.Fprintln(os.Stderr, i18n.T("cmd.push.warn_list_failed", map[string]any{"Range": r, "Error": listErr.Error()}))
continue
}
for _, hash := range hashes {
msg, msgErr := getPushCommitMessage(hash)
if msgErr != nil {
fmt.Fprintln(os.Stderr, i18n.T("cmd.push.warn_msg_failed", map[string]any{"Hash": hash[:7], "Error": msgErr.Error()}))
continue
}
errs := checker.CheckMsg(cfg, msg)
for _, e := range errs {
allErrs = append(allErrs, fmt.Sprintf("[%s] %s", hash[:7], e))
}
}
}
if len(allErrs) > 0 {
for _, e := range allErrs {
fmt.Fprintln(os.Stderr, e)
}
// 여러 커밋이 실패해도 가이드는 1회만 출력
if guideEnabled(cfg) {
printCommitMessageGuide()
}
return errSilentExit
}
return nil
},
}
// parsePushRanges: git pre-push 훅 stdin 형식을 파싱하여 커밋 범위 목록을 반환합니다.
// 각 줄 형식: <local ref> <local sha1> <remote ref> <remote sha1>
func parsePushRanges(r io.Reader) []string {
var ranges []string
scanner := bufio.NewScanner(r)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" {
continue
}
parts := strings.Fields(line)
if len(parts) != 4 {
continue
}
localSHA := parts[1]
remoteSHA := parts[3]
// 로컬 SHA가 0이면 브랜치 삭제 — 건너뜁니다
if isPushZeroSHA(localSHA) {
continue
}
if isPushZeroSHA(remoteSHA) {
// 새 브랜치: 리모트 기본 브랜치에서 분기된 커밋을 검사합니다
base := findPushRemoteBase()
if base == "" {
fmt.Fprintln(os.Stderr, i18n.T("cmd.push.warn_no_base", map[string]any{"Ref": parts[0]}))
continue
}
ranges = append(ranges, base+".."+localSHA)
} else {
ranges = append(ranges, remoteSHA+".."+localSHA)
}
}
return ranges
}
// isPushZeroSHA: SHA가 40개의 '0'으로 구성된 빈 SHA인지 확인합니다.
func isPushZeroSHA(sha string) bool {
if len(sha) != 40 {
return false
}
for _, c := range sha {
if c != '0' {
return false
}
}
return true
}
// findPushRemoteBase: 업스트림 추적 브랜치 또는 리모트 기본 브랜치를 찾아 반환합니다.
func findPushRemoteBase() string {
// 현재 브랜치의 업스트림 추적 브랜치 시도
out, err := exec.Command("git", "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}").Output()
if err == nil {
return strings.TrimSpace(string(out))
}
// origin HEAD로 기본 브랜치 탐지 (git remote set-head origin -a 필요)
out, err = exec.Command("git", "symbolic-ref", "refs/remotes/origin/HEAD").Output()
if err == nil {
// 예: refs/remotes/origin/main 에서 origin/main 추출
ref := strings.TrimSpace(string(out))
if after, ok := strings.CutPrefix(ref, "refs/remotes/"); ok {
return after
}
}
// 폴백: main, master 순으로 확인
for _, branch := range []string{"origin/main", "origin/master"} {
if _, err := exec.Command("git", "rev-parse", "--verify", branch).Output(); err == nil {
return branch
}
}
return ""
}
// listPushCommitHashes: 주어진 범위 내 커밋 해시 목록을 반환합니다.
func listPushCommitHashes(commitRange string) ([]string, error) {
out, err := exec.Command("git", "log", "--format=%H", commitRange).Output()
if err != nil {
return nil, err
}
var hashes []string
for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
line = strings.TrimSpace(line)
if line != "" {
hashes = append(hashes, line)
}
}
return hashes, nil
}
// getPushCommitMessage: 주어진 커밋 해시의 커밋 메시지를 반환합니다.
func getPushCommitMessage(hash string) (string, error) {
out, err := exec.Command("git", "show", "-s", "--format=%B", hash).Output()
if err != nil {
return "", err
}
return strings.TrimRight(string(out), "\n"), nil
}
func init() {
pushCmd.Short = i18n.T("cmd.push.short", nil)
pushCmd.Long = i18n.T("cmd.push.long", nil)
pushCmd.Flags().StringVar(&pushRange, "range", "", i18n.T("flag.push_range", nil))
rootCmd.AddCommand(pushCmd)
}