refactor: extract git to separate package

This commit is contained in:
Kamran Ahmed
2026-03-14 04:36:12 +00:00
parent da80473616
commit 780adf0850
17 changed files with 195 additions and 166 deletions
+14
View File
@@ -10,10 +10,15 @@
"license": "MIT",
"workspaces": [
"packages/cli",
"packages/git",
"packages/parser",
"packages/ui"
]
},
"node_modules/@diffity/git": {
"resolved": "packages/git",
"link": true
},
"node_modules/@diffity/parser": {
"resolved": "packages/parser",
"link": true
@@ -3661,6 +3666,7 @@
"name": "diffity",
"version": "0.1.0",
"dependencies": {
"@diffity/git": "0.1.0",
"@diffity/parser": "0.1.0",
"commander": "latest",
"open": "latest",
@@ -3675,6 +3681,14 @@
"typescript": "^5.9.3"
}
},
"packages/git": {
"name": "@diffity/git",
"version": "0.1.0",
"devDependencies": {
"@types/node": "^25.5.0",
"typescript": "^5.9.3"
}
},
"packages/parser": {
"name": "@diffity/parser",
"version": "0.1.0",
+2 -1
View File
@@ -5,11 +5,12 @@
"description": "GitHub-style git diff viewer in the browser",
"workspaces": [
"packages/cli",
"packages/git",
"packages/parser",
"packages/ui"
],
"scripts": {
"build": "npm run build -w @diffity/parser && npm run build -w @diffity/ui && npm run build -w diffity",
"build": "npm run build -w @diffity/parser && npm run build -w @diffity/git && npm run build -w @diffity/ui && npm run build -w diffity",
"test": "npm run test -w @diffity/parser && npm run test -w @diffity/ui",
"dev": "npm run dev -w @diffity/ui",
"start": "npm run dev -w diffity"
+1
View File
@@ -12,6 +12,7 @@
"dev": "tsx src/index.ts"
},
"dependencies": {
"@diffity/git": "0.1.0",
"@diffity/parser": "0.1.0",
"commander": "latest",
"open": "latest",
-156
View File
@@ -1,156 +0,0 @@
import { execSync } from 'node:child_process';
import { existsSync } from 'node:fs';
export function isGitRepo(): boolean {
try {
execSync('git rev-parse --is-inside-work-tree', {
stdio: 'pipe',
});
return true;
} catch {
return false;
}
}
export function getRepoRoot(): string {
return execSync('git rev-parse --show-toplevel', {
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'pipe'],
}).trim();
}
export function getRepoName(): string {
const root = getRepoRoot();
return root.split('/').pop() || root;
}
export function getCurrentBranch(): string {
try {
return execSync('git rev-parse --abbrev-ref HEAD', {
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'pipe'],
}).trim();
} catch {
return 'HEAD';
}
}
export function getGitDiff(args: string[] = []): string {
const cmd = ['git', 'diff', ...args].join(' ');
return execSync(cmd, {
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'pipe'],
maxBuffer: 50 * 1024 * 1024,
});
}
export function getUntrackedFiles(): string[] {
const output = execSync('git ls-files --others --exclude-standard', {
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'pipe'],
}).trim();
if (!output) {
return [];
}
return output.split('\n');
}
export function getUntrackedDiff(files: string[]): string {
const diffs: string[] = [];
for (const file of files) {
try {
execSync(`git diff --no-index -- /dev/null "${file}"`, {
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'pipe'],
maxBuffer: 50 * 1024 * 1024,
});
} catch (err: unknown) {
const error = err as { stdout?: string; status?: number };
if (error.status === 1 && error.stdout) {
diffs.push(error.stdout);
}
}
}
return diffs.join('\n');
}
export function getFileContent(path: string, ref?: string): string {
if (ref) {
return execSync(`git show ${ref}:${path}`, {
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'pipe'],
});
}
return execSync(`git show HEAD:${path}`, {
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'pipe'],
});
}
export function getStagedFiles(): string[] {
const output = execSync('git diff --staged --name-only', {
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'pipe'],
}).trim();
if (!output) {
return [];
}
return output.split('\n');
}
export function getUnstagedFiles(): string[] {
const output = execSync('git diff --name-only', {
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'pipe'],
}).trim();
if (!output) {
return [];
}
return output.split('\n');
}
export interface Commit {
hash: string;
shortHash: string;
message: string;
relativeDate: string;
}
export function getRecentCommits(count: number, skip = 0): Commit[] {
const output = execSync(`git log -n ${count} --skip=${skip} --format="%H|%h|%s|%cr"`, {
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'pipe'],
}).trim();
if (!output) {
return [];
}
return output.split('\n').map((line) => {
const [hash, shortHash, message, relativeDate] = line.split('|');
return { hash, shortHash, message, relativeDate };
});
}
export interface RepoInfo {
name: string;
branch: string;
root: string;
}
export function getRepoInfo(): RepoInfo {
return {
name: getRepoName(),
branch: getCurrentBranch(),
root: getRepoRoot(),
};
}
+1 -1
View File
@@ -3,7 +3,7 @@
import { Command } from 'commander';
import open from 'open';
import pc from 'picocolors';
import { isGitRepo } from './git.js';
import { isGitRepo } from '@diffity/git';
import { startServer } from './server.js';
const program = new Command();
+8 -8
View File
@@ -6,7 +6,7 @@ import { dirname } from 'node:path';
import { parseDiff } from '@diffity/parser';
import type { ParsedDiff } from '@diffity/parser';
import {
getGitDiff,
getDiff,
getUntrackedFiles,
getUntrackedDiff,
getRepoInfo,
@@ -14,7 +14,7 @@ import {
getStagedFiles,
getUnstagedFiles,
getRecentCommits,
} from './git.js';
} from '@diffity/git';
const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -65,13 +65,13 @@ interface ServerResult {
function resolveRef(ref: string, extraArgs: string[] = []): string {
switch (ref) {
case 'staged': {
return getGitDiff(['--staged', ...extraArgs]);
return getDiff(['--staged', ...extraArgs]);
}
case 'unstaged': {
return getGitDiff(extraArgs);
return getDiff(extraArgs);
}
case 'working': {
return getGitDiff(['HEAD', ...extraArgs]);
return getDiff(['HEAD', ...extraArgs]);
}
case 'untracked': {
const files = getUntrackedFiles();
@@ -81,7 +81,7 @@ function resolveRef(ref: string, extraArgs: string[] = []): string {
return getUntrackedDiff(files);
}
case 'all': {
let raw = getGitDiff(['HEAD', ...extraArgs]);
let raw = getDiff(['HEAD', ...extraArgs]);
const untrackedFiles = getUntrackedFiles();
if (untrackedFiles.length > 0) {
raw += '\n' + getUntrackedDiff(untrackedFiles);
@@ -89,7 +89,7 @@ function resolveRef(ref: string, extraArgs: string[] = []): string {
return raw;
}
default: {
return getGitDiff([ref, ...extraArgs]);
return getDiff([ref, ...extraArgs]);
}
}
}
@@ -99,7 +99,7 @@ export function startServer(options: ServerOptions): Promise<ServerResult> {
const includeUntracked = diffArgs.length === 0;
function getFullDiff(args: string[]): string {
let raw = getGitDiff(args);
let raw = getDiff(args);
if (includeUntracked) {
const untrackedFiles = getUntrackedFiles();
if (untrackedFiles.length > 0) {
+1
View File
@@ -7,6 +7,7 @@
},
"include": ["src/**/*.ts"],
"references": [
{ "path": "../git" },
{ "path": "../parser" }
]
}
+23
View File
@@ -0,0 +1,23 @@
{
"name": "@diffity/git",
"version": "0.1.0",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
}
},
"scripts": {
"build": "tsc"
},
"files": [
"dist"
],
"devDependencies": {
"@types/node": "^25.5.0",
"typescript": "^5.9.3"
}
}
+15
View File
@@ -0,0 +1,15 @@
import { exec } from './exec.js';
import type { Commit } from './types.js';
export function getRecentCommits(count: number, skip = 0): Commit[] {
const output = exec(`git log -n ${count} --skip=${skip} --format="%H|%h|%s|%cr"`);
if (!output) {
return [];
}
return output.split('\n').map((line) => {
const [hash, shortHash, message, relativeDate] = line.split('|');
return { hash, shortHash, message, relativeDate };
});
}
+31
View File
@@ -0,0 +1,31 @@
import { exec, execLarge, execLines } from './exec.js';
export function getDiff(args: string[] = []): string {
const cmd = ['git', 'diff', ...args].join(' ');
return execLarge(cmd);
}
export function getUntrackedFiles(): string[] {
return execLines('git ls-files --others --exclude-standard');
}
export function getUntrackedDiff(files: string[]): string {
const diffs: string[] = [];
for (const file of files) {
try {
execLarge(`git diff --no-index -- /dev/null "${file}"`);
} catch (err: unknown) {
const error = err as { stdout?: string; status?: number };
if (error.status === 1 && error.stdout) {
diffs.push(error.stdout);
}
}
}
return diffs.join('\n');
}
export function getFileContent(path: string, ref = 'HEAD'): string {
return exec(`git show ${ref}:${path}`);
}
+26
View File
@@ -0,0 +1,26 @@
import { execSync, type StdioOptions } from 'node:child_process';
const STDIO: StdioOptions = ['pipe', 'pipe', 'pipe'];
export function exec(cmd: string): string {
return execSync(cmd, {
encoding: 'utf-8',
stdio: STDIO,
}).trim();
}
export function execLarge(cmd: string): string {
return execSync(cmd, {
encoding: 'utf-8',
stdio: STDIO,
maxBuffer: 50 * 1024 * 1024,
});
}
export function execLines(cmd: string): string[] {
const output = exec(cmd);
if (!output) {
return [];
}
return output.split('\n');
}
+5
View File
@@ -0,0 +1,5 @@
export type { Commit, RepoInfo } from './types.js';
export { isGitRepo, getRepoRoot, getRepoName, getCurrentBranch, getRepoInfo } from './repo.js';
export { getDiff, getUntrackedFiles, getUntrackedDiff, getFileContent } from './diff.js';
export { getStagedFiles, getUnstagedFiles } from './status.js';
export { getRecentCommits } from './commits.js';
+37
View File
@@ -0,0 +1,37 @@
import { execSync } from 'node:child_process';
import { exec } from './exec.js';
import type { RepoInfo } from './types.js';
export function isGitRepo(): boolean {
try {
execSync('git rev-parse --is-inside-work-tree', { stdio: 'pipe' });
return true;
} catch {
return false;
}
}
export function getRepoRoot(): string {
return exec('git rev-parse --show-toplevel');
}
export function getRepoName(): string {
const root = getRepoRoot();
return root.split('/').pop() || root;
}
export function getCurrentBranch(): string {
try {
return exec('git rev-parse --abbrev-ref HEAD');
} catch {
return 'HEAD';
}
}
export function getRepoInfo(): RepoInfo {
return {
name: getRepoName(),
branch: getCurrentBranch(),
root: getRepoRoot(),
};
}
+9
View File
@@ -0,0 +1,9 @@
import { execLines } from './exec.js';
export function getStagedFiles(): string[] {
return execLines('git diff --staged --name-only');
}
export function getUnstagedFiles(): string[] {
return execLines('git diff --name-only');
}
+12
View File
@@ -0,0 +1,12 @@
export interface Commit {
hash: string;
shortHash: string;
message: string;
relativeDate: string;
}
export interface RepoInfo {
name: string;
branch: string;
root: string;
}
+9
View File
@@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"composite": true
},
"include": ["src/**/*.ts"]
}
+1
View File
@@ -15,6 +15,7 @@
},
"references": [
{ "path": "./packages/parser" },
{ "path": "./packages/git" },
{ "path": "./packages/cli" },
{ "path": "./packages/ui" }
]