Skip to content

Commit 4aa05b2

Browse files
silverwindclaude
andcommitted
share changelog split, simplify getFileChanges return
processChangelog walks the changelog content once, sharing extractEntry with readChangelogEntry so the entry-extraction loop isn't duplicated. getFileChanges no longer returns its input file unchanged; the 2-tuple matches what callers actually use. Co-Authored-By: Claude (Opus 4.7) <noreply@anthropic.com>
1 parent 4c24623 commit 4aa05b2

2 files changed

Lines changed: 48 additions & 34 deletions

File tree

index.test.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1408,15 +1408,15 @@ test("readVersionFromPyprojectToml returns null", () => withTmpDir(async (tmpDir
14081408
test("getFileChanges package.json", () => withTmpDir(async (tmpDir) => {
14091409
const file = join(tmpDir, "package.json");
14101410
await writeFile(file, JSON.stringify({name: "test", version: "1.0.0"}, null, 2));
1411-
const [, content] = getFileChanges({file, baseVersion: "1.0.0", newVersion: "1.0.1"});
1411+
const [content] = getFileChanges({file, baseVersion: "1.0.0", newVersion: "1.0.1"});
14121412
expect(JSON.parse(content!).version).toEqual("1.0.1");
14131413
}));
14141414

14151415
test("getFileChanges package-lock.json", () => withTmpDir(async (tmpDir) => {
14161416
const file = join(tmpDir, "package-lock.json");
14171417
const data = {name: "test", version: "1.0.0", lockfileVersion: 3, packages: {"": {version: "1.0.0"}}};
14181418
await writeFile(file, JSON.stringify(data, null, 2));
1419-
const [, content] = getFileChanges({file, baseVersion: "1.0.0", newVersion: "2.0.0"});
1419+
const [content] = getFileChanges({file, baseVersion: "1.0.0", newVersion: "2.0.0"});
14201420
const result = JSON.parse(content!);
14211421
expect(result.version).toEqual("2.0.0");
14221422
expect(result.packages[""].version).toEqual("2.0.0");
@@ -1425,43 +1425,43 @@ test("getFileChanges package-lock.json", () => withTmpDir(async (tmpDir) => {
14251425
test("getFileChanges pyproject.toml", () => withTmpDir(async (tmpDir) => {
14261426
const file = join(tmpDir, "pyproject.toml");
14271427
await writeFile(file, `[project]\nname = "test"\nversion = "1.0.0"\n`);
1428-
const [, content] = getFileChanges({file, baseVersion: "1.0.0", newVersion: "1.1.0"});
1428+
const [content] = getFileChanges({file, baseVersion: "1.0.0", newVersion: "1.1.0"});
14291429
expect(content).toContain(`version = "1.1.0"`);
14301430
}));
14311431

14321432
test("getFileChanges uv.lock", () => withTmpDir(async (tmpDir) => {
14331433
await writeFile(join(tmpDir, "pyproject.toml"), `[project]\nname = "myapp"\nversion = "1.0.0"\n`);
14341434
const file = join(tmpDir, "uv.lock");
14351435
await writeFile(file, `[[package]]\nname = "myapp"\nversion = "1.0.0"\n`);
1436-
const [, content] = getFileChanges({file, baseVersion: "1.0.0", newVersion: "1.1.0"});
1436+
const [content] = getFileChanges({file, baseVersion: "1.0.0", newVersion: "1.1.0"});
14371437
expect(content).toContain(`version = "1.1.0"`);
14381438
}));
14391439

14401440
test("getFileChanges generic file", () => withTmpDir(async (tmpDir) => {
14411441
const file = join(tmpDir, "version.txt");
14421442
await writeFile(file, "version 1.0.0 here");
1443-
const [, content] = getFileChanges({file, baseVersion: "1.0.0", newVersion: "2.0.0"});
1443+
const [content] = getFileChanges({file, baseVersion: "1.0.0", newVersion: "2.0.0"});
14441444
expect(content).toEqual("version 2.0.0 here");
14451445
}));
14461446

14471447
test("getFileChanges lockfile skip", () => withTmpDir(async (tmpDir) => {
14481448
const file = join(tmpDir, "yarn.lock");
14491449
await writeFile(file, "content 1.0.0");
1450-
const [, content] = getFileChanges({file, baseVersion: "1.0.0", newVersion: "2.0.0"});
1450+
const [content] = getFileChanges({file, baseVersion: "1.0.0", newVersion: "2.0.0"});
14511451
expect(content).toBeNull();
14521452
}));
14531453

14541454
test("getFileChanges with date", () => withTmpDir(async (tmpDir) => {
14551455
const file = join(tmpDir, "changelog.txt");
14561456
await writeFile(file, "version 1.0.0 released 2020-01-01");
1457-
const [, content] = getFileChanges({file, baseVersion: "1.0.0", newVersion: "1.0.1", date: "2025-06-15"});
1457+
const [content] = getFileChanges({file, baseVersion: "1.0.0", newVersion: "1.0.1", date: "2025-06-15"});
14581458
expect(content).toEqual("version 1.0.1 released 2025-06-15");
14591459
}));
14601460

14611461
test("getFileChanges with replacements", () => withTmpDir(async (tmpDir) => {
14621462
const file = join(tmpDir, "file.txt");
14631463
await writeFile(file, "version 1.0.0 FOO");
1464-
const [, content] = getFileChanges({
1464+
const [content] = getFileChanges({
14651465
file, baseVersion: "1.0.0", newVersion: "1.0.1",
14661466
replacements: [{re: /FOO/, replacement: "BAR"}],
14671467
});

index.ts

Lines changed: 40 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -109,13 +109,7 @@ function findVersionHeading(lines: string[], version: string): {index: number, l
109109
return null;
110110
}
111111
112-
// Lenient about heading shape: matches "# 1.2.3", "## v1.2.3", "## [1.2.3]",
113-
// "## [1.2.3] - 2024-01-15", "## 1.2.3 (2024-01-15)", etc.
114-
export function readChangelogEntry(content: string, version: string): string | null {
115-
const lines = content.split(reNewline);
116-
const head = findVersionHeading(lines, version);
117-
if (!head) return null;
118-
112+
function extractEntry(lines: string[], head: {index: number, level: number}): string | null {
119113
let end = lines.length;
120114
for (let i = head.index + 1; i < lines.length; i++) {
121115
const m = reHeading.exec(lines[i]);
@@ -124,26 +118,46 @@ export function readChangelogEntry(content: string, version: string): string | n
124118
break;
125119
}
126120
}
127-
128121
return lines.slice(head.index + 1, end).join("\n").trim() || null;
129122
}
130123
131-
export function updateChangelogHeadingDate(content: string, version: string, date: string): string | null {
132-
const lines = content.split(reNewline);
133-
const head = findVersionHeading(lines, version);
134-
if (!head) return null;
135-
136-
const heading = lines[head.index];
124+
function updateHeadingDateInLines(lines: string[], index: number, date: string): string | null {
125+
const heading = lines[index];
137126
if (rePlaceholderDate.test(heading)) {
138-
lines[head.index] = heading.replace(rePlaceholderDate, date);
127+
lines[index] = heading.replace(rePlaceholderDate, date);
139128
} else if (reDate.test(heading)) {
140129
return null;
141130
} else {
142-
lines[head.index] = `${heading.trimEnd()} - ${date}`;
131+
lines[index] = `${heading.trimEnd()} - ${date}`;
143132
}
144133
return lines.join("\n");
145134
}
146135
136+
// Lenient about heading shape: matches "# 1.2.3", "## v1.2.3", "## [1.2.3]",
137+
// "## [1.2.3] - 2024-01-15", "## 1.2.3 (2024-01-15)", etc.
138+
export function readChangelogEntry(content: string, version: string): string | null {
139+
const lines = content.split(reNewline);
140+
const head = findVersionHeading(lines, version);
141+
if (!head) return null;
142+
return extractEntry(lines, head);
143+
}
144+
145+
export function updateChangelogHeadingDate(content: string, version: string, date: string): string | null {
146+
const lines = content.split(reNewline);
147+
const head = findVersionHeading(lines, version);
148+
if (!head) return null;
149+
return updateHeadingDateInLines(lines, head.index, date);
150+
}
151+
152+
function processChangelog(content: string, version: string, date: string): {entry: string, updated: string | null} | null {
153+
const lines = content.split(reNewline);
154+
const head = findVersionHeading(lines, version);
155+
if (!head) return null;
156+
const entry = extractEntry(lines, head);
157+
if (!entry) return null;
158+
return {entry, updated: updateHeadingDateInLines(lines, head.index, date)};
159+
}
160+
147161
export async function removeIgnoredFiles(files: Array<string>, cwd?: string): Promise<Array<string>> {
148162
let result: Result;
149163
try {
@@ -163,12 +177,12 @@ export type GetFileChangesOpts = {
163177
date?: string,
164178
};
165179
166-
export function getFileChanges({file, baseVersion, newVersion, replacements, date}: GetFileChangesOpts): [string, string | null, string | null] {
180+
export function getFileChanges({file, baseVersion, newVersion, replacements, date}: GetFileChangesOpts): [string | null, string | null] {
167181
const fileName = basename(file);
168182
169183
// unhandled lockfiles: blind search-and-replace would corrupt dependency versions
170184
if ((/lock/i.test(fileName) || fileName === "go.sum") && fileName !== "package-lock.json" && fileName !== "uv.lock") {
171-
return [file, null, null];
185+
return [null, null];
172186
}
173187
174188
const oldData = readFileSync(file, "utf8");
@@ -207,7 +221,7 @@ export function getFileChanges({file, baseVersion, newVersion, replacements, dat
207221
}
208222
}
209223

210-
return [file, newData, oldData];
224+
return [newData, oldData];
211225
}
212226

213227
export function write(file: string, content: string): void {
@@ -600,9 +614,9 @@ async function main(): Promise<void> {
600614
} catch {
601615
return null;
602616
}
603-
const entry = readChangelogEntry(original, newVersion);
604-
if (!entry) return null;
605-
return {path, entry, original, updated: updateChangelogHeadingDate(original, newVersion, today)};
617+
const processed = processChangelog(original, newVersion, today);
618+
if (!processed) return null;
619+
return {path, original, entry: processed.entry, updated: processed.updated};
606620
})();
607621

608622
const allFiles = changelogInfo?.updated ? [...files, relative(pwd, changelogInfo.path)] : files;
@@ -659,11 +673,11 @@ async function main(): Promise<void> {
659673
for (const [file, content] of originals) write(file, content);
660674
});
661675
for (const file of files) {
662-
const [filePath, newData, oldData] = getFileChanges({file, baseVersion, newVersion, replacements, date});
676+
const [newData, oldData] = getFileChanges({file, baseVersion, newVersion, replacements, date});
663677
if (newData !== null) {
664-
if (!originals.has(filePath)) originals.set(filePath, oldData!);
665-
logVerbose(`writing ${filePath}`);
666-
write(filePath, newData);
678+
if (!originals.has(file)) originals.set(file, oldData!);
679+
logVerbose(`writing ${file}`);
680+
write(file, newData);
667681
} else {
668682
logVerbose(`skipping ${file} (unhandled lockfile)`);
669683
}

0 commit comments

Comments
 (0)