package main import ( "fmt" "log" "os" "regexp" "strings" git "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/object" "github.com/go-git/go-git/v5/plumbing/storer" gha "github.com/sethvargo/go-githubactions" ) var rxHeader = regexp.MustCompile(`^(\[\#[0-9]+\]\s|Release|Revert|Reapply)`) func main() { // Prepare regexp templates. rxSignOff := regexp.MustCompile(`^Signed-off-by:`) // Open current git dir. r, err := git.PlainOpen("./") if err != nil { log.Fatalf("Failed to open repository: %v", err) } // Retrieve the commit history. head, err := r.Head() if err != nil { log.Fatalf("Failed to retrieve HEAD reference: %v", err) } // Create iterator over commits. commits, err := r.Log(&git.LogOptions{From: head.Hash()}) if err != nil { log.Fatalf("Failed to retrieve commit history: %v", err) } // Limit number of iterations. from := gha.GetInput("from") log.Printf("Resolving revision '%s'", from) var lca *object.Commit if from != "" { lca = getMergeBase(r, head, from) } log.Printf("Resolved: %s", lca.Hash) // Processing result. var fail bool _ = commits.ForEach(func(c *object.Commit) error { // Stop iterator when limit is reached. if lca != nil && c.Hash == lca.Hash { return storer.ErrStop } // Parse commit data. id := c.ID().String()[:7] lines := strings.Split(strings.Trim(c.Message, "\n"), "\n") // Do not process empty commit. if len(lines) == 0 { fail = true fmt.Printf("Error: empty commit [%s]\n", id) return nil } // Check commit header. header := lines[0] if !rxHeader.MatchString(header) { fail = true fmt.Printf("Error: invalid header '%s' [%s]\n", header, id) return nil } // Check empty line after header. if 1 < len(lines) && lines[1] != "" { fail = true fmt.Printf("Error: second line must be empty '%s' [%s]\n", lines[1], id) return nil } // Check commit sign-off. if !rxSignOff.MatchString(lines[len(lines)-1]) { fail = true fmt.Printf("Error: missing sign-off '%s' [%s]\n", header, id) return nil } fmt.Printf("OK: '%s' [%s]\n", header, id) return nil }) // Return non-zero code if DCO check failed. if fail { os.Exit(1) } } func getMergeBase(r *git.Repository, head *plumbing.Reference, from string) *object.Commit { h, err := r.ResolveRevision(plumbing.Revision(from)) if err != nil { h, err = r.ResolveRevision(plumbing.Revision(plumbing.NewBranchReferenceName(from))) if err != nil { log.Fatalf("Failed to resolve a reference '%s': %v", from, err) } } to, err := r.CommitObject(*h) if err != nil { log.Fatalf("Failed to get commit object: %v", err) } other, err := r.CommitObject(head.Hash()) if err != nil { log.Fatalf("Failed to get HEAD commit object: %v", err) } cc, err := to.MergeBase(other) if err != nil { log.Fatalf("Failed to determine merge-base: %v", err) } return cc[0] }