wrangler-action/node_modules/@changesets/git/dist/git.esm.js
2023-08-07 15:11:15 -05:00

272 lines
7.9 KiB
JavaScript

import spawn from 'spawndamnit';
import fs from 'fs';
import path from 'path';
import { getPackages } from '@manypkg/get-packages';
import { GitError } from '@changesets/errors';
import isSubdir from 'is-subdir';
import micromatch from 'micromatch';
async function add(pathToFile, cwd) {
const gitCmd = await spawn("git", ["add", pathToFile], {
cwd
});
if (gitCmd.code !== 0) {
console.log(pathToFile, gitCmd.stderr.toString());
}
return gitCmd.code === 0;
}
async function commit(message, cwd) {
const gitCmd = await spawn("git", ["commit", "-m", message, "--allow-empty"], {
cwd
});
return gitCmd.code === 0;
}
async function getAllTags(cwd) {
const gitCmd = await spawn("git", ["tag"], {
cwd
});
if (gitCmd.code !== 0) {
throw new Error(gitCmd.stderr.toString());
}
const tags = gitCmd.stdout.toString().trim().split("\n");
return new Set(tags);
} // used to create a single tag at a time for the current head only
async function tag(tagStr, cwd) {
// NOTE: it's important we use the -m flag to create annotated tag otherwise 'git push --follow-tags' won't actually push
// the tags
const gitCmd = await spawn("git", ["tag", tagStr, "-m", tagStr], {
cwd
});
return gitCmd.code === 0;
} // Find the commit where we diverged from `ref` at using `git merge-base`
async function getDivergedCommit(cwd, ref) {
const cmd = await spawn("git", ["merge-base", ref, "HEAD"], {
cwd
});
if (cmd.code !== 0) {
throw new Error(`Failed to find where HEAD diverged from ${ref}. Does ${ref} exist?`);
}
return cmd.stdout.toString().trim();
}
/**
* Get the SHAs for the commits that added files, including automatically
* extending a shallow clone if necessary to determine any commits.
* @param gitPaths - Paths to fetch
* @param options - `cwd` and `short`
*/
async function getCommitsThatAddFiles(gitPaths, {
cwd,
short = false
}) {
// Maps gitPath to commit SHA
const map = new Map(); // Paths we haven't completed processing on yet
let remaining = gitPaths;
do {
// Fetch commit information for all paths we don't have yet
const commitInfos = await Promise.all(remaining.map(async gitPath => {
const [commitSha, parentSha] = (await spawn("git", ["log", "--diff-filter=A", "--max-count=1", short ? "--pretty=format:%h:%p" : "--pretty=format:%H:%p", gitPath], {
cwd
})).stdout.toString().split(":");
return {
path: gitPath,
commitSha,
parentSha
};
})); // To collect commits without parents (usually because they're absent from
// a shallow clone).
let 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.
map.set(info.path, info.commitSha);
} else {
commitsWithMissingParents.push(info);
}
}
}
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({
cwd
})) {
// Yes.
await deepenCloneBy({
by: 50,
cwd
});
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) {
map.set(unresolved.path, unresolved.commitSha);
}
break;
}
} while (true);
return gitPaths.map(p => map.get(p));
}
async function isRepoShallow({
cwd
}) {
const isShallowRepoOutput = (await spawn("git", ["rev-parse", "--is-shallow-repository"], {
cwd
})).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 spawn("git", ["rev-parse", "--git-dir"], {
cwd
})).stdout.toString().trim();
const fullGitDir = path.resolve(cwd, gitDir); // Check for the existence of <gitDir>/shallow
return fs.existsSync(path.join(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";
}
}
async function deepenCloneBy({
by,
cwd
}) {
await spawn("git", ["fetch", `--deepen=${by}`], {
cwd
});
}
async function getRepoRoot({
cwd
}) {
const {
stdout,
code,
stderr
} = await spawn("git", ["rev-parse", "--show-toplevel"], {
cwd
});
if (code !== 0) {
throw new Error(stderr.toString());
}
return stdout.toString().trim().replace(/\n|\r/g, "");
}
async function getChangedFilesSince({
cwd,
ref,
fullPath = false
}) {
const divergedAt = await getDivergedCommit(cwd, ref); // Now we can find which files we added
const cmd = await spawn("git", ["diff", "--name-only", divergedAt], {
cwd
});
if (cmd.code !== 0) {
throw new Error(`Failed to diff against ${divergedAt}. Is ${divergedAt} a valid ref?`);
}
const files = cmd.stdout.toString().trim().split("\n").filter(a => a);
if (!fullPath) return files;
const repoRoot = await getRepoRoot({
cwd
});
return files.map(file => path.resolve(repoRoot, file));
} // below are less generic functions that we use in combination with other things we are doing
async function getChangedChangesetFilesSinceRef({
cwd,
ref
}) {
try {
const divergedAt = await getDivergedCommit(cwd, ref); // Now we can find which files we added
const cmd = await spawn("git", ["diff", "--name-only", "--diff-filter=d", divergedAt], {
cwd
});
let tester = /.changeset\/[^/]+\.md$/;
const files = cmd.stdout.toString().trim().split("\n").filter(file => tester.test(file));
return files;
} catch (err) {
if (err instanceof GitError) return [];
throw err;
}
}
async function getChangedPackagesSinceRef({
cwd,
ref,
changedFilePatterns = ["**"]
}) {
const changedFiles = await getChangedFilesSince({
ref,
cwd,
fullPath: true
});
return [...(await getPackages(cwd)).packages] // sort packages by length of dir, so that we can check for subdirs first
.sort((pkgA, pkgB) => pkgB.dir.length - pkgA.dir.length).filter(pkg => {
const changedPackageFiles = [];
for (let i = changedFiles.length - 1; i >= 0; i--) {
const file = changedFiles[i];
if (isSubdir(pkg.dir, file)) {
changedFiles.splice(i, 1);
const relativeFile = file.slice(pkg.dir.length + 1);
changedPackageFiles.push(relativeFile);
}
}
return changedPackageFiles.length > 0 && micromatch(changedPackageFiles, changedFilePatterns).length > 0;
});
}
async function tagExists(tagStr, cwd) {
const gitCmd = await spawn("git", ["tag", "-l", tagStr], {
cwd
});
const output = gitCmd.stdout.toString().trim();
const tagExists = !!output;
return tagExists;
}
async function getCurrentCommitId({
cwd,
short = false
}) {
return (await spawn("git", ["rev-parse", short && "--short", "HEAD"].filter(Boolean), {
cwd
})).stdout.toString().trim();
}
async function remoteTagExists(tagStr) {
const gitCmd = await spawn("git", ["ls-remote", "--tags", "origin", "-l", tagStr]);
const output = gitCmd.stdout.toString().trim();
const tagExists = !!output;
return tagExists;
}
export { add, commit, deepenCloneBy, getAllTags, getChangedChangesetFilesSinceRef, getChangedFilesSince, getChangedPackagesSinceRef, getCommitsThatAddFiles, getCurrentCommitId, getDivergedCommit, isRepoShallow, remoteTagExists, tag, tagExists };