Skip to content

Commit 7299d86

Browse files
authored
Merge pull request #132 from eglavin/git-log-consistency
refactor: set no-color and date-order opts when getting commits
2 parents 120fa25 + b7d67e3 commit 7299d86

4 files changed

Lines changed: 140 additions & 19 deletions

File tree

src/services/__tests__/git.test.ts

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { setupTest } from "../../../tests/setup-tests";
2+
import { IS_E2E } from "../../../tests/options";
3+
import { CommitParser } from "../../commit-parser/commit-parser";
24
import { Git } from "../git";
35

46
describe("git", () => {
@@ -402,7 +404,7 @@ system so we'll see what happens.`,
402404
expect(email).toBe("");
403405
});
404406

405-
it("should read commits with and any tags", async () => {
407+
it("should read commits and tags", async () => {
406408
const { config, create } = await setupTest("execute-file");
407409
const git = new Git(config);
408410

@@ -450,4 +452,95 @@ system so we'll see what happens.`,
450452
expect(ref).toBe(" (tag: v1.0.0)");
451453
}
452454
});
455+
456+
it("should handle out of order commits", { timeout: 10_000, skip: !IS_E2E }, async () => {
457+
const { config, create, execGit, sleep } = await setupTest("execute-file");
458+
const git = new Git(config);
459+
460+
create.directory("src");
461+
create.file("File 1 content", "src", "file1.txt").add();
462+
await git.commit("-m", "feat: initial commit");
463+
await git.tag("v1.0.0");
464+
465+
await sleep(1000);
466+
467+
// Create a long lived branch and commit to it, but don't merge it yet so that the commit is out of order in the git log
468+
await execGit.raw("checkout", "-b", "long-lived-feature-branch");
469+
create.file("File 2 content", "src", "file2.txt").add();
470+
await git.commit("-m", "feat: add file2");
471+
await execGit.raw("checkout", "main");
472+
473+
await sleep(1000);
474+
475+
// Create two short lived branches and merge them in quick succession to create out of order commits in the git log
476+
await execGit.raw("checkout", "-b", "quick-feature-branch");
477+
create.file("File 3 content", "src", "file3.txt").add();
478+
await git.commit("-m", "feat: add file3");
479+
await execGit.raw("checkout", "main");
480+
481+
await sleep(1000);
482+
483+
await execGit.raw("merge", "quick-feature-branch", "--no-ff");
484+
await git.tag("v1.1.0");
485+
486+
await sleep(1000);
487+
488+
await execGit.raw("checkout", "-b", "quick-bugfix-branch");
489+
create.file("File 4 content", "src", "file4.txt").add();
490+
await git.commit("-m", "fix: add file4");
491+
await execGit.raw("checkout", "main");
492+
493+
await sleep(1000);
494+
495+
await execGit.raw("merge", "quick-bugfix-branch", "--no-ff");
496+
await git.tag("v1.1.1");
497+
498+
await sleep(1000);
499+
500+
// Merge the long lived branch last so that its commit is out of order in the git log
501+
await execGit.raw("merge", "long-lived-feature-branch", "--no-ff");
502+
await git.tag("v1.2.0");
503+
504+
const commits = await git.getCommits();
505+
506+
const commitParser = new CommitParser();
507+
const parsedCommits = commits.map(commitParser.parseRawCommit);
508+
509+
expect(parsedCommits.length).toBe(7);
510+
{
511+
const { subject, refNames } = parsedCommits[0];
512+
expect(subject).toBe("Merge branch 'long-lived-feature-branch'");
513+
expect(refNames).toBe("(HEAD -> main, tag: v1.2.0)");
514+
}
515+
{
516+
const { subject, refNames } = parsedCommits[1];
517+
expect(subject).toBe("Merge branch 'quick-bugfix-branch'");
518+
expect(refNames).toBe("(tag: v1.1.1)");
519+
}
520+
{
521+
const { subject, refNames } = parsedCommits[2];
522+
expect(subject).toBe("fix: add file4");
523+
expect(refNames).toBe("(quick-bugfix-branch)");
524+
}
525+
{
526+
const { subject, refNames } = parsedCommits[3];
527+
expect(subject).toBe("Merge branch 'quick-feature-branch'");
528+
expect(refNames).toBe("(tag: v1.1.0)");
529+
}
530+
{
531+
const { subject, refNames } = parsedCommits[4];
532+
expect(subject).toBe("feat: add file3");
533+
expect(refNames).toBe("(quick-feature-branch)");
534+
}
535+
{
536+
const { subject, refNames } = parsedCommits[5];
537+
expect(subject).toBe("feat: add file2");
538+
expect(refNames).toBe("(long-lived-feature-branch)");
539+
}
540+
{
541+
const { subject, refNames } = parsedCommits[6];
542+
expect(subject).toBe("feat: initial commit");
543+
expect(refNames).toBe("(tag: v1.0.0)");
544+
}
545+
});
453546
});

src/services/git.ts

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -240,28 +240,25 @@ export class Git {
240240
}
241241

242242
/**
243-
* `getTags` returns valid semver version tags in order of the commit history
243+
* Returns an array of tags from the git log, filtered by an optional tag prefix and pre-release configuration.
244244
*
245-
* Using `git log` to get the commit history, we then parse the tags from the
246-
* commit details which is expected to be in the following format:
247-
* ```txt
248-
* commit 3841b1d05750d42197fe958e3d8e06df378a842d (HEAD -> main, tag: v1.0.2, tag: v1.0.1, tag: v1.0.0)
249-
* Author: Username <username@example.com>
250-
* Date: Sat Nov 9 15:00:00 2024 +0000
245+
* @example
246+
* ```ts
247+
* // Given the following git log: (HEAD -> main, tag: v1.0.2, tag: v1.0.1, tag: v1.0.0)
248+
* await git.getTags("v"); // ["v1.0.2", "v1.0.1", "v1.0.0"]
249+
*
250+
* // Given the following git log: (HEAD -> main, tag: v1.0.2-0, tag: v1.0.1-0, tag: v1.0.0)
251+
* await git.getTags("v", true); // ["v1.0.2-0", "v1.0.1-0", "v1.0.0"]
251252
*
252-
* chore(release): v1.0.0
253+
* // Given the following git log: (HEAD -> main, tag: v1.0.2-alpha.0, tag: v1.0.1-alpha.0, tag: v1.0.0)
254+
* await git.getTags("v", "alpha"); // ["v1.0.2-alpha.0", "v1.0.1-alpha.0", "v1.0.0"]
253255
* ```
254256
*
255257
* - [Functionality extracted from the conventional-changelog - git-semver-tags project](https://github.com/conventional-changelog/conventional-changelog/blob/fac8045242099c016f5f3905e54e02b7d466bd7b/packages/git-semver-tags/index.js)
256258
* - [conventional-changelog git-semver-tags MIT Licence](https://github.com/conventional-changelog/conventional-changelog/blob/fac8045242099c016f5f3905e54e02b7d466bd7b/packages/git-semver-tags/LICENSE.md)
257-
*
258-
* @example
259-
* ```ts
260-
* await git.getTags("v"); // ["v1.0.2", "v1.0.1", "v1.0.0"]
261-
* ```
262259
*/
263260
async getTags(tagPrefix?: string, preRelease?: string | boolean): Promise<string[]> {
264-
const logOutput = await this.log("--decorate", "--no-color", "--date-order");
261+
const logOutput = await this.log("--format=%d", "--no-color", "--date-order");
265262

266263
/**
267264
* Search for tags in the following formats:
@@ -292,20 +289,22 @@ export class Git {
292289
}
293290

294291
/**
295-
* Get commit history in a parsable format
292+
* Returns an array of commits between two git references (e.g., tags, branches, or commits), optionally filtered by file paths.
296293
*
297-
* An array of strings with commit details is returned in the following format:
294+
* Each commit in the returned array is a string containing the following information, separated by newlines:
298295
* ```txt
299296
* subject
300297
* body
301298
* hash
299+
* ref names
302300
* committer date
303301
* committer name
304302
* committer email
305303
* ```
306304
*
307305
* @example
308306
* ```ts
307+
* await git.getCommits("v1.0.0", "HEAD");
309308
* await git.getCommits("v1.0.0", "HEAD", "src/utils");
310309
* ```
311310
*/
@@ -323,14 +322,16 @@ export class Git {
323322
SCISSOR,
324323
].join("%n");
325324

326-
const commits = await this.log(
325+
const logOutput = await this.log(
327326
`--format=${LOG_FORMAT}`,
327+
"--no-color",
328+
"--date-order",
328329
[from, to].filter(Boolean).join(".."),
329330
paths.length ? "--" : "",
330331
...paths,
331332
);
332333

333-
const splitCommits = commits.split(`\n${SCISSOR}\n`);
334+
const splitCommits = logOutput.split(`\n${SCISSOR}\n`);
334335

335336
if (splitCommits.length === 0) {
336337
return splitCommits;

tests/options.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/**
2+
* Environment option to disable brittle tests that may rely on timing, such as those that check commit order.
3+
*
4+
* To enable, set the environment variable `e2e` to "true" when running tests.
5+
*
6+
* @example
7+
* ```bash
8+
* e2e=true pnpm test
9+
* ```
10+
*
11+
* @example
12+
* ```pwsh
13+
* $env:e2e="true"; pnpm test
14+
* ```
15+
*/
16+
export const IS_E2E = process.env.e2e === "true";

tests/setup-tests.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,5 +239,16 @@ export async function setupTest(testName: string, options?: Partial<ISetupTestOp
239239
execSync(`git tag --annotate "${tag}" --message "${message}"`, execSyncOptions);
240240
},
241241
},
242+
243+
/**
244+
* Sleep for a specified number of milliseconds.
245+
* @example
246+
* ```js
247+
* await sleep(1000); // Sleep for 1 second
248+
* ```
249+
*/
250+
sleep: function _sleep(ms: number) {
251+
return new Promise((resolve) => setTimeout(resolve, ms));
252+
},
242253
};
243254
}

0 commit comments

Comments
 (0)