Skip to content

Commit 45a68d5

Browse files
vaindclaude
andcommitted
feat: add git commit fallback for repositories without changelog files
- Adds fallback to generate changelog from git commits when no changelog.md exists - Refactors code into separate functions for better maintainability: - Get-ChangelogFromCommits: Generate changelog from git log - Get-ChangelogFromDiff: Generate changelog from file diff - Format-ChangelogContent: Apply consistent formatting and sanitization - Filters out version tag commits to focus on meaningful changes - Applies same link formatting to prevent GitHub notifications - Supports repositories like Catch2, React, Vue.js that use GitHub releases - Maintains backward compatibility with existing changelog.md workflow - Adds comprehensive tests for new functionality 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 6af5c2d commit 45a68d5

File tree

2 files changed

+208
-62
lines changed

2 files changed

+208
-62
lines changed

updater/scripts/get-changelog.ps1

Lines changed: 159 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -45,24 +45,68 @@ function Get-ChangelogContent {
4545
return $false
4646
}
4747

48-
try {
49-
Write-Host 'Fetching CHANGELOG files for comparison...'
48+
# Function to generate changelog from git commits
49+
function Get-ChangelogFromCommits {
50+
param($repoUrl, $oldTag, $newTag, $tmpDir)
5051

51-
# Fetch old changelog
52-
$oldChangelogPath = Join-Path $tmpDir 'old-changelog.md'
53-
if (-not (Get-ChangelogContent $OldTag $oldChangelogPath)) {
54-
Write-Warning "Could not find changelog at $OldTag"
55-
return
56-
}
52+
# Clone the repository
53+
$repoDir = Join-Path $tmpDir 'repo'
54+
Write-Host "Cloning repository to generate changelog from commits..."
5755

58-
# Fetch new changelog
59-
$newChangelogPath = Join-Path $tmpDir 'new-changelog.md'
60-
if (-not (Get-ChangelogContent $NewTag $newChangelogPath)) {
61-
Write-Warning "Could not find changelog at $NewTag"
62-
return
56+
# Clone with limited depth for performance, but ensure we have both tags
57+
git clone --depth=200 --no-single-branch --quiet $repoUrl $repoDir 2>&1 | Out-Null
58+
59+
Push-Location $repoDir
60+
try {
61+
# Ensure we have both tags
62+
git fetch --tags --quiet 2>&1 | Out-Null
63+
64+
# Get commit messages between tags
65+
Write-Host "Getting commits between $oldTag and $newTag..."
66+
$commitMessages = git log "$oldTag..$newTag" --pretty=format:'%s' 2>&1
67+
68+
if ($LASTEXITCODE -ne 0) {
69+
Write-Warning "Could not get commits between $oldTag and $newTag"
70+
return $null
71+
}
72+
73+
if ([string]::IsNullOrEmpty($commitMessages)) {
74+
Write-Host "No commits found between $oldTag and $newTag"
75+
return $null
76+
}
77+
78+
# Filter out version tag commits and format as list
79+
$commits = $commitMessages -split "`n" |
80+
Where-Object {
81+
$_ -and
82+
$_ -notmatch '^\s*v?\d+\.\d+\.\d+' -and # Skip version commits
83+
$_.Trim().Length -gt 0
84+
} |
85+
ForEach-Object { "- $_" }
86+
87+
if ($commits.Count -eq 0) {
88+
Write-Host "No meaningful commits found between $oldTag and $newTag"
89+
return $null
90+
}
91+
92+
# Create changelog from commits
93+
$changelog = "## Changelog`n`n"
94+
$changelog += "### Commits between $oldTag and $newTag`n`n"
95+
$changelog += $commits -join "`n"
96+
97+
Write-Host "Generated changelog from $($commits.Count) commits"
98+
return $changelog
99+
}
100+
finally {
101+
Pop-Location
63102
}
103+
}
104+
105+
# Function to generate changelog from diff between changelog files
106+
function Get-ChangelogFromDiff {
107+
param($oldChangelogPath, $newChangelogPath, $oldTag, $newTag)
64108

65-
Write-Host "Generating changelog diff between $OldTag and $NewTag..."
109+
Write-Host "Generating changelog diff between $oldTag and $newTag..."
66110

67111
# Generate diff using git diff --no-index
68112
# git diff returns exit code 1 when differences are found, which is expected behavior
@@ -80,69 +124,122 @@ try {
80124
# The first lines are diff metadata, skip them
81125
$fullDiff = $fullDiff -split "`n" | Select-Object -Skip 4
82126
if ([string]::IsNullOrEmpty("$fullDiff")) {
83-
Write-Host "No differences found between $OldTag and $NewTag"
84-
return
127+
Write-Host "No differences found between $oldTag and $newTag"
128+
return $null
85129
} else {
86130
Write-Host "Successfully created a changelog diff - $($fullDiff.Count) lines"
87131
}
88132

89133
# Extract only the added lines (lines starting with + but not ++)
90134
$addedLines = $fullDiff | Where-Object { $_ -match '^[+][^+]*' } | ForEach-Object { $_.Substring(1) }
91135

92-
if ($addedLines.Count -gt 0) {
93-
# Create clean changelog from added lines
94-
$changelog = ($addedLines -join "`n").Trim()
136+
if ($addedLines.Count -eq 0) {
137+
Write-Host "No changelog additions found between $oldTag and $newTag"
138+
return $null
139+
}
95140

96-
# Apply formatting to clean changelog
97-
if ($changelog.Length -gt 0) {
98-
# Add header
99-
if (-not ($changelog -match '^(##|#) Changelog')) {
100-
$changelog = "## Changelog`n`n$changelog"
101-
}
141+
# Create clean changelog from added lines
142+
$changelog = ($addedLines -join "`n").Trim()
102143

103-
# Increase header level by one for content (not the main header)
104-
$changelog = $changelog -replace '(^|\n)(#+) ', '$1$2# ' -replace '^### Changelog', '## Changelog'
144+
if ($changelog.Length -eq 0) {
145+
return $null
146+
}
105147

106-
# Only add details section if there are deletions or modifications (not just additions)
107-
$hasModifications = $fullDiff | Where-Object { $_ -match '^[-]' -and $_ -notmatch '^[-]{3}' }
108-
if ($hasModifications) {
109-
$changelog += "`n`n<details>`n<summary>Full CHANGELOG.md diff</summary>`n`n"
110-
$changelog += '```diff' + "`n"
111-
$changelog += $fullDiff -join "`n"
112-
$changelog += "`n" + '```' + "`n`n</details>"
113-
}
148+
# Add header if needed
149+
if (-not ($changelog -match '^(##|#) Changelog')) {
150+
$changelog = "## Changelog`n`n$changelog"
151+
}
114152

115-
# Apply standard formatting
116-
# Remove at-mentions.
117-
$changelog = $changelog -replace '@', ''
118-
# Make PR/issue references into links to the original repository (unless they already are links).
119-
$changelog = $changelog -replace '(?<!\[)#([0-9]+)(?![\]0-9])', ('[#$1](' + $RepoUrl + '/issues/$1)')
120-
# Replace any links pointing to github.com so that the target PRs/Issues don't get na notification.
121-
$changelog = $changelog -replace ('\(' + $prefix), '(https://github-redirect.dependabot.com/'
122-
123-
# Limit the changelog length to ~60k to allow for other text in the PR body (total PR limit is 65536 characters).
124-
$limit = 60000
125-
if ($changelog.Length -gt $limit) {
126-
$oldLength = $changelog.Length
127-
Write-Warning "Truncating changelog because it's $($changelog.Length - $limit) characters longer than the limit $limit."
128-
while ($changelog.Length -gt $limit) {
129-
$lastNewlineIndex = $changelog.LastIndexOf("`n")
130-
if ($lastNewlineIndex -eq -1) {
131-
# No newlines found, just truncate to limit
132-
$changelog = $changelog.Substring(0, $limit)
133-
break
134-
}
135-
$changelog = $changelog.Substring(0, $lastNewlineIndex)
136-
}
137-
$changelog += "`n`n> :warning: **Changelog content truncated by $($oldLength - $changelog.Length) characters because it was over the limit ($limit) and wouldn't fit into PR description.**"
138-
}
153+
# Increase header level by one for content (not the main header)
154+
$changelog = $changelog -replace '(^|\n)(#+) ', '$1$2# ' -replace '^### Changelog', '## Changelog'
139155

140-
Write-Host "Final changelog length: $($changelog.Length) characters"
141-
Write-Output $changelog
156+
# Only add details section if there are deletions or modifications (not just additions)
157+
$hasModifications = $fullDiff | Where-Object { $_ -match '^[-]' -and $_ -notmatch '^[-]{3}' }
158+
if ($hasModifications) {
159+
$changelog += "`n`n<details>`n<summary>Full CHANGELOG.md diff</summary>`n`n"
160+
$changelog += '```diff' + "`n"
161+
$changelog += $fullDiff -join "`n"
162+
$changelog += "`n" + '```' + "`n`n</details>"
163+
}
164+
165+
return $changelog
166+
}
167+
168+
# Function to sanitize and format changelog content
169+
function Format-ChangelogContent {
170+
param($changelog, $repoUrl)
171+
172+
if ([string]::IsNullOrEmpty($changelog)) {
173+
return $null
174+
}
175+
176+
# Apply standard formatting
177+
# Remove at-mentions
178+
$changelog = $changelog -replace '@', ''
179+
180+
# Make PR/issue references into links to the original repository (unless they already are links)
181+
$changelog = $changelog -replace '(?<!\[)#([0-9]+)(?![\]0-9])', ('[#$1](' + $repoUrl + '/issues/$1)')
182+
183+
# Replace any links pointing to github.com so that the target PRs/Issues don't get notification
184+
$prefix = 'https?://(www\.)?github.com/'
185+
$changelog = $changelog -replace ('\(' + $prefix), '(https://github-redirect.dependabot.com/'
186+
187+
# Limit the changelog length to ~60k to allow for other text in the PR body (total PR limit is 65536 characters)
188+
$limit = 60000
189+
if ($changelog.Length -gt $limit) {
190+
$oldLength = $changelog.Length
191+
Write-Warning "Truncating changelog because it's $($changelog.Length - $limit) characters longer than the limit $limit."
192+
while ($changelog.Length -gt $limit) {
193+
$lastNewlineIndex = $changelog.LastIndexOf("`n")
194+
if ($lastNewlineIndex -eq -1) {
195+
# No newlines found, just truncate to limit
196+
$changelog = $changelog.Substring(0, $limit)
197+
break
198+
}
199+
$changelog = $changelog.Substring(0, $lastNewlineIndex)
142200
}
201+
$changelog += "`n`n> :warning: **Changelog content truncated by $($oldLength - $changelog.Length) characters because it was over the limit ($limit) and wouldn't fit into PR description.**"
202+
}
203+
204+
Write-Host "Final changelog length: $($changelog.Length) characters"
205+
return $changelog
206+
}
207+
208+
try {
209+
Write-Host 'Fetching CHANGELOG files for comparison...'
210+
211+
# Fetch old changelog
212+
$oldChangelogPath = Join-Path $tmpDir 'old-changelog.md'
213+
$hasOldChangelog = Get-ChangelogContent $OldTag $oldChangelogPath
214+
215+
# Fetch new changelog
216+
$newChangelogPath = Join-Path $tmpDir 'new-changelog.md'
217+
$hasNewChangelog = Get-ChangelogContent $NewTag $newChangelogPath
218+
219+
$changelog = $null
220+
221+
# Try changelog file diff first, fall back to git commits if not available
222+
if ($hasOldChangelog -and $hasNewChangelog) {
223+
$changelog = Get-ChangelogFromDiff $oldChangelogPath $newChangelogPath $OldTag $NewTag
224+
}
225+
226+
# Fall back to git commits if no changelog files or no diff found
227+
if (-not $changelog) {
228+
Write-Host "No changelog files found or no changes detected, falling back to git commits..."
229+
$changelog = Get-ChangelogFromCommits $RepoUrl $OldTag $NewTag $tmpDir
143230
}
144231

145-
Write-Host "No changelog additions found between $OldTag and $NewTag"
232+
# Apply formatting and output result
233+
if ($changelog) {
234+
$formattedChangelog = Format-ChangelogContent $changelog $RepoUrl
235+
if ($formattedChangelog) {
236+
Write-Output $formattedChangelog
237+
} else {
238+
Write-Host "No changelog content to display after formatting"
239+
}
240+
} else {
241+
Write-Host "No changelog found between $OldTag and $NewTag"
242+
}
146243
} catch {
147244
Write-Warning "Failed to get changelog: $($_.Exception.Message)"
148245
} finally {

updater/tests/get-changelog.Tests.ps1

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,4 +258,53 @@ Features, fixes and improvements in this release have been contributed by:
258258
$actualLines[$i].Trim() | Should -Be $expectedLines[$i].Trim()
259259
}
260260
}
261+
262+
It 'falls back to git commits when no changelog files exist' {
263+
# Test with a repository that doesn't have changelog files
264+
$actual = & "$PSScriptRoot/../scripts/get-changelog.ps1" `
265+
-RepoUrl 'https://github.com/catchorg/Catch2' -OldTag 'v3.9.1' -NewTag 'v3.10.0'
266+
267+
# Should contain the header for git commit fallback
268+
$actual | Should -Match "## Changelog"
269+
$actual | Should -Match "### Commits between v3.9.1 and v3.10.0"
270+
271+
# Should contain some expected commits (filtering out version tags)
272+
$actual | Should -Match "- Forbid deducing reference types for m_predicate in FilterGenerator"
273+
$actual | Should -Match "- Make message macros \(FAIL, WARN, INFO, etc\) thread safe"
274+
275+
# Should properly format PR links to prevent notifications
276+
$actual | Should -Match "github-redirect.dependabot.com"
277+
278+
# Should remove @mentions
279+
$actual | Should -Not -Match "@\w+"
280+
}
281+
282+
It 'git commit fallback handles PR references correctly' {
283+
# Test with a known repository and tags that contain PR references
284+
$actual = & "$PSScriptRoot/../scripts/get-changelog.ps1" `
285+
-RepoUrl 'https://github.com/catchorg/Catch2' -OldTag 'v3.9.1' -NewTag 'v3.10.0'
286+
287+
# Check that PR references are converted to links with github-redirect
288+
if ($actual -match '#(\d+)') {
289+
$actual | Should -Match '\[#\d+\]\(https://github-redirect\.dependabot\.com/catchorg/Catch2/issues/\d+\)'
290+
}
291+
}
292+
293+
It 'git commit fallback returns empty when no commits found' {
294+
# Test with same tags (no commits between them)
295+
$actual = & "$PSScriptRoot/../scripts/get-changelog.ps1" `
296+
-RepoUrl 'https://github.com/catchorg/Catch2' -OldTag 'v3.10.0' -NewTag 'v3.10.0'
297+
298+
$actual | Should -BeNullOrEmpty
299+
}
300+
301+
It 'git commit fallback filters out version tag commits' {
302+
# Test that version commits like "v3.10.0" are filtered out
303+
$actual = & "$PSScriptRoot/../scripts/get-changelog.ps1" `
304+
-RepoUrl 'https://github.com/catchorg/Catch2' -OldTag 'v3.9.1' -NewTag 'v3.10.0'
305+
306+
# Should not contain version tag lines
307+
$actual | Should -Not -Match "- v3\.10\.0"
308+
$actual | Should -Not -Match "- 3\.10\.0"
309+
}
261310
}

0 commit comments

Comments
 (0)