mirror of
https://github.com/Dictionarry-Hub/profilarr.git
synced 2026-01-24 19:51:03 +01:00
244 lines
6.5 KiB
TypeScript
244 lines
6.5 KiB
TypeScript
/**
|
|
* Git status queries (read-only)
|
|
*/
|
|
|
|
import { execGit, execGitSafe } from './exec.ts';
|
|
import { fetch } from './repo.ts';
|
|
import type { GitStatus, UpdateInfo, Commit } from './types.ts';
|
|
|
|
/**
|
|
* Get current branch name
|
|
*/
|
|
export async function getBranch(repoPath: string): Promise<string> {
|
|
return await execGit(['branch', '--show-current'], repoPath);
|
|
}
|
|
|
|
export interface GetStatusOptions {
|
|
/** Whether to fetch from remote first (slower but accurate ahead/behind) */
|
|
fetch?: boolean;
|
|
}
|
|
|
|
/**
|
|
* Get full repository status
|
|
*/
|
|
export async function getStatus(repoPath: string, options: GetStatusOptions = {}): Promise<GitStatus> {
|
|
const branch = await getBranch(repoPath);
|
|
|
|
// Optionally fetch to get accurate ahead/behind
|
|
if (options.fetch) {
|
|
await fetch(repoPath);
|
|
}
|
|
|
|
// Get ahead/behind
|
|
let ahead = 0;
|
|
let behind = 0;
|
|
const revOutput = await execGitSafe(
|
|
['rev-list', '--left-right', '--count', `origin/${branch}...HEAD`],
|
|
repoPath
|
|
);
|
|
if (revOutput) {
|
|
const parts = revOutput.split('\t').map(n => parseInt(n, 10) || 0);
|
|
behind = parts[0] || 0;
|
|
ahead = parts[1] || 0;
|
|
}
|
|
|
|
// Get file status
|
|
const statusOutput = await execGit(['status', '--porcelain'], repoPath);
|
|
const untracked: string[] = [];
|
|
const modified: string[] = [];
|
|
const staged: string[] = [];
|
|
|
|
for (const line of statusOutput.split('\n')) {
|
|
if (!line.trim()) continue;
|
|
|
|
const status = line.substring(0, 2);
|
|
const file = line.substring(3);
|
|
|
|
if (status.startsWith('??')) {
|
|
untracked.push(file);
|
|
} else if (status[1] === 'M' || status[1] === 'D') {
|
|
modified.push(file);
|
|
}
|
|
if (status[0] === 'M' || status[0] === 'A' || status[0] === 'D') {
|
|
staged.push(file);
|
|
}
|
|
}
|
|
|
|
const isDirty = untracked.length > 0 || modified.length > 0 || staged.length > 0;
|
|
|
|
return { branch, isDirty, ahead, behind, untracked, modified, staged };
|
|
}
|
|
|
|
/**
|
|
* Check for updates from remote
|
|
*/
|
|
export async function checkForUpdates(repoPath: string): Promise<UpdateInfo> {
|
|
await fetch(repoPath);
|
|
|
|
const branch = await getBranch(repoPath);
|
|
const remoteBranch = `origin/${branch}`;
|
|
|
|
const currentLocalCommit = await execGit(['rev-parse', 'HEAD'], repoPath);
|
|
|
|
let latestRemoteCommit: string;
|
|
try {
|
|
latestRemoteCommit = await execGit(['rev-parse', remoteBranch], repoPath);
|
|
} catch {
|
|
return {
|
|
hasUpdates: false,
|
|
commitsBehind: 0,
|
|
commitsAhead: 0,
|
|
latestRemoteCommit: currentLocalCommit,
|
|
currentLocalCommit
|
|
};
|
|
}
|
|
|
|
const behindOutput = await execGitSafe(['rev-list', '--count', `HEAD..${remoteBranch}`], repoPath);
|
|
const commitsBehind = parseInt(behindOutput || '0') || 0;
|
|
|
|
const aheadOutput = await execGitSafe(['rev-list', '--count', `${remoteBranch}..HEAD`], repoPath);
|
|
const commitsAhead = parseInt(aheadOutput || '0') || 0;
|
|
|
|
return {
|
|
hasUpdates: commitsBehind > 0,
|
|
commitsBehind,
|
|
commitsAhead,
|
|
latestRemoteCommit,
|
|
currentLocalCommit
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get the last push timestamp (approximate via remote HEAD)
|
|
*/
|
|
export async function getLastPushed(repoPath: string): Promise<string | null> {
|
|
const branch = await getBranch(repoPath);
|
|
const output = await execGitSafe(['log', '-1', '--format=%cI', `origin/${branch}`], repoPath);
|
|
return output || null;
|
|
}
|
|
|
|
/**
|
|
* Get all local and remote branches
|
|
*/
|
|
export async function getBranches(repoPath: string): Promise<string[]> {
|
|
// Get all branches (local and remote tracking)
|
|
const output = await execGit(['branch', '-a', '--format=%(refname:short)'], repoPath);
|
|
const branches = output
|
|
.split('\n')
|
|
.map((b) => b.trim())
|
|
.filter((b) => b && !b.includes('HEAD'))
|
|
.map((b) => b.replace(/^origin\//, ''))
|
|
.filter((b, i, arr) => arr.indexOf(b) === i); // dedupe
|
|
|
|
return branches;
|
|
}
|
|
|
|
/**
|
|
* Check if a file is uncommitted (untracked or staged but not yet committed)
|
|
*/
|
|
export async function isFileUncommitted(repoPath: string, filepath: string): Promise<boolean> {
|
|
// Get relative path from repo root
|
|
const relativePath = filepath.startsWith(repoPath + '/')
|
|
? filepath.slice(repoPath.length + 1)
|
|
: filepath;
|
|
|
|
const output = await execGitSafe(['status', '--porcelain', relativePath], repoPath);
|
|
if (!output) return false;
|
|
|
|
const status = output.substring(0, 2);
|
|
// ?? = untracked, A = added (staged), AM = added and modified
|
|
return status.startsWith('??') || status[0] === 'A';
|
|
}
|
|
|
|
/**
|
|
* Get diff for specific files (or all uncommitted changes if no files specified)
|
|
* Handles both tracked (modified) and untracked (new) files
|
|
*/
|
|
export async function getDiff(repoPath: string, filepaths?: string[]): Promise<string> {
|
|
const diffs: string[] = [];
|
|
|
|
if (filepaths && filepaths.length > 0) {
|
|
for (const filepath of filepaths) {
|
|
const relativePath = filepath.startsWith(repoPath + '/')
|
|
? filepath.slice(repoPath.length + 1)
|
|
: filepath;
|
|
|
|
// Check if file is untracked
|
|
const status = await execGitSafe(['status', '--porcelain', relativePath], repoPath);
|
|
const isUntracked = status?.startsWith('??');
|
|
|
|
if (isUntracked) {
|
|
// For untracked files, show as new file diff
|
|
try {
|
|
const content = await Deno.readTextFile(`${repoPath}/${relativePath}`);
|
|
diffs.push(`diff --git a/${relativePath} b/${relativePath}
|
|
new file mode 100644
|
|
--- /dev/null
|
|
+++ b/${relativePath}
|
|
@@ -0,0 +1,${content.split('\n').length} @@
|
|
${content.split('\n').map(line => '+' + line).join('\n')}`);
|
|
} catch {
|
|
// File doesn't exist or can't be read
|
|
}
|
|
} else {
|
|
// For tracked files, use git diff
|
|
const diff = await execGitSafe(['diff', 'HEAD', '--', relativePath], repoPath);
|
|
if (diff) {
|
|
diffs.push(diff);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// No specific files, get all changes
|
|
const diff = await execGitSafe(['diff', 'HEAD'], repoPath);
|
|
if (diff) {
|
|
diffs.push(diff);
|
|
}
|
|
}
|
|
|
|
return diffs.join('\n\n');
|
|
}
|
|
|
|
/**
|
|
* Get commit history
|
|
*/
|
|
export async function getCommits(repoPath: string, limit: number = 50): Promise<Commit[]> {
|
|
// Format: hash|shortHash|message|author|email|date
|
|
const format = '%H|%h|%s|%an|%ae|%cI';
|
|
const output = await execGit(
|
|
['log', `--format=${format}`, `-${limit}`],
|
|
repoPath
|
|
);
|
|
|
|
if (!output.trim()) {
|
|
return [];
|
|
}
|
|
|
|
const commits: Commit[] = [];
|
|
|
|
for (const line of output.split('\n')) {
|
|
if (!line.trim()) continue;
|
|
|
|
const [hash, shortHash, message, author, authorEmail, date] = line.split('|');
|
|
|
|
// Get files changed for this commit
|
|
const statOutput = await execGitSafe(
|
|
['diff-tree', '--no-commit-id', '--name-only', '-r', hash],
|
|
repoPath
|
|
);
|
|
const files = statOutput ? statOutput.split('\n').filter(f => f.trim()) : [];
|
|
|
|
commits.push({
|
|
hash,
|
|
shortHash,
|
|
message,
|
|
author,
|
|
authorEmail,
|
|
date,
|
|
files
|
|
});
|
|
}
|
|
|
|
return commits;
|
|
}
|