workspace-tools
Version:
A collection of tools that are useful in a git-controlled monorepo that is managed by one of these software:
451 lines (450 loc) • 14.8 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const child_process_1 = require("child_process");
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const paths_1 = require("./paths");
const git_url_parse_1 = __importDefault(require("git-url-parse"));
/**
* A maxBuffer override globally for all git operations
* Bumps up the default to 500MB as opposed to the 1MB
* Override this value with "GIT_MAX_BUFFER" environment variable
*/
const MaxBufferOption = process.env.GIT_MAX_BUFFER ? parseInt(process.env.GIT_MAX_BUFFER) : 500 * 1024 * 1024;
const observers = [];
let observing;
/**
* Adds an observer for the git operations, e.g. for testing
* @param observer
*/
function addGitObserver(observer) {
observers.push(observer);
}
exports.addGitObserver = addGitObserver;
/**
* Runs git command - use this for read only commands
*/
function git(args, options) {
const results = child_process_1.spawnSync("git", args, Object.assign({ maxBuffer: MaxBufferOption }, options));
let output;
if (results.status === 0) {
output = {
stderr: results.stderr.toString().trimRight(),
stdout: results.stdout.toString().trimRight(),
success: true,
};
}
else {
output = {
stderr: results.stderr.toString().trimRight(),
stdout: results.stdout.toString().trimRight(),
success: false,
};
}
// notify observers, flipping the observing bit to prevent infinite loops
if (!observing) {
observing = true;
for (const observer of observers) {
observer(args, output);
}
observing = false;
}
return output;
}
exports.git = git;
/**
* Runs git command - use this for commands that makes changes to the file system
*/
function gitFailFast(args, options) {
const gitResult = git(args, options);
if (!gitResult.success) {
process.exitCode = 1;
throw new Error(`CRITICAL ERROR: running git command: git ${args.join(" ")}!
${gitResult.stdout && gitResult.stdout.toString().trimRight()}
${gitResult.stderr && gitResult.stderr.toString().trimRight()}`);
}
}
exports.gitFailFast = gitFailFast;
function getUntrackedChanges(cwd) {
try {
const results = git(["status", "--short"], { cwd });
if (!results.success) {
return [];
}
const changes = results.stdout;
if (changes.length == 0) {
return [];
}
const lines = changes.split(/\0/).filter((line) => line) || [];
const untracked = [];
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (line[0] === " " || line[0] === "?") {
untracked.push(line.substr(3));
}
else if (line[0] === "R") {
i++;
}
}
return untracked;
}
catch (e) {
throw new Error(`Cannot gather information about untracked changes: ${e.message}`);
}
}
exports.getUntrackedChanges = getUntrackedChanges;
function fetchRemote(remote, cwd) {
const results = git(["fetch", remote], { cwd });
if (!results.success) {
throw new Error(`Cannot fetch remote: ${remote}`);
}
}
exports.fetchRemote = fetchRemote;
function fetchRemoteBranch(remote, remoteBranch, cwd) {
const results = git(["fetch", remote, remoteBranch], { cwd });
if (!results.success) {
throw new Error(`Cannot fetch remote: ${remote} ${remoteBranch}`);
}
}
exports.fetchRemoteBranch = fetchRemoteBranch;
/**
* Gets all the changes that have not been staged yet
* @param cwd
*/
function getUnstagedChanges(cwd) {
try {
return processGitOutput(git(["--no-pager", "diff", "--name-only", "--relative"], { cwd }));
}
catch (e) {
throw new Error(`Cannot gather information about unstaged changes: ${e.message}`);
}
}
exports.getUnstagedChanges = getUnstagedChanges;
function getChanges(branch, cwd) {
try {
return processGitOutput(git(["--no-pager", "diff", "--relative", "--name-only", branch + "..."], { cwd }));
}
catch (e) {
throw new Error(`Cannot gather information about changes: ${e.message}`);
}
}
exports.getChanges = getChanges;
/**
* Gets all the changes between the branch and the merge-base
* @param branch
* @param cwd
*/
function getBranchChanges(branch, cwd) {
try {
return processGitOutput(git(["--no-pager", "diff", "--name-only", "--relative", branch + "..."], { cwd }));
}
catch (e) {
throw new Error(`Cannot gather information about branch changes: ${e.message}`);
}
}
exports.getBranchChanges = getBranchChanges;
function getChangesBetweenRefs(fromRef, toRef, options, pattern, cwd) {
try {
return processGitOutput(git(["--no-pager", "diff", "--relative", "--name-only", ...options, `${fromRef}...${toRef}`, "--", pattern], {
cwd,
}));
}
catch (e) {
throw new Error(`Cannot gather information about change between refs changes (${fromRef} to ${toRef}): ${e.message}`);
}
}
exports.getChangesBetweenRefs = getChangesBetweenRefs;
function getStagedChanges(cwd) {
try {
return processGitOutput(git(["--no-pager", "diff", "--relative", "--staged", "--name-only"], { cwd }));
}
catch (e) {
throw new Error(`Cannot gather information about staged changes: ${e.message}`);
}
}
exports.getStagedChanges = getStagedChanges;
function getRecentCommitMessages(branch, cwd) {
try {
const results = git(["log", "--decorate", "--pretty=format:%s", `${branch}..HEAD`], { cwd });
if (!results.success) {
return [];
}
let changes = results.stdout;
let lines = changes.split(/\n/) || [];
return lines.map((line) => line.trim());
}
catch (e) {
throw new Error(`Cannot gather information about recent commits: ${e.message}`);
}
}
exports.getRecentCommitMessages = getRecentCommitMessages;
function getUserEmail(cwd) {
try {
const results = git(["config", "user.email"], { cwd });
if (!results.success) {
return null;
}
return results.stdout;
}
catch (e) {
throw new Error(`Cannot gather information about user.email: ${e.message}`);
}
}
exports.getUserEmail = getUserEmail;
function getBranchName(cwd) {
try {
const results = git(["rev-parse", "--abbrev-ref", "HEAD"], { cwd });
if (results.success) {
return results.stdout;
}
}
catch (e) {
throw new Error(`Cannot get branch name: ${e.message}`);
}
return null;
}
exports.getBranchName = getBranchName;
function getFullBranchRef(branch, cwd) {
const showRefResults = git(["show-ref", "--heads", branch], { cwd });
if (showRefResults.success) {
return showRefResults.stdout.split(" ")[1];
}
return null;
}
exports.getFullBranchRef = getFullBranchRef;
function getShortBranchName(fullBranchRef, cwd) {
const showRefResults = git(["name-rev", "--name-only", fullBranchRef], {
cwd,
});
if (showRefResults.success) {
return showRefResults.stdout;
}
return null;
}
exports.getShortBranchName = getShortBranchName;
function getCurrentHash(cwd) {
try {
const results = git(["rev-parse", "HEAD"], { cwd });
if (results.success) {
return results.stdout;
}
}
catch (e) {
throw new Error(`Cannot get current git hash: ${e.message}`);
}
return null;
}
exports.getCurrentHash = getCurrentHash;
/**
* Get the commit hash in which the file was first added.
*/
function getFileAddedHash(filename, cwd) {
const results = git(["rev-list", "HEAD", filename], { cwd });
if (results.success) {
return results.stdout.trim().split("\n").slice(-1)[0];
}
return undefined;
}
exports.getFileAddedHash = getFileAddedHash;
function init(cwd, email, username) {
git(["init"], { cwd });
const configLines = git(["config", "--list"], { cwd }).stdout.split("\n");
if (!configLines.find((line) => line.includes("user.name"))) {
if (!username) {
throw new Error("must include a username when initializing git repo");
}
git(["config", "user.name", username], { cwd });
}
if (!configLines.find((line) => line.includes("user.email"))) {
if (!email) {
throw new Error("must include a email when initializing git repo");
}
git(["config", "user.email", email], { cwd });
}
}
exports.init = init;
function stage(patterns, cwd) {
try {
patterns.forEach((pattern) => {
git(["add", pattern], { cwd });
});
}
catch (e) {
throw new Error(`Cannot stage changes: ${e.message}`);
}
}
exports.stage = stage;
function commit(message, cwd, options = []) {
try {
const commitResults = git(["commit", "-m", message, ...options], { cwd });
if (!commitResults.success) {
throw new Error(`Cannot commit changes: ${commitResults.stdout} ${commitResults.stderr}`);
}
}
catch (e) {
throw new Error(`Cannot commit changes: ${e.message}`);
}
}
exports.commit = commit;
function stageAndCommit(patterns, message, cwd, commitOptions = []) {
stage(patterns, cwd);
commit(message, cwd, commitOptions);
}
exports.stageAndCommit = stageAndCommit;
function revertLocalChanges(cwd) {
const stash = `beachball_${new Date().getTime()}`;
git(["stash", "push", "-u", "-m", stash], { cwd });
const results = git(["stash", "list"]);
if (results.success) {
const lines = results.stdout.split(/\n/);
const foundLine = lines.find((line) => line.includes(stash));
if (foundLine) {
const matched = foundLine.match(/^[^:]+/);
if (matched) {
git(["stash", "drop", matched[0]]);
return true;
}
}
}
return false;
}
exports.revertLocalChanges = revertLocalChanges;
function getParentBranch(cwd) {
const branchName = getBranchName(cwd);
if (!branchName || branchName === "HEAD") {
return null;
}
const showBranchResult = git(["show-branch", "-a"], { cwd });
if (showBranchResult.success) {
const showBranchLines = showBranchResult.stdout.split(/\n/);
const parentLine = showBranchLines.find((line) => line.indexOf("*") > -1 && line.indexOf(branchName) < 0 && line.indexOf("publish_") < 0);
if (!parentLine) {
return null;
}
const matched = parentLine.match(/\[(.*)\]/);
if (!matched) {
return null;
}
return matched[1];
}
return null;
}
exports.getParentBranch = getParentBranch;
function getRemoteBranch(branch, cwd) {
const results = git(["rev-parse", "--abbrev-ref", "--symbolic-full-name", `${branch}@\{u\}`], { cwd });
if (results.success) {
return results.stdout.trim();
}
return null;
}
exports.getRemoteBranch = getRemoteBranch;
function parseRemoteBranch(branch) {
const firstSlashPos = branch.indexOf("/", 0);
const remote = branch.substring(0, firstSlashPos);
const remoteBranch = branch.substring(firstSlashPos + 1);
return {
remote,
remoteBranch,
};
}
exports.parseRemoteBranch = parseRemoteBranch;
function normalizeRepoUrl(repositoryUrl) {
try {
const parsed = git_url_parse_1.default(repositoryUrl);
return parsed
.toString("https")
.replace(/\.git$/, "")
.toLowerCase();
}
catch (e) {
return "";
}
}
function getDefaultRemoteBranch(branch, cwd) {
const defaultRemote = getDefaultRemote(cwd);
const showRemote = git(["remote", "show", defaultRemote], { cwd });
/**
* The `showRemote` returns something like this in stdout:
*
* * remote origin
* Fetch URL: ../monorepo-upstream/
* Push URL: ../monorepo-upstream/
* HEAD branch: main
*
*/
const headBranchLine = showRemote.stdout.split(/\n/).find(line => line.includes('HEAD branch'));
let remoteDefaultBranch;
if (headBranchLine) {
remoteDefaultBranch = headBranchLine.replace(/^\s*HEAD branch:\s+/, '');
}
branch = branch || remoteDefaultBranch || getDefaultBranch(cwd);
return `${defaultRemote}/${branch}`;
}
exports.getDefaultRemoteBranch = getDefaultRemoteBranch;
function getDefaultBranch(cwd) {
const result = git(["config", "init.defaultBranch"], { cwd });
if (!result.success) {
// Default to the legacy 'master' for backwards compat and old git clients
return "master";
}
return result.stdout.trim();
}
exports.getDefaultBranch = getDefaultBranch;
function getDefaultRemote(cwd) {
let packageJson;
try {
packageJson = JSON.parse(fs_1.default.readFileSync(path_1.default.join(paths_1.findGitRoot(cwd), "package.json")).toString());
}
catch (e) {
throw new Error("invalid package.json detected");
}
const { repository } = packageJson;
let repositoryUrl = "";
if (typeof repository === "string") {
repositoryUrl = repository;
}
else if (repository && repository.url) {
repositoryUrl = repository.url;
}
const normalizedUrl = normalizeRepoUrl(repositoryUrl);
const remotesResult = git(["remote", "-v"], { cwd });
if (remotesResult.success) {
const allRemotes = {};
remotesResult.stdout.split("\n").forEach((line) => {
const parts = line.split(/\s+/);
allRemotes[normalizeRepoUrl(parts[1])] = parts[0];
});
if (Object.keys(allRemotes).length > 0) {
const remote = allRemotes[normalizedUrl];
if (remote) {
return remote;
}
}
}
return "origin";
}
exports.getDefaultRemote = getDefaultRemote;
function listAllTrackedFiles(patterns, cwd) {
if (patterns) {
const results = git(["ls-files", ...patterns], { cwd });
if (results.success) {
return results.stdout.split(/\n/);
}
}
return [];
}
exports.listAllTrackedFiles = listAllTrackedFiles;
function processGitOutput(output) {
if (!output.success) {
return [];
}
let stdout = output.stdout;
let lines = stdout.split(/\n/) || [];
return lines
.filter((line) => line.trim() !== "")
.map((line) => line.trim())
.filter((line) => !line.includes("node_modules"));
}