glu-cli
Version:
Git stacked branch management with GitHub integration
195 lines • 7.23 kB
JavaScript
import { GitError, GitErrorType } from "../core/errors/git-error.js";
import { ValidationError } from "../core/errors/validation-error.js";
export class CommitService {
git;
constructor(git) {
this.git = git;
}
// MARK: Queries
async getCurrentBranch() {
return await this.git.getCurrentBranch();
}
async getUpstreamBranch(branch) {
try {
return await this.git.getUpstreamBranch(branch);
}
catch (error) {
throw new GitError(GitErrorType.ORIGIN_NOT_FOUND, {
underlyingError: error,
});
}
}
async getUnpushedCommits() {
const currentBranch = await this.git.getCurrentBranch();
if (currentBranch === "HEAD") {
throw new GitError(GitErrorType.DETACHED_HEAD, {
operation: "get_unpushed_commits",
});
}
const upstream = await this.git.getUpstreamBranch(currentBranch);
try {
await this.git.revparse(["--verify", upstream]);
}
catch {
throw new GitError(GitErrorType.ORIGIN_NOT_FOUND, {
branch: currentBranch,
upstreamBranch: upstream,
});
}
// commits come in order of newest-to-earliest, so flip
const commits = (await this.git.getCommitRange(upstream, "HEAD")).reverse();
return commits.map((commit, index) => ({
...commit,
gluIndex: index + 1,
}));
}
async getAheadBehindStatus() {
const localRef = await this.git.getCurrentBranch();
const remoteRef = await this.git.getUpstreamBranch(localRef);
try {
const command = [
"rev-list",
"--left-right",
"--count",
`${localRef}...${remoteRef}`,
];
const result = await this.git.raw(command);
const trimmed = result.trim();
const parts = trimmed.split("\t");
if (parts.length !== 2 || !parts[0] || !parts[1]) {
throw new GitError(GitErrorType.INVALID_COMMIT_RANGE, {
command: command.join(" "),
output: result,
localRef,
remoteRef,
});
}
const ahead = parseInt(parts[0], 10);
const behind = parseInt(parts[1], 10);
if (isNaN(ahead) || isNaN(behind)) {
throw new GitError(GitErrorType.COMMAND_FAILED, {
command: command.join(" "),
output: result,
parsedAhead: parts[0],
parsedBehind: parts[1],
localRef,
remoteRef,
});
}
return {
ahead,
behind,
isAhead: ahead > 0,
isBehind: behind > 0,
isUpToDate: ahead === 0 && behind === 0,
};
}
catch (error) {
if (error instanceof GitError) {
throw error;
}
if (error instanceof Error &&
error.message.includes("unknown revision")) {
throw new GitError(GitErrorType.BRANCH_NOT_FOUND, {
branch: localRef,
upstream: remoteRef,
originalError: error,
});
}
throw new GitError(GitErrorType.BRANCH_COMPARISON_FAILED, {
branch: localRef,
upstream: remoteRef,
originalError: error,
});
}
}
async getCommitsBetween(from, to) {
return [];
}
// MARK: Commit Range
parseRange(range) {
const trimmed = range.trim();
// Validate format
if (!/^\d+(-\d+)?$/.test(trimmed)) {
throw new ValidationError('Invalid range format. Use "n" or "n-m" where n and m are numbers.', { range, format: "expected n or n-m" });
}
// Parse single number
if (!trimmed.includes("-")) {
const single = parseInt(trimmed);
if (isNaN(single)) {
throw new ValidationError("Invalid number in range", { range });
}
return {
startIndex: single - 1, // Convert to 0-based
endIndex: single - 1,
};
}
// Parse range
const parts = trimmed.split("-");
if (parts.length !== 2) {
throw new ValidationError('Invalid range format. Use "n-m" for ranges.', {
range,
});
}
const start = parseInt(parts[0]);
const end = parseInt(parts[1]);
if (isNaN(start) || isNaN(end)) {
throw new ValidationError("Invalid numbers in range", { range });
}
if (start > end) {
throw new ValidationError("Start index must be less than or equal to end index", { range, start, end });
}
if (start < 1) {
throw new ValidationError("Indices must start from 1", { range, start });
}
return {
startIndex: start - 1, // Convert to 0-based
endIndex: end - 1,
};
}
validateRangeBounds(parsed, commits) {
if (commits.length === 0) {
throw new ValidationError("No commits available", { parsed });
}
if (parsed.startIndex < 0 || parsed.startIndex >= commits.length) {
throw new ValidationError(`Start index ${parsed.startIndex + 1} is out of range (1-${commits.length})`, { range: parsed, availableCommits: commits.length });
}
if (parsed.endIndex < 0 || parsed.endIndex >= commits.length) {
throw new ValidationError(`End index ${parsed.endIndex + 1} is out of range (1-${commits.length})`, { range: parsed, availableCommits: commits.length });
}
}
async getCommitsInRange(range) {
const parsed = this.parseRange(range);
const allCommits = await this.getUnpushedCommits();
this.validateRangeBounds(parsed, allCommits);
return allCommits.slice(parsed.startIndex, parsed.endIndex + 1);
}
// MARK: Working Directory
async isWorkingDirectoryClean() {
const status = await this.git.getStatus();
return status.isClean();
}
async getWorkingDirectoryStatus() {
const status = await this.git.getStatus();
return {
isClean: status.isClean(),
modified: status.modified,
staged: status.staged,
created: status.created,
deleted: status.deleted,
untracked: status.not_added,
conflicted: status.conflicted,
};
}
async requireCleanWorkingDirectory() {
const status = await this.getWorkingDirectoryStatus();
if (!status.isClean) {
throw new GitError(GitErrorType.DIRTY_WORKING_DIRECTORY, {
modified: status.modified,
staged: status.staged,
untracked: status.untracked,
});
}
}
}
//# sourceMappingURL=commit-service.js.map