changelog generator & diff tool stormlightlabs.github.io/git-storm/
changelog changeset markdown golang git
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(unreleased): add 'partial' command for creating entries linked to specific commits

* implement changelog validation command for CI enforcement

+920 -32
+32 -29
ROADMAP.md
··· 19 19 - [x] `unreleased add` - Create new entry 20 20 - [x] `unreleased list` - Display entries (text and JSON) 21 21 - [x] `unreleased review` - Interactive TUI review 22 + - [x] `unreleased partial` - Create entry linked to specific commit 23 + - [x] Filename format: `<sha7>.<type>.md` 24 + - [x] Auto-detect type from conventional commit format 25 + - [x] Optional `--type`, `--scope`, `--summary` override flags 26 + - [ ] Optional `--issue` flag (TODO: see issue-linking task below) 22 27 - [ ] Implement delete action from review 23 28 - [ ] Implement edit action from review 24 - - [ ] Figure out how these fit in to `unreleased` 25 - - [ ] `storm partial create` - Create new partial file 26 - - [ ] Filename format: `<sha7>.<type>.md` 27 - - [ ] Supports configurable categories (feature, fix, doc, removal, etc.) 28 - - [ ] Optional `--type`, `--issue`, `--message` flags 29 - - [ ] `storm check` - Validate that changes include unreleased partials 30 - - [ ] Detect missing partials for changed code paths 31 - - [ ] Honor `[nochanges]` marker in commit messages 32 - - [ ] Exit non-zero for CI enforcement 29 + - [x] `storm check` - Validate that changes include unreleased partials 30 + - [x] Detect missing partials for changed code paths 31 + - [x] Honor `[nochanges]` and `[skip changelog]` markers in commit messages 32 + - [x] Exit non-zero for CI enforcement 33 + - [x] Support `--since` flag for checking since a tag 33 34 - [x] `storm release` - Promote unreleased changes to CHANGELOG 34 35 - [x] Read all `.changes/*.md` files 35 36 - [x] Merge into `CHANGELOG.md` ··· 38 39 - [x] Optional date override with `--date` flag 39 40 - [x] Generate GitHub comparison links automatically 40 41 - [x] Dry-run mode 41 - - [ ] Optional Git tag creation (Phase 7) 42 + - [x] Optional Git tag creation 42 43 - [x] `storm diff`: display inline diffs between refs with support for file filtering, 43 44 context expansion, and multiple view modes. 44 45 ··· 55 56 56 57 ## `.changes` Storage and Parsing 57 58 58 - Local storage for unreleased changelog entries. 59 - 60 - ### Tasks 61 - 62 - - [x] Define `Entry` struct with YAML frontmatter 63 - - [x] Implement `changeset.Write(dir, entry)` 64 - - [x] Generate unique filenames (timestamp-based) 65 - - [x] Write YAML frontmatter 66 - - [x] Create `.changes/` directory if missing 67 - - [x] Implement `changeset.List(dir)` 68 - - [x] Parse YAML frontmatter 69 - - [x] Return `EntryWithFile` structs 70 - - [x] Implement diff-based deduplication 71 - - [x] Compute diff hash for commits 72 - - [x] Load existing entries by hash 73 - - [x] Detect rebased commits (same diff, different hash) 74 - - [x] Update rebased commit metadata automatically 59 + - [x] Provides a local .changes/ store that writes and lists YAML-frontmatter entries, 60 + auto-creates the directory, and deduplicates rebased commits by diff hash to keep 61 + unreleased changelog items clean. 75 62 76 63 ## TUI 77 64 ··· 82 69 ## Keep a Changelog Writer 83 70 84 71 - [x] Adds a full changelog pipeline that parses the existing file, builds and writes 85 - new releases, and validates dates/sections to strictly match the Keep a Changelog 86 - [spec](https://keepachangelog.com/en/1.1.0/), including autogenerated comparison links. 72 + new releases, and validates dates/sections to strictly match the Keep a Changelog 73 + [spec](https://keepachangelog.com/en/1.1.0/), including autogenerated comparison links. 87 74 - [ ] Ensure deterministic sorting by category and filename timestamp 88 75 76 + ## Issue Linking 77 + 78 + Add support for linking changelog entries to issue/PR numbers. 79 + 80 + ### Tasks 81 + 82 + - [ ] Add `--issue` flag to `unreleased add` and `unreleased partial` 83 + - [ ] Add `issue` field to Entry struct 84 + - [ ] Include issue number in YAML frontmatter 85 + - [ ] Support issue validation in `check` command 86 + - [ ] Format issue links in generated CHANGELOG (e.g., #123, owner/repo#123) 87 + 89 88 ## Phase 7: Git Tagging and CI Integration 90 89 91 90 Repository tagging and automation-friendly features. ··· 97 96 - [x] Include release notes in tag message 98 97 - [x] Validate tag doesn't already exist 99 98 - [x] Support `--tag` flag 99 + - [x] Implement CI validation with `check` command 100 + - [x] Validate changelog entries exist for commits 101 + - [x] Honor `[nochanges]` markers 102 + - [x] Exit codes for CI integration 100 103 - [ ] Add JSON output modes for all commands 101 104 - [x] `unreleased list --json` 102 105 - [ ] `generate --output-json`
+144
cmd/check.go
··· 1 + /* 2 + USAGE 3 + 4 + storm check [from] [to] [options] 5 + 6 + FLAGS 7 + 8 + --since <tag> Check changes since the given tag 9 + --repo <path> Path to the Git repository (default: .) 10 + 11 + # DESCRIPTION 12 + 13 + Validates that all commits in the specified range have corresponding unreleased 14 + changelog entries. This is useful for CI enforcement to ensure developers 15 + document their changes. 16 + 17 + Commits containing [nochanges] or [skip changelog] in the message are skipped. 18 + 19 + Exit 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 + 25 + TODO(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 + */ 29 + package main 30 + 31 + import ( 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. 43 + func 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 + 52 + Commits 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 + }
+1 -1
cmd/main.go
··· 41 41 42 42 root.PersistentFlags().StringVar(&repoPath, "repo", ".", "Path to the Git repository") 43 43 root.PersistentFlags().StringVarP(&output, "output", "o", "CHANGELOG.md", "Output changelog file path") 44 - root.AddCommand(generateCmd(), unreleasedCmd(), releaseCmd(), diffCmd(), versionCmd()) 44 + root.AddCommand(generateCmd(), unreleasedCmd(), releaseCmd(), diffCmd(), checkCmd(), versionCmd()) 45 45 46 46 if err := fang.Execute(ctx, root, fang.WithColorSchemeFunc(style.NewColorScheme)); err != nil { 47 47 log.Fatalf("Execution failed: %v", err)
+100 -1
cmd/unreleased.go
··· 8 8 add Add a new unreleased change entry 9 9 list List all unreleased changes 10 10 review Review unreleased changes interactively 11 + partial Create entry linked to a specific commit 11 12 12 13 USAGE 13 14 ··· 37 38 38 39 --repo <path> Path to the repository (default: .) 39 40 --output <file> Optional file to export reviewed notes 41 + 42 + USAGE 43 + 44 + storm unreleased partial <commit-ref> [options] 45 + 46 + FLAGS 47 + 48 + --type <type> Override change type (auto-detected from commit message) 49 + --summary <text> Override summary (auto-detected from commit message) 50 + --scope <scope> Optional subsystem or module name 51 + --repo <path> Path to the repository (default: .) 40 52 */ 41 53 package main 42 54 ··· 47 59 "strings" 48 60 49 61 tea "github.com/charmbracelet/bubbletea" 62 + "github.com/go-git/go-git/v6" 63 + "github.com/go-git/go-git/v6/plumbing" 50 64 "github.com/spf13/cobra" 51 65 "github.com/stormlightlabs/git-storm/internal/changeset" 66 + "github.com/stormlightlabs/git-storm/internal/gitlog" 52 67 "github.com/stormlightlabs/git-storm/internal/style" 53 68 "github.com/stormlightlabs/git-storm/internal/ui" 54 69 ) ··· 186 201 }, 187 202 } 188 203 204 + partial := &cobra.Command{ 205 + Use: "partial <commit-ref>", 206 + Short: "Create entry linked to a specific commit", 207 + Long: `Creates a new .changes/<sha7>.<type>.md file based on the specified commit. 208 + Auto-detects type and summary from conventional commit format, with optional overrides.`, 209 + Args: cobra.ExactArgs(1), 210 + RunE: func(cmd *cobra.Command, args []string) error { 211 + commitRef := args[0] 212 + 213 + repo, err := git.PlainOpen(repoPath) 214 + if err != nil { 215 + return fmt.Errorf("failed to open repository: %w", err) 216 + } 217 + 218 + hash, err := repo.ResolveRevision(plumbing.Revision(commitRef)) 219 + if err != nil { 220 + return fmt.Errorf("failed to resolve commit ref %q: %w", commitRef, err) 221 + } 222 + 223 + commit, err := repo.CommitObject(*hash) 224 + if err != nil { 225 + return fmt.Errorf("failed to get commit object: %w", err) 226 + } 227 + 228 + parser := &gitlog.ConventionalParser{} 229 + subject := commit.Message 230 + body := "" 231 + lines := strings.Split(commit.Message, "\n") 232 + if len(lines) > 0 { 233 + subject = lines[0] 234 + if len(lines) > 1 { 235 + body = strings.Join(lines[1:], "\n") 236 + } 237 + } 238 + 239 + meta, err := parser.Parse(hash.String(), subject, body, commit.Author.When) 240 + if err != nil { 241 + return fmt.Errorf("failed to parse commit message: %w", err) 242 + } 243 + 244 + category := parser.Categorize(meta) 245 + 246 + if changeType != "" { 247 + if !slices.Contains(validTypes, changeType) { 248 + return fmt.Errorf("invalid type %q: must be one of %s", changeType, strings.Join(validTypes, ", ")) 249 + } 250 + category = changeType 251 + } else if category == "" { 252 + return fmt.Errorf("could not auto-detect change type from commit message, please specify --type") 253 + } 254 + 255 + entrySummary := meta.Description 256 + if summary != "" { 257 + entrySummary = summary 258 + } 259 + 260 + if scope != "" { 261 + meta.Scope = scope 262 + } 263 + 264 + sha7 := hash.String()[:7] 265 + filename := fmt.Sprintf("%s.%s.md", sha7, category) 266 + filePath := changesDir + "/" + filename 267 + 268 + entry := changeset.Entry{ 269 + Type: category, 270 + Scope: meta.Scope, 271 + Summary: entrySummary, 272 + Breaking: meta.Breaking, 273 + CommitHash: hash.String(), 274 + } 275 + 276 + if _, err := changeset.WritePartial(changesDir, filename, entry); err != nil { 277 + return fmt.Errorf("failed to create changelog entry: %w", err) 278 + } 279 + 280 + style.Addedf("Created %s", filePath) 281 + return nil 282 + }, 283 + } 284 + partial.Flags().StringVar(&changeType, "type", "", "Override change type (auto-detected from commit)") 285 + partial.Flags().StringVar(&scope, "scope", "", "Optional scope or subsystem name") 286 + partial.Flags().StringVar(&summary, "summary", "", "Override summary (auto-detected from commit)") 287 + 189 288 root := &cobra.Command{ 190 289 Use: "unreleased", 191 290 Short: "Manage unreleased changes (.changes directory)", 192 291 Long: `Work with unreleased change notes. Supports adding, listing, 193 292 and reviewing pending entries before release.`, 194 293 } 195 - root.AddCommand(add, list, review) 294 + root.AddCommand(add, list, review, partial) 196 295 return root 197 296 } 198 297
+29 -1
internal/changeset/changeset.go
··· 70 70 type Metadata struct { 71 71 CommitHash string `json:"commit_hash"` // current commit hash 72 72 DiffHash string `json:"diff_hash"` // stable diff content hash 73 + Filename string `json:"filename"` // relative path to .md file 73 74 Type string `json:"type"` 74 75 Scope string `json:"scope"` 75 76 Summary string `json:"summary"` 76 77 Breaking bool `json:"breaking"` 77 78 Author string `json:"author"` 78 79 Date time.Time `json:"date"` 79 - Filename string `json:"filename"` // relative path to .md file 80 80 } 81 81 82 82 // Write creates a new .changes/<timestamp>-<slug>.md file with YAML frontmatter. ··· 99 99 filename = fmt.Sprintf("%s-%s-%d.md", timestamp, slug, counter) 100 100 filePath = filepath.Join(dir, filename) 101 101 counter++ 102 + } 103 + 104 + yamlBytes, err := yaml.Marshal(entry) 105 + if err != nil { 106 + return "", fmt.Errorf("failed to marshal entry to YAML: %w", err) 107 + } 108 + 109 + content := fmt.Sprintf("---\n%s---\n", string(yamlBytes)) 110 + 111 + if err := os.WriteFile(filePath, []byte(content), 0644); err != nil { 112 + return "", fmt.Errorf("failed to write file %s: %w", filePath, err) 113 + } 114 + 115 + return filePath, nil 116 + } 117 + 118 + // WritePartial creates a .changes/<filename> file with the specified name and YAML frontmatter. 119 + // This is used by the `unreleased partial` command to create entries with commit-hash based names. 120 + // Creates the .changes directory if it doesn't exist. 121 + func WritePartial(dir string, filename string, entry Entry) (string, error) { 122 + if err := os.MkdirAll(dir, 0755); err != nil { 123 + return "", fmt.Errorf("failed to create directory %s: %w", dir, err) 124 + } 125 + 126 + filePath := filepath.Join(dir, filename) 127 + 128 + if _, err := os.Stat(filePath); err == nil { 129 + return "", fmt.Errorf("file %s already exists", filename) 102 130 } 103 131 104 132 yamlBytes, err := yaml.Marshal(entry)
+103
internal/changeset/changeset_test.go
··· 513 513 } 514 514 } 515 515 } 516 + 517 + func TestWritePartial(t *testing.T) { 518 + tmpDir := t.TempDir() 519 + 520 + tests := []struct { 521 + name string 522 + filename string 523 + entry Entry 524 + wantErr bool 525 + wantType string 526 + wantSummary string 527 + }{ 528 + { 529 + name: "basic partial entry", 530 + filename: "abc1234.added.md", 531 + entry: Entry{ 532 + Type: "added", 533 + Scope: "cli", 534 + Summary: "Add feature", 535 + CommitHash: "abc123def456", 536 + }, 537 + wantErr: false, 538 + wantType: "added", 539 + wantSummary: "Add feature", 540 + }, 541 + { 542 + name: "partial with different type", 543 + filename: "def5678.fixed.md", 544 + entry: Entry{ 545 + Type: "fixed", 546 + Summary: "Fix bug", 547 + CommitHash: "def5678abc", 548 + }, 549 + wantErr: false, 550 + wantType: "fixed", 551 + wantSummary: "Fix bug", 552 + }, 553 + } 554 + 555 + for _, tt := range tests { 556 + t.Run(tt.name, func(t *testing.T) { 557 + filePath, err := WritePartial(tmpDir, tt.filename, tt.entry) 558 + if (err != nil) != tt.wantErr { 559 + t.Fatalf("WritePartial() error = %v, wantErr %v", err, tt.wantErr) 560 + } 561 + 562 + if tt.wantErr { 563 + return 564 + } 565 + 566 + expectedPath := filepath.Join(tmpDir, tt.filename) 567 + testutils.Expect.Equal(t, filePath, expectedPath, "File path should match expected") 568 + 569 + if _, err := os.Stat(filePath); os.IsNotExist(err) { 570 + t.Errorf("File was not created: %s", filePath) 571 + } 572 + 573 + content, err := os.ReadFile(filePath) 574 + if err != nil { 575 + t.Fatalf("Failed to read file: %v", err) 576 + } 577 + 578 + parts := strings.SplitN(string(content), "---\n", 3) 579 + if len(parts) < 3 { 580 + t.Fatal("Invalid YAML frontmatter format") 581 + } 582 + 583 + var parsed Entry 584 + if err := yaml.Unmarshal([]byte(parts[1]), &parsed); err != nil { 585 + t.Fatalf("Failed to parse YAML: %v", err) 586 + } 587 + 588 + testutils.Expect.Equal(t, parsed.Type, tt.wantType) 589 + testutils.Expect.Equal(t, parsed.Summary, tt.wantSummary) 590 + testutils.Expect.Equal(t, parsed.CommitHash, tt.entry.CommitHash) 591 + }) 592 + } 593 + } 594 + 595 + func TestWritePartial_DuplicateFilename(t *testing.T) { 596 + tmpDir := t.TempDir() 597 + 598 + filename := "abc1234.added.md" 599 + entry := Entry{ 600 + Type: "added", 601 + Summary: "Test feature", 602 + CommitHash: "abc1234", 603 + } 604 + 605 + _, err := WritePartial(tmpDir, filename, entry) 606 + if err != nil { 607 + t.Fatalf("First WritePartial() error = %v", err) 608 + } 609 + 610 + _, err = WritePartial(tmpDir, filename, entry) 611 + if err == nil { 612 + t.Error("Expected error when writing duplicate filename, got nil") 613 + } 614 + 615 + if !strings.Contains(err.Error(), "already exists") { 616 + t.Errorf("Expected 'already exists' error, got: %v", err) 617 + } 618 + }
+423
internal/docs/README.md
··· 1 + --- 2 + title: Testing Workflow 3 + updated: 2025-01-08 4 + version: 1 5 + --- 6 + 7 + "Ride the lightning." 8 + 9 + This document provides a comprehensive testing workflow for the `storm` changelog manager. 10 + All tests should be run within this repository to validate functionality against real Git history. 11 + 12 + ## Setup 13 + 14 + ```bash 15 + # Build the CLI 16 + task build 17 + ``` 18 + 19 + ## Core Workflow 20 + 21 + ### Manual Entry Creation (`unreleased add`) 22 + 23 + Create entries manually without linking to commits. 24 + 25 + #### Basic entry creation 26 + 27 + ```bash 28 + storm unreleased add --type added --summary "Test manual entry" 29 + ``` 30 + 31 + **Expected:** 32 + 33 + - Creates `.changes/<timestamp>-test-manual-entry.md` 34 + - File contains YAML frontmatter with type and summary 35 + - Styled success message displays created file path 36 + 37 + #### Entry with scope 38 + 39 + ```bash 40 + storm unreleased add --type fixed --scope api --summary "Fix authentication bug" 41 + ``` 42 + 43 + **Expected:** 44 + 45 + - Includes `scope: api` in frontmatter 46 + - Filename slugifies to `...-fix-authentication-bug.md` 47 + 48 + #### Collision handling 49 + 50 + ```bash 51 + # Run same command twice rapidly 52 + storm unreleased add --type added --summary "Duplicate test" 53 + storm unreleased add --type added --summary "Duplicate test" 54 + ``` 55 + 56 + **Expected:** 57 + 58 + - Two different files created (second has `-1` suffix) 59 + - Both files exist and are readable 60 + 61 + **Edge Cases:** 62 + 63 + - Invalid type (should error with helpful message) 64 + - Missing required flags (should error) 65 + - Very long summary (should truncate to 50 chars) 66 + - Special characters in summary (should slugify correctly) 67 + - Empty summary (should error) 68 + 69 + ### Commit-Linked Entry Creation (`unreleased partial`) 70 + 71 + Create entries linked to specific commits with auto-detection. 72 + 73 + #### Basic partial from commit 74 + 75 + ```bash 76 + # Use a recent commit hash 77 + storm unreleased partial HEAD 78 + ``` 79 + 80 + **Expected:** 81 + 82 + - Auto-detects type from conventional commit format 83 + - Creates `.changes/<sha7>.<type>.md` 84 + - Includes `commit_hash` in frontmatter 85 + - Shows styled success message 86 + 87 + #### Override auto-detection 88 + 89 + ```bash 90 + storm unreleased partial HEAD~1 --type fixed --summary "Custom summary" 91 + ``` 92 + 93 + **Expected:** 94 + 95 + - Uses provided type instead of auto-detected 96 + - Uses custom summary 97 + - Preserves commit hash in frontmatter 98 + 99 + #### Non-conventional commit 100 + 101 + ```bash 102 + # Try a commit without conventional format 103 + storm unreleased partial <old-commit> 104 + ``` 105 + 106 + **Expected:** 107 + 108 + - Error message: "could not auto-detect change type" 109 + - Suggests using `--type` flag 110 + 111 + #### Duplicate prevention 112 + 113 + ```bash 114 + storm unreleased partial HEAD 115 + storm unreleased partial HEAD # Run again 116 + ``` 117 + 118 + **Expected:** 119 + 120 + - Second command fails with "file already exists" error 121 + 122 + **Edge Cases:** 123 + 124 + - Invalid commit ref (should error) 125 + - Merge commit (should handle gracefully) 126 + - Initial commit with no parent (should work) 127 + - Commit with multi-line message (should parse correctly) 128 + - Commit with breaking change marker (should set `breaking: true`) 129 + 130 + ### Listing Entries (`unreleased list`) 131 + 132 + Display all unreleased changes. 133 + 134 + #### Text output 135 + 136 + ```bash 137 + storm unreleased list 138 + ``` 139 + 140 + **Expected:** 141 + 142 + - Color-coded type labels ([added], [fixed], etc.) 143 + - Shows scope if present 144 + - Displays filename 145 + - Shows breaking change indicator if applicable 146 + - Empty state message if no entries 147 + 148 + #### JSON output 149 + 150 + ```bash 151 + storm unreleased list --json 152 + ``` 153 + 154 + **Expected:** 155 + 156 + - Valid JSON array 157 + - Each entry has type, scope, summary, filename 158 + - Can be piped to `jq` for processing 159 + 160 + **Edge Cases:** 161 + 162 + - Empty `.changes/` directory 163 + - Malformed YAML in entry file 164 + - Mixed entry types (manual + partial) 165 + 166 + ### Generating Entries from Git History (`generate`) 167 + 168 + Scan commit ranges and create changelog entries. 169 + 170 + #### Range generation 171 + 172 + ```bash 173 + # Generate from last 5 commits 174 + storm generate HEAD~5 HEAD 175 + ``` 176 + 177 + **Expected:** 178 + 179 + - Lists N commits found 180 + - Creates entries for conventional commits 181 + - Skips non-conventional commits 182 + - Shows created count and skipped count 183 + - Uses diff-based deduplication 184 + 185 + #### Interactive selection 186 + 187 + ```bash 188 + storm generate HEAD~10 HEAD --interactive 189 + ``` 190 + 191 + **Expected:** 192 + 193 + - Launches TUI with commit list 194 + - Shows parsed metadata (type, scope, summary) 195 + - Allows selection/deselection 196 + - Creates only selected entries 197 + - Handles cancellation (Ctrl+C) 198 + 199 + #### Since tag 200 + 201 + ```bash 202 + storm generate --since v0.1.0 203 + ``` 204 + 205 + **Expected:** 206 + 207 + - Generates entries from v0.1.0 to HEAD 208 + - Auto-detects tag as starting point 209 + 210 + #### Deduplication 211 + 212 + ```bash 213 + storm generate HEAD~3 HEAD 214 + storm generate HEAD~3 HEAD # Run again 215 + ``` 216 + 217 + **Expected:** 218 + 219 + - First run creates N entries 220 + - Second run shows "Skipped N duplicates" 221 + - No duplicate files created 222 + 223 + #### Rebased commits 224 + 225 + ```bash 226 + # Simulate rebase by checking metadata 227 + storm generate <range-with-rebased-commits> 228 + ``` 229 + 230 + **Expected:** 231 + 232 + - Detects same diff, different commit hash 233 + - Updates metadata with new commit hash 234 + - Shows "Updated N rebased commits" 235 + 236 + **Edge Cases:** 237 + 238 + - No commits in range (should show "No commits found") 239 + - Range with only merge commits 240 + - Range with revert commits (should skip) 241 + - Commits with `[nochanges]` marker (should skip) 242 + - Non-existent refs (should error) 243 + 244 + ### Reviewing Entries (`unreleased review`) 245 + 246 + Interactive TUI for reviewing unreleased changes. 247 + 248 + #### Basic review 249 + 250 + ```bash 251 + storm unreleased review 252 + ``` 253 + 254 + **Expected:** 255 + 256 + - Launches TUI with list of entries 257 + - Shows entry details on selection 258 + - Keyboard navigation works (j/k or arrows) 259 + - Can mark for delete/edit (not yet implemented) 260 + - Exit with q or ESC 261 + 262 + **Edge Cases:** 263 + 264 + - Empty changes directory (should show message, not crash) 265 + - Corrupted entry file (should handle gracefully) 266 + - Non-TTY environment (should detect and warn) 267 + 268 + ### CI Validation (`check`) 269 + 270 + Validate that commits have changelog entries. 271 + 272 + #### All commits documented 273 + 274 + ```bash 275 + # After running generate for a range 276 + storm check HEAD~5 HEAD 277 + ``` 278 + 279 + **Expected:** 280 + 281 + - Shows "✓ All commits have changelog entries" 282 + - Exit code 0 283 + 284 + #### Missing entries 285 + 286 + ```bash 287 + # Create new commits without entries 288 + git commit --allow-empty -m "feat: undocumented feature" 289 + storm check HEAD~1 HEAD 290 + ``` 291 + 292 + **Expected:** 293 + 294 + - Shows "✗ N commits missing changelog entries" 295 + - Lists missing commit SHAs and subjects 296 + - Suggests commands to fix 297 + - Exit code 1 298 + 299 + #### Skip markers 300 + 301 + ```bash 302 + git commit --allow-empty -m "chore: update deps [nochanges]" 303 + storm check HEAD~1 HEAD 304 + ``` 305 + 306 + **Expected:** 307 + 308 + - Skips commit with marker 309 + - Shows "Skipped N commits with [nochanges] marker" 310 + - Exit code 0 311 + 312 + #### Since tag 313 + 314 + ```bash 315 + storm check --since v0.1.0 316 + ``` 317 + 318 + **Expected:** 319 + 320 + - Checks all commits since tag 321 + - Reports missing entries 322 + 323 + **Edge Cases:** 324 + 325 + - Empty commit range (should succeed with 0 checks) 326 + - Range with all skipped commits 327 + - Invalid tag/ref (should error) 328 + 329 + ### Release Generation (`release`) 330 + 331 + Promote unreleased changes to CHANGELOG. 332 + 333 + #### Basic release 334 + 335 + ```bash 336 + storm release --version 1.2.0 337 + ``` 338 + 339 + **Expected:** 340 + 341 + - Creates/updates CHANGELOG.md 342 + - Adds version header with date 343 + - Groups entries by type (Added, Changed, Fixed, etc.) 344 + - Maintains Keep a Changelog format 345 + - Preserves existing changelog content 346 + 347 + #### Dry run 348 + 349 + ```bash 350 + storm release --version 1.2.0 --dry-run 351 + ``` 352 + 353 + **Expected:** 354 + 355 + - Shows preview of changes 356 + - No files modified 357 + - Styled output shows what would be written 358 + 359 + #### Clear changes 360 + 361 + ```bash 362 + storm release --version 1.2.0 --clear-changes 363 + ``` 364 + 365 + **Expected:** 366 + 367 + - Moves entries from `.changes/` to CHANGELOG 368 + - Deletes `.changes/*.md` files after release 369 + - Keeps `.changes/data/` metadata 370 + 371 + #### Git tagging 372 + 373 + ```bash 374 + storm release --version 1.2.0 --tag 375 + ``` 376 + 377 + **Expected:** 378 + 379 + - Creates annotated Git tag `v1.2.0` 380 + - Includes release notes in tag message 381 + - Validates tag doesn't exist 382 + 383 + **Edge Cases:** 384 + 385 + - No unreleased entries (should warn) 386 + - Existing version in CHANGELOG (should append) 387 + - Malformed CHANGELOG.md (should handle) 388 + - Tag already exists (should error) 389 + - Custom date format with `--date` 390 + 391 + ### Diff Viewing (`diff`) 392 + 393 + Display inline diffs between refs. 394 + 395 + #### Basic diff 396 + 397 + ```bash 398 + storm diff HEAD~1 HEAD 399 + ``` 400 + 401 + **Expected:** 402 + 403 + - Shows unified diff with syntax highlighting 404 + - Iceberg theme colors 405 + - Context lines displayed 406 + - File headers shown 407 + 408 + #### File filtering 409 + 410 + ```bash 411 + storm diff HEAD~1 HEAD -- "*.go" 412 + ``` 413 + 414 + **Expected:** 415 + 416 + - Shows only Go file changes 417 + - Respects glob patterns 418 + 419 + **Edge Cases:** 420 + 421 + - No changes between refs 422 + - Binary files (should indicate) 423 + - Large diffs (should handle gracefully)
+88
internal/docs/e2e/README.md
··· 1 + --- 2 + title: Integration Testing Scenarios 3 + updated: 2025-11-08 4 + version: 1 5 + --- 6 + 7 + ## Feature Branch 8 + 9 + ```bash 10 + # 1. Create feature branch 11 + git checkout -b feature/new-auth 12 + 13 + # 2. Make commits 14 + git commit -m "feat(auth): add OAuth support" 15 + git commit -m "test(auth): add OAuth tests" 16 + 17 + # 3. Generate entries interactively 18 + storm generate main HEAD --interactive 19 + 20 + # 4. Validate all documented 21 + storm check main HEAD 22 + 23 + # 5. Review entries 24 + storm unreleased list 25 + 26 + # Expected: 2 entries created, check passes 27 + ``` 28 + 29 + ## Release Preparation 30 + 31 + ```bash 32 + # 1. Generate from last release 33 + storm generate --since v1.0.0 34 + 35 + # 2. Review what was generated 36 + storm unreleased review 37 + 38 + # 3. Add manual entry for non-code change 39 + storm unreleased add --type changed --summary "Updated documentation" 40 + 41 + # 4. Dry-run release 42 + storm release --version 1.1.0 --dry-run 43 + 44 + # 5. Execute release with tag 45 + storm release --version 1.1.0 --tag --clear-changes 46 + 47 + # 6. Verify 48 + git tag -n9 v1.1.0 49 + cat CHANGELOG.md 50 + 51 + # Expected: Clean CHANGELOG, annotated tag, empty .changes/ 52 + ``` 53 + 54 + ## CI Pipeline Validation 55 + 56 + ```bash 57 + # 1. Simulate PR with new commits 58 + git checkout -b pr/fix-bug 59 + git commit -m "fix(api): resolve rate limit bug" 60 + 61 + # 2. CI check (should fail) 62 + storm check main HEAD 63 + # Exit code: 1 64 + 65 + # 3. Create entry 66 + storm unreleased partial HEAD 67 + 68 + # 4. CI check (should pass) 69 + storm check main HEAD 70 + # Exit code: 0 71 + 72 + # Expected: PR can be merged with confidence 73 + ``` 74 + 75 + ## Rebase Handling 76 + 77 + ```bash 78 + # 1. Create entries for commits 79 + storm generate HEAD~3 HEAD 80 + 81 + # 2. Rebase interactively (squash/reword) 82 + git rebase -i HEAD~3 83 + 84 + # 3. Regenerate (should detect rebased commits) 85 + storm generate HEAD~2 HEAD 86 + 87 + # Expected: Metadata updated, no duplicates 88 + ```