this repo has no description
at master 109 lines 3.2 kB view raw
1// Copyright 2024 CUE Authors 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15package main 16 17import ( 18 "bytes" 19 "fmt" 20 "log" 21 "os" 22 "os/exec" 23 "regexp" 24 "slices" 25 "strings" 26) 27 28func main() { 29 wd, err := os.Getwd() 30 if err != nil { 31 log.Fatal(err) 32 } 33 if err := checkCommit(wd); err != nil { 34 log.Fatal(err) 35 } 36} 37 38func checkCommit(dir string) error { 39 body, err := runCmd(dir, "git", "log", "-1", "--format=%B", "HEAD") 40 if err != nil { 41 return err 42 } 43 44 // Ensure that commit messages have a blank second line. 45 // We know that a commit message must be longer than a single 46 // line because each commit must be signed-off. 47 lines := strings.Split(body, "\n") 48 if len(lines) > 1 && lines[1] != "" { 49 return fmt.Errorf("the second line of a commit message must be blank") 50 } 51 52 // All authors, including co-authors, must have a signed-off trailer by email. 53 // Note that trailers are in the form "Name <email>", so grab the email with regexp. 54 // For now, we require the sorted lists of author and signer emails to match. 55 // Note that this also fails if a commit isn't signed-off at all. 56 // 57 // In Gerrit we already enable a form of this via https://gerrit-review.googlesource.com/Documentation/project-configuration.html#require-signed-off-by, 58 // but it does not support co-authors nor can it be used when testing GitHub PRs. 59 authorEmail, err := runCmd(dir, "git", "log", "-1", "--format=%ae") 60 if err != nil { 61 return err 62 } 63 coauthorList, err := runCmd(dir, "git", "log", "-1", "--format=%(trailers:key=Co-authored-by,valueonly)") 64 if err != nil { 65 return err 66 } 67 authors := slices.Concat([]string{authorEmail}, extractEmails(coauthorList)) 68 slices.Sort(authors) 69 authors = slices.Compact(authors) 70 71 signerList, err := runCmd(dir, "git", "log", "-1", "--format=%(trailers:key=Signed-off-by,valueonly)") 72 if err != nil { 73 return err 74 } 75 signers := extractEmails(signerList) 76 slices.Sort(signers) 77 signers = slices.Compact(signers) 78 79 if !slices.Equal(authors, signers) { 80 return fmt.Errorf("commit author email addresses %q do not match signed-off-by trailers %q", 81 authors, signers) 82 } 83 return nil 84} 85 86func runCmd(dir string, exe string, args ...string) (string, error) { 87 cmd := exec.Command(exe, args...) 88 cmd.Dir = dir 89 out, err := cmd.CombinedOutput() 90 return string(bytes.TrimSpace(out)), err 91} 92 93var ( 94 rxExtractEmail = regexp.MustCompile(`.*<(.*)\>$`) 95 rxUserMention = regexp.MustCompile(`(^|\s)(@[a-z0-9][a-z0-9-]*)`) 96) 97 98func extractEmails(list string) []string { 99 lines := strings.Split(list, "\n") 100 var emails []string 101 for _, line := range lines { 102 m := rxExtractEmail.FindStringSubmatch(line) 103 if m == nil { 104 continue // no match; discard this line 105 } 106 emails = append(emails, m[1]) 107 } 108 return emails 109}