Skip to content

Commit

Permalink
Fix: Not resolving commits on shallow clones (#102)
Browse files Browse the repository at this point in the history
  • Loading branch information
timotheeguerin authored Mar 3, 2024
1 parent 53dd702 commit a349e0e
Show file tree
Hide file tree
Showing 4 changed files with 100 additions and 6 deletions.
6 changes: 6 additions & 0 deletions .chronus/changes/fix-get-commits-2024-2-3-23-6-47.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
# Change versionKind to one of: breaking, feature, fix, internal
changeKind: internal
packages:
- "@chronus/chronus"
---
5 changes: 5 additions & 0 deletions .github/workflows/codeql-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ on:
schedule:
- cron: "35 14 * * 3"

permissions:
actions: read
contents: read
security-events: write

jobs:
analyze:
name: Analyze
Expand Down
2 changes: 1 addition & 1 deletion eng/prepare-release-branch.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { showStatusAsMarkdown } from "../packages/chronus/dist/cli/commands/show
const branchName = "publish/auto-release";

const changeStatus = await showStatusAsMarkdown(process.cwd());
execSync(`pnpm change version`);
execSync(`pnpm change version`, { stdio: "inherit" });
const stdout = execSync(`git status --porcelain`).toString();

if (stdout.trim() !== "") {
Expand Down
93 changes: 88 additions & 5 deletions packages/chronus/src/source-control/git.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { existsSync } from "fs";
import { execAsync, type ExecResult } from "../utils/exec-async.js";
import { resolvePath } from "../utils/path-utils.js";

export interface GitRepository {
/**
Expand Down Expand Up @@ -196,14 +198,95 @@ export function createGitSourceControl(repositoryPath: string): GitRepository {
return trimSingleLine(await execGit(["rev-parse", "--abbrev-ref", "HEAD"], { repositoryPath }));
}

async function getCommitsThatAddFiles(files: string[]) {
async function getCommitsThatAddFiles(files: string[]): Promise<Record<string, string>> {
const commits: Record<string, string> = {};
for (const file of files) {
const { stdout } = await execGit(["log", "--format=%H", "--", file], { repositoryPath });
commits[file] = stdout.toString().trim();
}
// Paths we haven't completed processing on yet
let remaining = files;

do {
// Fetch commit information for all paths we don't have yet
const commitInfos = await Promise.all(
remaining.map(async (gitPath: string) => {
const [commitSha, parentSha] = trimSingleLine(
await execGit(["log", "--diff-filter=A", "--max-count=1", "--format=%H:%p", gitPath], { repositoryPath }),
).split(":");
return { path: gitPath, commitSha, parentSha };
}),
);

// To collect commits without parents (usually because they're absent from
// a shallow clone).
const commitsWithMissingParents = [];

for (const info of commitInfos) {
if (info.commitSha) {
if (info.parentSha) {
// We have found the parent of the commit that added the file.
// Therefore we know that the commit is legitimate and isn't simply the boundary of a shallow clone.
commits[info.path] = info.commitSha;
} else {
commitsWithMissingParents.push(info);
}
} else {
// No commit for this file, which indicates it doesn't exist.
}
}

if (commitsWithMissingParents.length === 0) {
break;
}

// The commits we've found may be the real commits or they may be the boundary of
// a shallow clone.

// Can we deepen the clone?
if (await isRepoShallow({ repositoryPath })) {
// Yes.
await deepenCloneBy({ by: 50, repositoryPath });

remaining = commitsWithMissingParents.map((p) => p.path);
} else {
// It's not a shallow clone, so all the commit SHAs we have are legitimate.
for (const unresolved of commitsWithMissingParents) {
commits[unresolved.path] = unresolved.commitSha;
}
break;
}
// eslint-disable-next-line no-constant-condition
} while (true);
return commits;
}

async function deepenCloneBy({ by, repositoryPath }: { by: number; repositoryPath: string }) {
await execGit(["fetch", `--deepen=${by}`], { repositoryPath });
}

async function isRepoShallow({ repositoryPath }: { repositoryPath: string }) {
const isShallowRepoOutput = (
await execGit(["rev-parse", "--is-shallow-repository"], {
repositoryPath,
})
).stdout
.toString()
.trim();

if (isShallowRepoOutput === "--is-shallow-repository") {
// We have an old version of Git (<2.15) which doesn't support `rev-parse --is-shallow-repository`
// In that case, we'll test for the existence of .git/shallow.

// Firstly, find the .git folder for the repo; note that this will be relative to the repo dir
const gitDir = (await execGit(["rev-parse", "--git-dir"], { repositoryPath })).stdout.toString().trim();

const fullGitDir = resolvePath(repositoryPath, gitDir);

// Check for the existence of <gitDir>/shallow
return existsSync(resolvePath(fullGitDir, "shallow"));
} else {
// We have a newer Git which supports `rev-parse --is-shallow-repository`. We'll use
// the output of that instead of messing with .git/shallow in case that changes in the future.
return isShallowRepoOutput === "true";
}
}
}

function splitStdoutLines(result: ExecResult): string[] {
Expand Down

0 comments on commit a349e0e

Please sign in to comment.