Skip to content

Commit 224ebee

Browse files
authored
feat: add error and event modules (#22)
* feat: add error and event modules * chore: add changeset * fix: delete unused import * chore: reorder labeler patterns to attempt .changeset exclusion * refactor
1 parent 4fb5871 commit 224ebee

File tree

15 files changed

+640
-299
lines changed

15 files changed

+640
-299
lines changed

.changeset/modern-schools-shout.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@offlegacy/git-intent-core": patch
3+
"git-intent": patch
4+
---
5+
6+
feat: add error and event modules
7+

.github/labeler.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ core:
88

99
documentation:
1010
- changed-files:
11-
- any-glob-to-any-file: ['**/*.md', '**/*.mdx', '!.changeset/**/*.md']
11+
- any-glob-to-any-file: ['!changeset/**/*.md', '**/*.md', '**/*.mdx']
1212

1313
github-actions:
1414
- changed-files:

packages/core/src/errors/index.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
export class GitIntentError extends Error {
2+
constructor(
3+
message: string,
4+
public readonly code: string
5+
) {
6+
super(message);
7+
this.name = 'GitIntentError';
8+
}
9+
}
10+
11+
export class GitError extends Error {
12+
constructor(
13+
message: string,
14+
public readonly code: string
15+
) {
16+
super(message);
17+
this.name = 'GitError';
18+
}
19+
}

packages/core/src/events/index.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
export type EventMap = Record<string, unknown>;
2+
export type EventHandler<T = unknown> = (data: T) => void;
3+
4+
export class TypedEventEmitter<T extends EventMap> {
5+
private listeners: Partial<Record<keyof T, EventHandler[]>> = {};
6+
7+
on<K extends keyof T>(event: K, handler: EventHandler<T[K]>): void {
8+
if (!this.listeners[event]) {
9+
this.listeners[event] = [];
10+
}
11+
12+
this.listeners[event].push(handler as EventHandler);
13+
}
14+
15+
off<K extends keyof T>(event: K, handler?: EventHandler<T[K]>): void {
16+
if (!handler) {
17+
delete this.listeners[event];
18+
return;
19+
}
20+
21+
const handlers = this.listeners[event];
22+
if (handlers) {
23+
this.listeners[event] = handlers.filter((h) => h !== handler) as EventHandler[];
24+
}
25+
}
26+
27+
emit<K extends keyof T>(event: K, data: T[K]): void {
28+
const handlers = this.listeners[event];
29+
if (handlers) {
30+
for (const handler of handlers) {
31+
handler(data);
32+
}
33+
}
34+
}
35+
36+
once<K extends keyof T>(event: K, handler: EventHandler<T[K]>): void {
37+
const onceHandler: EventHandler = (data: unknown) => {
38+
this.off(event, onceHandler as EventHandler<T[K]>);
39+
handler(data as T[K]);
40+
};
41+
42+
this.on(event, onceHandler as EventHandler<T[K]>);
43+
}
44+
}
45+
46+
export function createEmitter<T extends EventMap>(): TypedEventEmitter<T> {
47+
return new TypedEventEmitter<T>();
48+
}

packages/core/src/git/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export * from './types.js';
2+
export * from './service.js';
3+
export * from './refs.js';
4+
5+
import { gitService } from './service.js';
6+
export default gitService;

packages/core/src/utils/git-refs.ts renamed to packages/core/src/git/refs.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { git } from './git.js';
1+
import { gitService } from './service.js';
22

33
class GitRefsService {
44
private static instance: GitRefsService;
@@ -16,7 +16,7 @@ class GitRefsService {
1616
if (!treeContent || treeContent.trim() === '') {
1717
throw new Error('Invalid tree content: tree content cannot be empty');
1818
}
19-
return git.execGit(['mktree'], { input: treeContent, cwd });
19+
return gitService.execGit(['mktree'], { input: treeContent, cwd });
2020
}
2121

2222
async createCommitTree(treeHash: string, message: string, cwd?: string): Promise<string> {
@@ -25,7 +25,7 @@ class GitRefsService {
2525
}
2626

2727
try {
28-
const result = git.execGit(['commit-tree', treeHash, '-m', message], { cwd });
28+
const result = gitService.execGit(['commit-tree', treeHash, '-m', message], { cwd });
2929

3030
if (!result || result.trim() === '') {
3131
throw new Error(`Failed to create commit tree from hash: ${treeHash}`);
@@ -43,16 +43,16 @@ class GitRefsService {
4343
throw new Error(`Invalid commit hash: commit hash cannot be empty for ref ${refName}`);
4444
}
4545

46-
await git.raw(['update-ref', refName, commitHash], cwd);
46+
await gitService.raw(['update-ref', refName, commitHash], cwd);
4747
}
4848

4949
async deleteRef(refName: string, cwd?: string): Promise<void> {
50-
await git.raw(['update-ref', '-d', refName], cwd);
50+
await gitService.raw(['update-ref', '-d', refName], cwd);
5151
}
5252

5353
async checkRefExists(refName: string, cwd?: string): Promise<boolean> {
5454
try {
55-
await git.raw(['show-ref', '--verify', refName], cwd);
55+
await gitService.raw(['show-ref', '--verify', refName], cwd);
5656
return true;
5757
} catch {
5858
return false;
@@ -61,7 +61,7 @@ class GitRefsService {
6161

6262
async showRef(refPath: string, cwd?: string): Promise<string> {
6363
try {
64-
return await git.raw(['show', refPath], cwd);
64+
return await gitService.raw(['show', refPath], cwd);
6565
} catch (error) {
6666
throw new Error(`Failed to show ref: ${refPath}`);
6767
}

packages/core/src/git/service.ts

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import { spawnSync } from 'node:child_process';
2+
import { type SimpleGit, simpleGit } from 'simple-git';
3+
import { GitError } from '../errors/index.js';
4+
import type { GitServiceInterface } from './types.js';
5+
6+
class GitService implements GitServiceInterface {
7+
private static instance: GitService;
8+
private gitInstances: Map<string, SimpleGit> = new Map();
9+
private defaultGit: SimpleGit;
10+
11+
private constructor() {
12+
this.defaultGit = simpleGit();
13+
this.gitInstances.set('default', this.defaultGit);
14+
}
15+
16+
static getInstance(): GitService {
17+
if (!GitService.instance) {
18+
GitService.instance = new GitService();
19+
}
20+
return GitService.instance;
21+
}
22+
23+
getGit(cwd?: string): SimpleGit {
24+
if (!cwd) {
25+
return this.defaultGit;
26+
}
27+
28+
const cacheKey = cwd;
29+
if (!this.gitInstances.has(cacheKey)) {
30+
this.gitInstances.set(cacheKey, simpleGit(cwd));
31+
}
32+
33+
return this.gitInstances.get(cacheKey) as SimpleGit;
34+
}
35+
36+
execGit(args: string[], options: { input?: string; cwd?: string } = {}): string {
37+
const { input, cwd } = options;
38+
39+
try {
40+
const result = spawnSync('git', args, {
41+
input: input ? Buffer.from(input) : undefined,
42+
cwd,
43+
encoding: 'utf-8',
44+
stdio: ['pipe', 'pipe', 'pipe'],
45+
});
46+
47+
if (result.status !== 0) {
48+
throw new GitError(`Git command failed: git ${args.join(' ')}\n${result.stderr}`, 'GIT_COMMAND_FAILED');
49+
}
50+
51+
return result.stdout ? result.stdout.trim() : '';
52+
} catch (error) {
53+
if (error instanceof GitError) {
54+
throw error;
55+
}
56+
throw new GitError(`Git execution error: ${(error as Error).message}`, 'GIT_EXEC_ERROR');
57+
}
58+
}
59+
60+
async checkIsRepo(cwd?: string): Promise<boolean> {
61+
try {
62+
const git = this.getGit(cwd);
63+
const isRepo = await git.checkIsRepo();
64+
return isRepo;
65+
} catch (error) {
66+
throw new GitError(
67+
`Failed to check if directory is a Git repository: ${(error as Error).message}`,
68+
'CHECK_REPO_ERROR'
69+
);
70+
}
71+
}
72+
73+
async getRepoRoot(cwd?: string): Promise<string> {
74+
try {
75+
const git = this.getGit(cwd);
76+
const root = await git.revparse(['--show-toplevel']);
77+
return root.trim();
78+
} catch (error) {
79+
throw new GitError(`Failed to get repository root: ${(error as Error).message}`, 'REPO_ROOT_ERROR');
80+
}
81+
}
82+
83+
async getCurrentBranch(cwd?: string): Promise<string> {
84+
try {
85+
const git = this.getGit(cwd);
86+
const branch = await git.revparse(['--abbrev-ref', 'HEAD']);
87+
return branch.trim();
88+
} catch (error) {
89+
throw new GitError(`Failed to get current branch: ${(error as Error).message}`, 'CURRENT_BRANCH_ERROR');
90+
}
91+
}
92+
93+
async getStatus(cwd?: string): Promise<any> {
94+
try {
95+
const git = this.getGit(cwd);
96+
return await git.status();
97+
} catch (error) {
98+
throw new GitError(`Failed to get repository status: ${(error as Error).message}`, 'STATUS_ERROR');
99+
}
100+
}
101+
102+
async hasStagedFiles(cwd?: string): Promise<boolean> {
103+
try {
104+
const status = await this.getStatus(cwd);
105+
return status.staged.length > 0;
106+
} catch (error) {
107+
throw new GitError(`Failed to check for staged files: ${(error as Error).message}`, 'STAGED_FILES_ERROR');
108+
}
109+
}
110+
111+
async createCommit(message: string, cwd?: string): Promise<string> {
112+
try {
113+
const git = this.getGit(cwd);
114+
const result = await git.commit(message);
115+
return result.commit;
116+
} catch (error) {
117+
throw new GitError(`Failed to create commit: ${(error as Error).message}`, 'CREATE_COMMIT_ERROR');
118+
}
119+
}
120+
121+
async hashObject(content: string, cwd?: string): Promise<string> {
122+
try {
123+
return this.execGit(['hash-object', '-w', '--stdin'], { input: content, cwd });
124+
} catch (error) {
125+
throw new GitError(`Failed to hash object: ${(error as Error).message}`, 'HASH_OBJECT_ERROR');
126+
}
127+
}
128+
129+
async raw(commands: string[], cwd?: string): Promise<string> {
130+
try {
131+
const git = this.getGit(cwd);
132+
return await git.raw(commands);
133+
} catch (error) {
134+
throw new GitError(`Failed to execute raw git command: ${(error as Error).message}`, 'RAW_COMMAND_ERROR');
135+
}
136+
}
137+
}
138+
139+
export function createGitService(): GitServiceInterface {
140+
return GitService.getInstance();
141+
}
142+
143+
export const gitService = GitService.getInstance();

packages/core/src/git/types.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import type { SimpleGit } from 'simple-git';
2+
3+
export interface GitServiceInterface {
4+
getGit(cwd?: string): SimpleGit;
5+
execGit(args: string[], options?: { input?: string; cwd?: string }): string;
6+
checkIsRepo(cwd?: string): Promise<boolean>;
7+
getRepoRoot(cwd?: string): Promise<string>;
8+
getCurrentBranch(cwd?: string): Promise<string>;
9+
getStatus(cwd?: string): Promise<any>;
10+
hasStagedFiles(cwd?: string): Promise<boolean>;
11+
createCommit(message: string, cwd?: string): Promise<string>;
12+
hashObject(content: string, cwd?: string): Promise<string>;
13+
raw(commands: string[], cwd?: string): Promise<string>;
14+
}

packages/core/src/index.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
export * from './types/intent.js';
2+
export * from './errors/index.js';
3+
export * from './events/index.js';
4+
export * from './git/index.js';
5+
export * from './storage/index.js';
26

37
export { generateId } from './utils/generateId.js';
48
export { getPackageInfo } from './utils/get-package-info.js';
5-
export { git } from './utils/git.js';
6-
export { gitRefs } from './utils/git-refs.js';
7-
export { storage } from './utils/storage.js';

0 commit comments

Comments
 (0)