changelog generator & diff tool stormlightlabs.github.io/git-storm/
changelog changeset markdown golang git
at main 4.1 kB view raw
1/* 2USAGE 3 4 storm check [from] [to] [options] 5 6FLAGS 7 8 --since <tag> Check changes since the given tag 9 --repo <path> Path to the Git repository (default: .) 10 11# DESCRIPTION 12 13Validates that all commits in the specified range have corresponding unreleased 14changelog entries. This is useful for CI enforcement to ensure developers 15document their changes. 16 17Commits containing [nochanges] or [skip changelog] in the message are skipped. 18 19Exit codes: 20 21 0 - All commits have changelog entries 22 1 - One or more commits are missing changelog entries 23 2 - Command execution error 24 25TODO(issue-linking): Support checking for issue numbers in entries when --issue flag is implemented in `unreleased partial`. 26 27 - This requires integrating with at Gitea/Forgejo, Github, Gitlab, and Tangled 28*/ 29package main 30 31import ( 32 "fmt" 33 "strings" 34 35 "github.com/go-git/go-git/v6" 36 "github.com/spf13/cobra" 37 "github.com/stormlightlabs/git-storm/internal/changeset" 38 "github.com/stormlightlabs/git-storm/internal/gitlog" 39 "github.com/stormlightlabs/git-storm/internal/style" 40) 41 42// checkCmd validates that all commits in a range have corresponding changelog entries. 43func checkCmd() *cobra.Command { 44 var sinceTag string 45 46 c := &cobra.Command{ 47 Use: "check [from] [to]", 48 Short: "Validate changelog entries exist for all commits", 49 Long: `Checks that all commits in the specified range have corresponding 50.changes/*.md entries. Useful for CI enforcement. 51 52Commits with [nochanges] or [skip changelog] in their message are skipped.`, 53 Args: cobra.MaximumNArgs(2), 54 RunE: func(cmd *cobra.Command, args []string) error { 55 var from, to string 56 57 if sinceTag != "" { 58 from = sinceTag 59 if len(args) > 0 { 60 to = args[0] 61 } else { 62 to = "HEAD" 63 } 64 } else if len(args) == 0 { 65 return fmt.Errorf("must specify either --since flag or [from] [to] arguments") 66 } else { 67 from, to = gitlog.ParseRefArgs(args) 68 } 69 70 repo, err := git.PlainOpen(repoPath) 71 if err != nil { 72 return fmt.Errorf("failed to open repository: %w", err) 73 } 74 75 commits, err := gitlog.GetCommitRange(repo, from, to) 76 if err != nil { 77 return err 78 } 79 80 if len(commits) == 0 { 81 style.Headlinef("No commits found between %s and %s", from, to) 82 return nil 83 } 84 85 changesDir := ".changes" 86 existingMetadata, err := changeset.LoadExistingMetadata(changesDir) 87 if err != nil { 88 return fmt.Errorf("failed to load existing metadata: %w", err) 89 } 90 91 style.Headlinef("Checking %d commits between %s and %s", len(commits), from, to) 92 style.Newline() 93 94 var missingEntries []string 95 skippedCount := 0 96 97 for _, commit := range commits { 98 message := strings.ToLower(commit.Message) 99 if strings.Contains(message, "[nochanges]") || strings.Contains(message, "[skip changelog]") { 100 skippedCount++ 101 continue 102 } 103 104 diffHash, err := changeset.ComputeDiffHash(commit) 105 if err != nil { 106 style.Println("Warning: failed to compute diff hash for commit %s: %v", commit.Hash.String()[:7], err) 107 continue 108 } 109 110 if _, exists := existingMetadata[diffHash]; !exists { 111 sha7 := commit.Hash.String()[:7] 112 subject := strings.Split(commit.Message, "\n")[0] 113 missingEntries = append(missingEntries, fmt.Sprintf("%s - %s", sha7, subject)) 114 } 115 } 116 117 if len(missingEntries) == 0 { 118 style.Addedf("✓ All commits have changelog entries") 119 if skippedCount > 0 { 120 style.Println(" Skipped %d commits with [nochanges] marker", skippedCount) 121 } 122 return nil 123 } 124 125 style.Println("%s", style.StyleRemoved.Render(fmt.Sprintf("✗ %d commits missing changelog entries:", len(missingEntries)))) 126 style.Newline() 127 128 for _, entry := range missingEntries { 129 style.Println(" - %s", entry) 130 } 131 132 style.Newline() 133 style.Println("To create entries, run:") 134 style.Println(" storm generate %s %s --interactive", from, to) 135 style.Println("Or manually create entries with:") 136 style.Println(" storm unreleased partial <commit-ref>") 137 138 return fmt.Errorf("changelog validation failed") 139 }, 140 } 141 142 c.Flags().StringVar(&sinceTag, "since", "", "Check changes since the given tag") 143 return c 144}