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: test utilities & basic types

+686 -48
+85 -27
README.md
··· 9 9 - Provide a terminal UI for reviewing commits and changes interactively. 10 10 - Generate Markdown in strict [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) format. 11 11 12 - ## Design Overview 12 + ### Architecture 13 13 14 - ### Core Packages 14 + - **Git integration:** Uses `go-git` for commit history and tag resolution — no shell calls. 15 + - **Diffing:** Custom lightweight diff engine for readable line-by-line output. 16 + - **Unreleased storage:** Simple Markdown files with YAML frontmatter (no external formats). 17 + - **Interactive mode:** Bubble Tea model for categorizing and confirming changes. 18 + - **Output:** Always produces Keep a Changelog–compliant Markdown. 19 + 20 + ## Core Packages 15 21 16 22 ```sh 17 23 . ··· 55 61 56 62 Merges `.changes/*.md` into the changelog, writes a new section, and optionally tags the repository. 57 63 58 - ## Architecture 59 - 60 - - **Git integration:** Uses `go-git` for commit history and tag resolution — no shell calls. 61 - - **Diffing:** Custom lightweight diff engine for readable line-by-line output. 62 - - **Unreleased storage:** Simple Markdown files with YAML frontmatter (no external formats). 63 - - **Interactive mode:** Bubble Tea model for categorizing and confirming changes. 64 - - **Output:** Always produces Keep a Changelog–compliant Markdown. 65 - 66 64 ## Development Guidance 67 65 68 66 1. Composable 69 67 Each subsystem (`diff`, `gitlog`, `tui`, etc.) should work standalone and be callable from tests or other Go programs. 70 68 2. Frontmatter 71 69 72 - ```yaml 73 - type: added 74 - scope: cli 75 - summary: Add changelog command 76 - ``` 70 + ```yaml 71 + type: added 72 + scope: cli 73 + summary: Add changelog command 74 + ``` 77 75 78 76 3. Consistent Palette 79 77 80 - | Type | Color | 81 - | -------- | --------- | 82 - | Added | `#10b981` | 83 - | Changed | `#0ea5e9` | 84 - | Fixed | `#f43f5e` | 85 - | Removed | `#f59e0b` | 86 - | Security | `#9333ea` | 78 + | Type | Color | 79 + | -------- | --------- | 80 + | Added | `#10b981` | 81 + | Changed | `#0ea5e9` | 82 + | Fixed | `#f43f5e` | 83 + | Removed | `#f59e0b` | 84 + | Security | `#9333ea` | 87 85 88 86 4. Commands should chain naturally and script cleanly: 89 87 90 - ```sh 91 - storm unreleased list --json 92 - storm generate --since v1.2.0 --interactive 93 - storm release --version 1.3.0 94 - ``` 88 + ```sh 89 + storm unreleased list --json 90 + storm generate --since v1.2.0 --interactive 91 + storm release --version 1.3.0 92 + ``` 95 93 96 94 5. Tests 97 95 - Research testing bubbletea programs ··· 115 113 - No external dependencies beyond `cobra`, `go-git`, `bubbletea`, `lipgloss`, and `yaml.v3`. 116 114 - Keep the workflow simple and reproducible so changelogs can be deterministically derived from local data. 117 115 - Make sure interactive sessions degrade gracefully in non-TTY environments. 116 + 117 + ## Conventional Commits 118 + 119 + ### Structure 120 + 121 + | Element | Format | Description | 122 + | ------------------------- | ---------------------------------------- | ---------------------------------------- | 123 + | Header | `<type>(<scope>): <description>` | The main commit message line. | 124 + | Scope | Optional, e.g. `api`, `cli`, `deps` | Indicates part of the codebase affected. | 125 + | Breaking Change Indicator | `!` after type/scope, e.g. `feat(api)!:` | Marks a breaking API change. | 126 + | Body | (Optional) one blank line then body text | Explanation of what & why. | 127 + | Footer | (Optional) one blank line then meta info | Issue refs, `BREAKING CHANGE: …`, etc | 128 + 129 + ### Types 130 + 131 + | Type | Description | 132 + | ---------- | ----------------------------------------------------------------------- | 133 + | `feat` | A new feature. | 134 + | `fix` | A bug fix. | 135 + | `docs` | Documentation only changes. | 136 + | `style` | Code style changes (formatting, whitespace) that don’t affect behavior. | 137 + | `refactor` | Code changes that neither fix a bug nor add a feature. | 138 + | `perf` | Performance improvements. | 139 + | `test` | Adding or updating tests. | 140 + | `build` | Changes that affect the build system or dependencies. | 141 + | `ci` | Changes to CI configuration and scripts. | 142 + | `chore` | Other changes that don’t touch src/test (e.g., tooling, config). | 143 + | `revert` | Reverts a previous commit. | 144 + 145 + ### Examples 146 + 147 + ```text 148 + feat(api): add pagination endpoint 149 + 150 + fix(ui): correct button alignment issue 151 + 152 + docs: update README installation instructions 153 + 154 + perf(core): optimize user query performance 155 + 156 + refactor: restructure payment module for clarity 157 + 158 + style: apply consistent formatting 159 + 160 + test(auth): add integration tests for OAuth flow 161 + 162 + build(deps): bump dependencies to latest versions 163 + 164 + ci: add GitHub Actions workflow for CI 165 + 166 + chore: update .gitignore and clean up obsolete files 167 + 168 + feat(api)!: remove support for legacy endpoints 169 + 170 + BREAKING CHANGE: API no longer accepts XML-formatted requests. 171 + ``` 172 + 173 + ### Reference 174 + 175 + <https://www.conventionalcommits.org/en/v1.0.0/> "Conventional Commits"
+18 -6
go.mod
··· 7 7 github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 8 8 github.com/charmbracelet/log v0.4.2 9 9 github.com/go-git/go-git/v6 v6.0.0-20251103200709-47b1ed2930c9 10 - github.com/spf13/cobra v1.9.1 10 + github.com/spf13/cobra v1.10.1 11 + ) 12 + 13 + require ( 14 + github.com/clipperhouse/displaywidth v0.4.1 // indirect 15 + github.com/clipperhouse/stringish v0.1.1 // indirect 16 + github.com/clipperhouse/uax29/v2 v2.3.0 // indirect 11 17 ) 12 18 13 19 require ( 14 20 github.com/Microsoft/go-winio v0.6.2 // indirect 15 21 github.com/ProtonMail/go-crypto v1.3.0 // indirect 16 22 github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect 17 - github.com/charmbracelet/colorprofile v0.3.2 // indirect 23 + github.com/aymanbagabas/go-udiff v0.3.1 // indirect 24 + github.com/charmbracelet/bubbletea v1.3.10 25 + github.com/charmbracelet/colorprofile v0.3.3 // indirect 18 26 github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3.0.20250917201909-41ff0bf215ea // indirect 19 27 github.com/charmbracelet/ultraviolet v0.0.0-20250915111650-81d4262876ef // indirect 20 - github.com/charmbracelet/x/ansi v0.10.1 // indirect 28 + github.com/charmbracelet/x/ansi v0.10.3 // indirect 21 29 github.com/charmbracelet/x/cellbuf v0.0.13 // indirect 22 30 github.com/charmbracelet/x/exp/charmtone v0.0.0-20250603201427-c31516f43444 // indirect 23 31 github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 // indirect 24 - github.com/charmbracelet/x/term v0.2.1 // indirect 32 + github.com/charmbracelet/x/exp/teatest v0.0.0-20251103210727-681bf553bc2e 33 + github.com/charmbracelet/x/term v0.2.2 // indirect 25 34 github.com/charmbracelet/x/termios v0.1.1 // indirect 26 35 github.com/charmbracelet/x/windows v0.2.2 // indirect 27 36 github.com/cloudflare/circl v1.6.1 // indirect 28 37 github.com/cyphar/filepath-securejoin v0.5.0 // indirect 29 38 github.com/emirpasic/gods v1.18.1 // indirect 39 + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect 30 40 github.com/go-git/gcfg/v2 v2.0.2 // indirect 31 41 github.com/go-git/go-billy/v6 v6.0.0-20251022185412-61e52df296a5 // indirect 32 42 github.com/go-logfmt/logfmt v0.6.0 // indirect ··· 36 46 github.com/klauspost/cpuid/v2 v2.3.0 // indirect 37 47 github.com/lucasb-eyer/go-colorful v1.3.0 // indirect 38 48 github.com/mattn/go-isatty v0.0.20 // indirect 39 - github.com/mattn/go-runewidth v0.0.16 // indirect 49 + github.com/mattn/go-localereader v0.0.1 // indirect 50 + github.com/mattn/go-runewidth v0.0.19 // indirect 51 + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect 40 52 github.com/muesli/cancelreader v0.2.2 // indirect 41 53 github.com/muesli/mango v0.1.0 // indirect 42 54 github.com/muesli/mango-cobra v1.2.0 // indirect ··· 46 58 github.com/pjbgf/sha1cd v0.5.0 // indirect 47 59 github.com/rivo/uniseg v0.4.7 // indirect 48 60 github.com/sergi/go-diff v1.4.0 // indirect 49 - github.com/spf13/pflag v1.0.6 // indirect 61 + github.com/spf13/pflag v1.0.9 // indirect 50 62 github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect 51 63 golang.org/x/crypto v0.43.0 // indirect 52 64 golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
+31 -15
go.sum
··· 8 8 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= 9 9 github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= 10 10 github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= 11 - github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= 12 - github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= 13 - github.com/charmbracelet/colorprofile v0.3.2 h1:9J27WdztfJQVAQKX2WOlSSRB+5gaKqqITmrvb1uTIiI= 14 - github.com/charmbracelet/colorprofile v0.3.2/go.mod h1:mTD5XzNeWHj8oqHb+S1bssQb7vIHbepiebQ2kPKVKbI= 11 + github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY= 12 + github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E= 13 + github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= 14 + github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= 15 + github.com/charmbracelet/colorprofile v0.3.3 h1:DjJzJtLP6/NZ8p7Cgjno0CKGr7wwRJGxWUwh2IyhfAI= 16 + github.com/charmbracelet/colorprofile v0.3.3/go.mod h1:nB1FugsAbzq284eJcjfah2nhdSLppN2NqvfotkfRYP4= 15 17 github.com/charmbracelet/fang v0.4.3 h1:qXeMxnL4H6mSKBUhDefHu8NfikFbP/MBNTfqTrXvzmY= 16 18 github.com/charmbracelet/fang v0.4.3/go.mod h1:wHJKQYO5ReYsxx+yZl+skDtrlKO/4LLEQ6EXsdHhRhg= 17 19 github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE= ··· 22 24 github.com/charmbracelet/log v0.4.2/go.mod h1:qifHGX/tc7eluv2R6pWIpyHDDrrb/AG71Pf2ysQu5nw= 23 25 github.com/charmbracelet/ultraviolet v0.0.0-20250915111650-81d4262876ef h1:VrWaUi2LXYLjfjCHowdSOEc6dQ9Ro14KY7Bw4IWd19M= 24 26 github.com/charmbracelet/ultraviolet v0.0.0-20250915111650-81d4262876ef/go.mod h1:AThRsQH1t+dfyOKIwXRoJBniYFQUkUpQq4paheHMc2o= 25 - github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ= 26 - github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= 27 + github.com/charmbracelet/x/ansi v0.10.3 h1:3WoV9XN8uMEnFRZZ+vBPRy59TaIWa+gJodS4Vg5Fut0= 28 + github.com/charmbracelet/x/ansi v0.10.3/go.mod h1:uQt8bOrq/xgXjlGcFMc8U2WYbnxyjrKhnvTQluvfCaE= 27 29 github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k= 28 30 github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= 29 31 github.com/charmbracelet/x/exp/charmtone v0.0.0-20250603201427-c31516f43444 h1:IJDiTgVE56gkAGfq0lBEloWgkXMk4hl/bmuPoicI4R0= 30 32 github.com/charmbracelet/x/exp/charmtone v0.0.0-20250603201427-c31516f43444/go.mod h1:T9jr8CzFpjhFVHjNjKwbAD7KwBNyFnj2pntAO7F2zw0= 31 33 github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ= 32 34 github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= 33 - github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= 34 - github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= 35 + github.com/charmbracelet/x/exp/teatest v0.0.0-20251103210727-681bf553bc2e h1:8hEJFkBwIvEVQTfwsFF3fmCJSqibJT7NvQsGLbZzfkU= 36 + github.com/charmbracelet/x/exp/teatest v0.0.0-20251103210727-681bf553bc2e/go.mod h1:aPVjFrBwbJgj5Qz1F0IXsnbcOVJcMKgu1ySUfTAxh7k= 37 + github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= 38 + github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= 35 39 github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY= 36 40 github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo= 37 41 github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM= 38 42 github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k= 43 + github.com/clipperhouse/displaywidth v0.4.1 h1:uVw9V8UDfnggg3K2U84VWY1YLQ/x2aKSCtkRyYozfoU= 44 + github.com/clipperhouse/displaywidth v0.4.1/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o= 45 + github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs= 46 + github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= 47 + github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4= 48 + github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= 39 49 github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= 40 50 github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= 41 51 github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= ··· 48 58 github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= 49 59 github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= 50 60 github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= 61 + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= 62 + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= 51 63 github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= 52 64 github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= 53 65 github.com/go-git/gcfg/v2 v2.0.2 h1:MY5SIIfTGGEMhdA7d7JePuVVxtKL7Hp+ApGDJAJ7dpo= ··· 75 87 github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 76 88 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 77 89 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 78 - github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= 79 - github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 90 + github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= 91 + github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= 92 + github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= 93 + github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= 94 + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= 95 + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= 80 96 github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= 81 97 github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= 82 98 github.com/muesli/mango v0.1.0 h1:DZQK45d2gGbql1arsYA4vfg4d7I9Hfx5rX/GCmzsAvI= ··· 93 109 github.com/pjbgf/sha1cd v0.5.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM= 94 110 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 95 111 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 96 - github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 97 112 github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 98 113 github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 99 114 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 100 115 github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= 101 116 github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= 102 - github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= 103 - github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= 104 - github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= 105 - github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 117 + github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= 118 + github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= 119 + github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= 120 + github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 106 121 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 107 122 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 108 123 github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= ··· 117 132 golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= 118 133 golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= 119 134 golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 135 + golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 120 136 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 121 137 golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= 122 138 golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+77
internal/diff/tools.go
··· 1 + package diff 2 + 3 + import "io" 4 + 5 + // DiffViewKind enumerates the supported diff views. 6 + type DiffViewKind int 7 + 8 + const ( 9 + ViewUnified DiffViewKind = iota 10 + ViewSplit 11 + ViewHunk 12 + ViewInline 13 + ViewRich 14 + ViewSource 15 + ) 16 + 17 + // String returns a human-readable name for the view kind. 18 + func (k DiffViewKind) String() string { 19 + switch k { 20 + case ViewUnified: 21 + return "Unified" 22 + case ViewSplit: 23 + return "Split" 24 + case ViewHunk: 25 + return "Hunk" 26 + case ViewInline: 27 + return "Inline" 28 + case ViewRich: 29 + return "Rich" 30 + case ViewSource: 31 + return "Source" 32 + default: 33 + return "Unknown" 34 + } 35 + } 36 + 37 + // DiffResult holds the output of a diff tool. 38 + type DiffResult struct { 39 + // Content is the rendered diff output (text, ANSI, HTML, etc.) 40 + Content string 41 + 42 + // View is the kind of view used to render the diff. 43 + View DiffViewKind 44 + 45 + // Metadata like file paths, commit hashes, timestamps (optional). 46 + // You may extend as needed. 47 + OldPath string 48 + NewPath string 49 + FromHash string 50 + ToHash string 51 + } 52 + 53 + // DiffTool is the interface for generating diffs between two versions. 54 + type DiffTool interface { 55 + // Diff takes two blobs (old and new versions) and returns a DiffResult. 56 + // The reader parameters may be full file contents or other abstraction. 57 + // The viewKind parameter selects which view implementation (Unified/Split/...). 58 + Diff(oldContent io.Reader, newContent io.Reader, viewKind DiffViewKind) (DiffResult, error) 59 + } 60 + 61 + // UnifiedDiff implements unified view (single linear view with additions & deletions). 62 + type UnifiedDiff struct{} 63 + 64 + // SplitDiff implements side-by-side view (old on left, new on right). 65 + type SplitDiff struct{} 66 + 67 + // HunkDiff focuses on changed blocks, minimal context. 68 + type HunkDiff struct{} 69 + 70 + // InlineDiff renders changes inline in full file flow. 71 + type InlineDiff struct{} 72 + 73 + // RichDiff renders changes for formatted preview (for example, markdown or html previews). 74 + type RichDiff struct{} 75 + 76 + // SourceDiff simply returns raw diff data or patch format without special formatting. 77 + type SourceDiff struct{}
+88
internal/gitlog/gitlog.go
··· 1 + package gitlog 2 + 3 + import ( 4 + "fmt" 5 + "time" 6 + 7 + "github.com/go-git/go-git/v6" 8 + ) 9 + 10 + // CommitKind represents the kind of commit according to Conventional Commits. 11 + type CommitKind int 12 + 13 + const ( 14 + CommitTypeUnknown CommitKind = iota 15 + CommitTypeFeat 16 + CommitTypeFix 17 + CommitTypeDocs 18 + CommitTypeStyle 19 + CommitTypeRefactor 20 + CommitTypePerf 21 + CommitTypeTest 22 + CommitTypeBuild 23 + CommitTypeCI 24 + CommitTypeChore 25 + CommitTypeRevert 26 + ) 27 + 28 + // String returns the string representation of the CommitType. 29 + func (kind CommitKind) String() string { 30 + switch kind { 31 + case CommitTypeFeat: 32 + return "feat" 33 + case CommitTypeFix: 34 + return "fix" 35 + case CommitTypeDocs: 36 + return "docs" 37 + case CommitTypeStyle: 38 + return "style" 39 + case CommitTypeRefactor: 40 + return "refactor" 41 + case CommitTypePerf: 42 + return "perf" 43 + case CommitTypeTest: 44 + return "test" 45 + case CommitTypeBuild: 46 + return "build" 47 + case CommitTypeCI: 48 + return "ci" 49 + case CommitTypeChore: 50 + return "chore" 51 + case CommitTypeRevert: 52 + return "revert" 53 + default: 54 + return "unknown" 55 + } 56 + } 57 + 58 + func Log() { fmt.Println(git.GitDirName) } 59 + 60 + type CommitMeta struct { 61 + Type string // feat, fix, docs, etc. 62 + Scope string // optional 63 + Description string 64 + Breaking bool 65 + Body string 66 + Footers map[string]string 67 + } 68 + 69 + // CommitParser defines parsing of raw commit message strings into structured metadata. 70 + type CommitParser interface { 71 + // Parse takes a commit hash, subject line, body (including footers) 72 + // and the commit date, and returns a structured [CommitMeta] 73 + Parse(hash, subject, body string, date time.Time) (CommitMeta, error) 74 + 75 + // IsValidType returns true if the given [CommitKind] is recognised / allowed by your tooling. 76 + IsValidType(kind CommitKind) bool 77 + 78 + // Categorize returns the category (e.g., "Added", "Fixed", "Changed") for the given CommitMeta. 79 + Categorize(meta CommitMeta) string 80 + } 81 + 82 + // DefaultParser implements [CommitParser] and parses single 83 + // or multi-line commits into one or more [CommitMeta] 84 + type DefaultParser struct{} 85 + 86 + // ConventionalParser implements [CommitParser] and parses 87 + // conventional commits into one or more [CommitMeta] 88 + type ConventionalParser struct{}
+61
internal/style/style.go
··· 1 + // package styles define a color palette from nord & iceberg.vim 2 + package style 3 + 4 + import ( 5 + "fmt" 6 + 7 + "github.com/charmbracelet/lipgloss" 8 + ) 9 + 10 + var ( 11 + Background = lipgloss.Color("#1B1F27") // dark slate blue/charcoal 12 + Foreground = lipgloss.Color("#D8DEE9") // light grey-white for readable text 13 + AccentBlue = lipgloss.Color("#71A6D2") // main iceberg blue 14 + AccentSteel = lipgloss.Color("#4484B4") // steel blue 15 + AccentLapis = lipgloss.Color("#1B5E98") // lapis lazuli 16 + AccentCerulean = lipgloss.Color("#074683") // dark cerulean 17 + AddedColor = lipgloss.Color("#a3be8c") // nord aurora 18 + ChangedColor = AccentSteel 19 + FixedColor = AccentCerulean 20 + RemovedColor = lipgloss.Color("#BF616A") // nord aurora 21 + SecurityColor = lipgloss.Color("#d08770") // nord aurora 22 + ) 23 + 24 + var ( 25 + StyleHeadline = fgColor(AccentBlue).Bold(true) 26 + StyleText = fgColor(Foreground).Background(Background) 27 + StyleAdded = fgColor(AddedColor) 28 + StyleChanged = fgColor(ChangedColor) 29 + StyleFixed = fgColor(FixedColor) 30 + StyleRemoved = fgColor(RemovedColor) 31 + StyleSecurity = fgColor(SecurityColor) 32 + ) 33 + 34 + func fgColor(c lipgloss.Color) lipgloss.Style { return lipgloss.NewStyle().Foreground(c) } 35 + 36 + func Headline(s string) { 37 + v := StyleHeadline.Render(s) 38 + fmt.Println(v) 39 + } 40 + 41 + func Added(s string) { 42 + v := StyleAdded.Render(s) 43 + fmt.Println(v) 44 + } 45 + 46 + func Fixed(s string) { 47 + v := StyleFixed.Render(s) 48 + fmt.Println(v) 49 + } 50 + 51 + func Styled(st lipgloss.Style) func(s string, a ...any) { 52 + return func(s string, a ...any) { 53 + fmt.Printf(s, a...) 54 + } 55 + } 56 + 57 + func Styledln(st lipgloss.Style) func(s string, a ...any) { 58 + return func(s string, a ...any) { 59 + fmt.Println(fmt.Sprintf(s, a...)) 60 + } 61 + }
+147
internal/testutils/assertions.go
··· 1 + // package testutils contains assertions with struct [expect] 2 + // 3 + // Adapted from https://www.alexedwards.net/blog/the-9-go-test-assertions-i-use 4 + package testutils 5 + 6 + import ( 7 + "errors" 8 + "fmt" 9 + "reflect" 10 + "regexp" 11 + "testing" 12 + ) 13 + 14 + type expect struct{} 15 + 16 + // Expect is the exported instance users call. 17 + var Expect = expect{} 18 + 19 + // Equal asserts that got and want are equal (using reflect.DeepEqual where necessary). 20 + func (e expect) Equal(t *testing.T, got, want any, args ...any) { 21 + t.Helper() 22 + if !isDeepEqual(got, want) { 23 + t.Errorf("Equal assertion failed: got %#v; want %#v", got, want) 24 + if len(args) > 0 { 25 + t.Logf("Message: %v", fmt.Sprint(args...)) 26 + } 27 + } 28 + } 29 + 30 + // NotEqual asserts that got and want are *not* equal. 31 + func (e expect) NotEqual(t *testing.T, got, want any, args ...any) { 32 + t.Helper() 33 + if isDeepEqual(got, want) { 34 + t.Errorf("NotEqual assertion failed: got %#v; expected different value", got) 35 + if len(args) > 0 { 36 + t.Logf("Message: %v", fmt.Sprint(args...)) 37 + } 38 + } 39 + } 40 + 41 + // True asserts that the boolean got is true. 42 + func (e expect) True(t *testing.T, got bool, args ...any) { 43 + t.Helper() 44 + if !got { 45 + t.Errorf("True assertion failed: got false; want true") 46 + if len(args) > 0 { 47 + t.Logf("Message: %v", fmt.Sprint(args...)) 48 + } 49 + } 50 + } 51 + 52 + // False asserts that the boolean got is false. 53 + func (e expect) False(t *testing.T, got bool, args ...any) { 54 + t.Helper() 55 + if got { 56 + t.Errorf("False assertion failed: got true; want false") 57 + if len(args) > 0 { 58 + t.Logf("Message: %v", fmt.Sprint(args...)) 59 + } 60 + } 61 + } 62 + 63 + // Nil asserts that got is nil. 64 + func (e expect) Nil(t *testing.T, got any, args ...any) { 65 + t.Helper() 66 + if !isNil(got) { 67 + t.Errorf("Nil assertion failed: got non-nil value %#v", got) 68 + if len(args) > 0 { 69 + t.Logf("Message: %v", fmt.Sprint(args...)) 70 + } 71 + } 72 + } 73 + 74 + // NotNil asserts that got is *not* nil. 75 + func (e expect) NotNil(t *testing.T, got any, args ...any) { 76 + t.Helper() 77 + if isNil(got) { 78 + t.Errorf("NotNil assertion failed: got nil; want non-nil") 79 + if len(args) > 0 { 80 + t.Logf("Message: %v", fmt.Sprint(args...)) 81 + } 82 + } 83 + } 84 + 85 + // ErrorIs asserts that err wraps or is target. 86 + func (e expect) ErrorIs(t *testing.T, err, target error, args ...any) { 87 + t.Helper() 88 + if !errors.Is(err, target) { 89 + t.Errorf("ErrorIs assertion failed: got error %#v; want error matching %#v", err, target) 90 + if len(args) > 0 { 91 + t.Logf("Message: %v", fmt.Sprint(args...)) 92 + } 93 + } 94 + } 95 + 96 + // ErrorAs asserts that err can be assigned to target via [errors.As]. 97 + func (e expect) ErrorAs(t *testing.T, err error, target any, args ...any) { 98 + t.Helper() 99 + if err == nil { 100 + t.Errorf("ErrorAs assertion failed: got nil; want assignable to %T", target) 101 + return 102 + } 103 + if !errors.As(err, &target) { 104 + t.Errorf("ErrorAs assertion failed: got error %#v; want assignable to %T", err, target) 105 + if len(args) > 0 { 106 + t.Logf("Message: %v", fmt.Sprint(args...)) 107 + } 108 + } 109 + } 110 + 111 + // MatchesRegexp asserts that the string got matches the given regex pattern. 112 + func (e expect) MatchesRegexp(t *testing.T, got, pattern string, args ...any) { 113 + t.Helper() 114 + matched, err := regexp.MatchString(pattern, got) 115 + if err != nil { 116 + t.Fatalf("MatchesRegexp assertion: invalid pattern %q: %v", pattern, err) 117 + return 118 + } 119 + if !matched { 120 + t.Errorf("MatchesRegexp assertion failed: got %#v; want to match pattern %q", got, pattern) 121 + if len(args) > 0 { 122 + t.Logf("Message: %v", fmt.Sprint(args...)) 123 + } 124 + } 125 + } 126 + 127 + // isDeepEqual handles deep equality including nil checks. 128 + func isDeepEqual(a, b any) bool { 129 + if isNil(a) && isNil(b) { 130 + return true 131 + } 132 + return reflect.DeepEqual(a, b) 133 + } 134 + 135 + // isNil tests nil for interface, pointer, slice, map, chan, func types. 136 + func isNil(v any) bool { 137 + if v == nil { 138 + return true 139 + } 140 + rv := reflect.ValueOf(v) 141 + switch rv.Kind() { 142 + case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice, reflect.UnsafePointer: 143 + return rv.IsNil() 144 + default: 145 + return false 146 + } 147 + }
+50
internal/testutils/tea.go
··· 1 + package testutils 2 + 3 + import ( 4 + "io" 5 + "testing" 6 + "time" 7 + 8 + tea "github.com/charmbracelet/bubbletea" 9 + "github.com/charmbracelet/x/exp/teatest" 10 + ) 11 + 12 + // WithTermSize adjust terminal size for tests 13 + func WithTermSize(width, height int) teatest.TestOption { 14 + return teatest.WithInitialTermSize(width, height) 15 + } 16 + 17 + // RunModel runs a Bubble Tea model until it completes, and returns final output. 18 + func RunModel(t *testing.T, m tea.Model, opts ...teatest.TestOption) io.Reader { 19 + t.Helper() 20 + tm := teatest.NewTestModel(t, m, opts...) 21 + return tm.FinalOutput(t) 22 + } 23 + 24 + // RunModelWithInteraction runs a model, sends messages, waits for finish, returns final model. 25 + func RunModelWithInteraction(t *testing.T, m tea.Model, sendMsgs []tea.Msg, opts ...teatest.TestOption) tea.Model { 26 + t.Helper() 27 + tm := teatest.NewTestModel(t, m, opts...) 28 + 29 + for _, msg := range sendMsgs { 30 + tm.Send(msg) 31 + } 32 + 33 + tm.WaitFinished(t, teatest.WithFinalTimeout(2*time.Second)) 34 + return tm.FinalModel(t) 35 + } 36 + 37 + // AssertModelField asserts a field on the final model using a selector function 38 + func AssertModelField[T comparable](t *testing.T, finalModel tea.Model, fieldName string, getVal func(tea.Model) T, expected T) { 39 + t.Helper() 40 + actual := getVal(finalModel) 41 + if actual != expected { 42 + t.Errorf("model field %s: expected %v, got %v", fieldName, expected, actual) 43 + } 44 + } 45 + 46 + // WaitUntil waits for a condition on the output reader within timeout 47 + func WaitUntil(t *testing.T, reader io.Reader, condition func([]byte) bool, timeout time.Duration) { 48 + t.Helper() 49 + teatest.WaitFor(t, reader, condition, teatest.WithDuration(timeout)) 50 + }
+128
internal/testutils/testutils.go
··· 1 + package testutils 2 + 3 + import ( 4 + "os" 5 + "path/filepath" 6 + "testing" 7 + "time" 8 + 9 + "github.com/go-git/go-git/v6" 10 + "github.com/go-git/go-git/v6/plumbing/object" 11 + ) 12 + 13 + // SetupTestRepo creates a git repository in a temporary directory with sample commits. 14 + // The repository contains multiple commits with different types of changes to support 15 + // testing diff algorithms, changelog generation, and git log parsing. 16 + func SetupTestRepo(t *testing.T) *git.Repository { 17 + t.Helper() 18 + dir := t.TempDir() 19 + 20 + repo, err := git.PlainInit(dir, false) 21 + if err != nil { 22 + t.Fatalf("failed to init repo: %v", err) 23 + } 24 + 25 + w, err := repo.Worktree() 26 + if err != nil { 27 + t.Fatalf("failed to get worktree: %v", err) 28 + } 29 + 30 + // Create commits with varied content for diff testing 31 + commits := []struct { 32 + name, content, message string 33 + }{ 34 + {"README.md", "# Project\n\nInitial version", "chore: initial commit"}, 35 + {"a.txt", "hello world", "feat: add hello world"}, 36 + {"b.txt", "fixed bug", "fix: patch file"}, 37 + {"a.txt", "hello world\ngoodbye world", "feat: add goodbye"}, 38 + {"c.txt", "new feature\nwith multiple lines\nof content", "feat: add multi-line file"}, 39 + {"b.txt", "fixed bug\nwith proper handling", "fix: improve error handling"}, 40 + } 41 + 42 + for _, c := range commits { 43 + path := filepath.Join(dir, c.name) 44 + if err := os.WriteFile(path, []byte(c.content), 0644); err != nil { 45 + t.Fatalf("failed to write file %s: %v", c.name, err) 46 + } 47 + if _, err := w.Add(c.name); err != nil { 48 + t.Fatalf("failed to add file %s: %v", c.name, err) 49 + } 50 + if _, err := w.Commit(c.message, &git.CommitOptions{ 51 + Author: &object.Signature{ 52 + Name: "Test Author", 53 + Email: "test@example.com", 54 + When: time.Now(), 55 + }, 56 + }); err != nil { 57 + t.Fatalf("commit failed: %v", err) 58 + } 59 + } 60 + return repo 61 + } 62 + 63 + // CreateTag creates a lightweight tag at the current HEAD of the repository. 64 + func CreateTag(t *testing.T, repo *git.Repository, tagName string) { 65 + t.Helper() 66 + head, err := repo.Head() 67 + if err != nil { 68 + t.Fatalf("failed to get HEAD: %v", err) 69 + } 70 + 71 + _, err = repo.CreateTag(tagName, head.Hash(), nil) 72 + if err != nil { 73 + t.Fatalf("failed to create tag %s: %v", tagName, err) 74 + } 75 + } 76 + 77 + // GetCommitHistory returns all commits in the repository from HEAD backwards. 78 + func GetCommitHistory(t *testing.T, repo *git.Repository) []*object.Commit { 79 + t.Helper() 80 + head, err := repo.Head() 81 + if err != nil { 82 + t.Fatalf("failed to get HEAD: %v", err) 83 + } 84 + 85 + commitIter, err := repo.Log(&git.LogOptions{From: head.Hash()}) 86 + if err != nil { 87 + t.Fatalf("failed to get commit log: %v", err) 88 + } 89 + 90 + var commits []*object.Commit 91 + err = commitIter.ForEach(func(c *object.Commit) error { 92 + commits = append(commits, c) 93 + return nil 94 + }) 95 + if err != nil { 96 + t.Fatalf("failed to iterate commits: %v", err) 97 + } 98 + 99 + return commits 100 + } 101 + 102 + // AddCommit adds a new commit to the repository with the given file changes. 103 + func AddCommit(t *testing.T, repo *git.Repository, filename, content, message string) { 104 + t.Helper() 105 + w, err := repo.Worktree() 106 + if err != nil { 107 + t.Fatalf("failed to get worktree: %v", err) 108 + } 109 + 110 + path := filepath.Join(w.Filesystem.Root(), filename) 111 + if err := os.WriteFile(path, []byte(content), 0644); err != nil { 112 + t.Fatalf("failed to write file %s: %v", filename, err) 113 + } 114 + 115 + if _, err := w.Add(filename); err != nil { 116 + t.Fatalf("failed to add file %s: %v", filename, err) 117 + } 118 + 119 + if _, err := w.Commit(message, &git.CommitOptions{ 120 + Author: &object.Signature{ 121 + Name: "Test Author", 122 + Email: "test@example.com", 123 + When: time.Now(), 124 + }, 125 + }); err != nil { 126 + t.Fatalf("commit failed: %v", err) 127 + } 128 + }
+1
internal/ui/ui.go
··· 1 + package ui