blob: bfe306e80acf0e44a768e71664e4bfb6b6f950b9 [file] [log] [blame]
Philip Zeyliger272a90e2025-05-16 14:49:51 -07001// git-data-service.ts
2// Interface and implementation for fetching Git data
3
Autoformatter8c463622025-05-16 21:54:17 +00004import { DiffFile, GitLogEntry } from "../types";
Philip Zeyliger272a90e2025-05-16 14:49:51 -07005
6// Re-export DiffFile as GitDiffFile
7export type GitDiffFile = DiffFile;
8
9/**
10 * Interface for Git data services
11 */
12export interface GitDataService {
13 /**
14 * Fetches recent commit history
15 * @param initialCommit The initial commit hash to start from
16 * @returns List of commits
17 */
18 getCommitHistory(initialCommit?: string): Promise<GitLogEntry[]>;
19
20 /**
21 * Fetches diff between two commits
22 * @param from Starting commit hash
23 * @param to Ending commit hash (can be empty string for unstaged changes)
24 * @returns List of changed files
25 */
26 getDiff(from: string, to: string): Promise<GitDiffFile[]>;
27
28 /**
29 * Fetches diff for a single commit
30 * @param commit Commit hash
31 * @returns List of changed files
32 */
33 getCommitDiff(commit: string): Promise<GitDiffFile[]>;
34
35 /**
36 * Fetches file content from git using a file hash
37 * @param fileHash Git blob hash of the file to fetch
38 * @returns File content as string
39 */
40 getFileContent(fileHash: string): Promise<string>;
41
42 /**
43 * Gets file content from the current working directory
44 * @param filePath Path to the file within the repository
45 * @returns File content as string
46 */
47 getWorkingCopyContent(filePath: string): Promise<string>;
Autoformatter8c463622025-05-16 21:54:17 +000048
Philip Zeyliger272a90e2025-05-16 14:49:51 -070049 /**
50 * Saves file content to the working directory
51 * @param filePath Path to the file within the repository
52 * @param content New content to save to the file
53 */
54 saveFileContent(filePath: string, content: string): Promise<void>;
55
56 /**
57 * Gets the base commit reference (often "sketch-base")
58 * @returns Base commit reference
59 */
60 getBaseCommitRef(): Promise<string>;
61
62 /**
63 * Fetches unstaged changes (diff between a commit and working directory)
64 * @param from Starting commit hash (defaults to HEAD if not specified)
65 * @returns List of changed files
66 */
67 getUnstagedChanges(from?: string): Promise<GitDiffFile[]>;
Josh Bleecher Snydera8561f72025-07-15 23:47:59 +000068
69 /**
70 * Fetches list of untracked files in the repository
71 * @returns List of untracked file paths
72 */
73 getUntrackedFiles(): Promise<string[]>;
Philip Zeyliger272a90e2025-05-16 14:49:51 -070074}
75
76/**
77 * Default implementation of GitDataService for the real application
78 */
79export class DefaultGitDataService implements GitDataService {
80 private baseCommitRef: string | null = null;
81
82 async getCommitHistory(initialCommit?: string): Promise<GitLogEntry[]> {
83 try {
Autoformatter8c463622025-05-16 21:54:17 +000084 const url = initialCommit
85 ? `git/recentlog?initialCommit=${encodeURIComponent(initialCommit)}`
86 : "git/recentlog";
Philip Zeyliger272a90e2025-05-16 14:49:51 -070087 const response = await fetch(url);
Autoformatter8c463622025-05-16 21:54:17 +000088
Philip Zeyliger272a90e2025-05-16 14:49:51 -070089 if (!response.ok) {
Autoformatter8c463622025-05-16 21:54:17 +000090 throw new Error(
91 `Failed to fetch commit history: ${response.statusText}`,
92 );
Philip Zeyliger272a90e2025-05-16 14:49:51 -070093 }
Autoformatter8c463622025-05-16 21:54:17 +000094
Philip Zeyliger272a90e2025-05-16 14:49:51 -070095 return await response.json();
96 } catch (error) {
Autoformatter8c463622025-05-16 21:54:17 +000097 console.error("Error fetching commit history:", error);
Philip Zeyliger272a90e2025-05-16 14:49:51 -070098 throw error;
99 }
100 }
101
102 async getDiff(from: string, to: string): Promise<GitDiffFile[]> {
103 try {
104 const url = `git/rawdiff?from=${encodeURIComponent(from)}&to=${encodeURIComponent(to)}`;
105 const response = await fetch(url);
Autoformatter8c463622025-05-16 21:54:17 +0000106
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700107 if (!response.ok) {
108 throw new Error(`Failed to fetch diff: ${response.statusText}`);
109 }
Autoformatter8c463622025-05-16 21:54:17 +0000110
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700111 return await response.json();
112 } catch (error) {
Autoformatter8c463622025-05-16 21:54:17 +0000113 console.error("Error fetching diff:", error);
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700114 throw error;
115 }
116 }
117
118 async getCommitDiff(commit: string): Promise<GitDiffFile[]> {
119 try {
120 const url = `git/rawdiff?commit=${encodeURIComponent(commit)}`;
121 const response = await fetch(url);
Autoformatter8c463622025-05-16 21:54:17 +0000122
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700123 if (!response.ok) {
124 throw new Error(`Failed to fetch commit diff: ${response.statusText}`);
125 }
Autoformatter8c463622025-05-16 21:54:17 +0000126
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700127 return await response.json();
128 } catch (error) {
Autoformatter8c463622025-05-16 21:54:17 +0000129 console.error("Error fetching commit diff:", error);
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700130 throw error;
131 }
132 }
133
134 async getFileContent(fileHash: string): Promise<string> {
135 try {
136 // If the hash is marked as a working copy (special value '000000' or empty)
Autoformatter8c463622025-05-16 21:54:17 +0000137 if (
138 fileHash === "0000000000000000000000000000000000000000" ||
139 !fileHash
140 ) {
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700141 // This shouldn't happen, but if it does, return empty string
142 // Working copy content should be fetched through getWorkingCopyContent
Autoformatter8c463622025-05-16 21:54:17 +0000143 console.warn(
144 "Invalid file hash for getFileContent, returning empty string",
145 );
146 return "";
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700147 }
Autoformatter8c463622025-05-16 21:54:17 +0000148
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700149 const url = `git/show?hash=${encodeURIComponent(fileHash)}`;
150 const response = await fetch(url);
Autoformatter8c463622025-05-16 21:54:17 +0000151
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700152 if (!response.ok) {
153 throw new Error(`Failed to fetch file content: ${response.statusText}`);
154 }
Autoformatter8c463622025-05-16 21:54:17 +0000155
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700156 const data = await response.json();
Autoformatter8c463622025-05-16 21:54:17 +0000157 return data.output || "";
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700158 } catch (error) {
Autoformatter8c463622025-05-16 21:54:17 +0000159 console.error("Error fetching file content:", error);
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700160 throw error;
161 }
162 }
Autoformatter8c463622025-05-16 21:54:17 +0000163
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700164 async getWorkingCopyContent(filePath: string): Promise<string> {
165 try {
166 const url = `git/cat?path=${encodeURIComponent(filePath)}`;
167 const response = await fetch(url);
Autoformatter8c463622025-05-16 21:54:17 +0000168
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700169 if (!response.ok) {
Josh Bleecher Snyder77bac8c2025-05-28 11:04:09 -0700170 const errorText = await response.text();
Autoformatter8c463622025-05-16 21:54:17 +0000171 throw new Error(
Josh Bleecher Snyder77bac8c2025-05-28 11:04:09 -0700172 `Failed to fetch working copy content (${response.status}): ${errorText}`,
Autoformatter8c463622025-05-16 21:54:17 +0000173 );
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700174 }
Josh Bleecher Snyderfadffe32025-07-10 00:08:38 +0000175 if (response.status === 204) {
176 return ""; // file doesn't exist
177 }
Autoformatter8c463622025-05-16 21:54:17 +0000178
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700179 const data = await response.json();
Autoformatter8c463622025-05-16 21:54:17 +0000180 return data.output || "";
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700181 } catch (error) {
Autoformatter8c463622025-05-16 21:54:17 +0000182 console.error("Error fetching working copy content:", error);
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700183 throw error;
184 }
185 }
Autoformatter8c463622025-05-16 21:54:17 +0000186
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700187 async saveFileContent(filePath: string, content: string): Promise<void> {
188 try {
189 const url = `git/save`;
190 const response = await fetch(url, {
Autoformatter8c463622025-05-16 21:54:17 +0000191 method: "POST",
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700192 headers: {
Autoformatter8c463622025-05-16 21:54:17 +0000193 "Content-Type": "application/json",
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700194 },
195 body: JSON.stringify({
196 path: filePath,
Autoformatter8c463622025-05-16 21:54:17 +0000197 content: content,
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700198 }),
199 });
Autoformatter8c463622025-05-16 21:54:17 +0000200
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700201 if (!response.ok) {
202 const errorText = await response.text();
Autoformatter8c463622025-05-16 21:54:17 +0000203 throw new Error(
204 `Failed to save file content: ${response.statusText} - ${errorText}`,
205 );
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700206 }
Autoformatter8c463622025-05-16 21:54:17 +0000207
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700208 // Don't need to return the response, just ensure it was successful
209 } catch (error) {
Autoformatter8c463622025-05-16 21:54:17 +0000210 console.error("Error saving file content:", error);
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700211 throw error;
212 }
213 }
Autoformatter8c463622025-05-16 21:54:17 +0000214
215 async getUnstagedChanges(from: string = "HEAD"): Promise<GitDiffFile[]> {
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700216 try {
217 // To get unstaged changes, we diff the specified commit (or HEAD) with an empty 'to'
Autoformatter8c463622025-05-16 21:54:17 +0000218 return await this.getDiff(from, "");
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700219 } catch (error) {
Autoformatter8c463622025-05-16 21:54:17 +0000220 console.error("Error fetching unstaged changes:", error);
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700221 throw error;
222 }
223 }
224
225 async getBaseCommitRef(): Promise<string> {
226 // Cache the base commit reference to avoid multiple requests
227 if (this.baseCommitRef) {
228 return this.baseCommitRef;
229 }
230
231 try {
232 // This could be replaced with a specific endpoint call if available
233 // For now, we'll use a fixed value or try to get it from the server
Autoformatter8c463622025-05-16 21:54:17 +0000234 this.baseCommitRef = "sketch-base";
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700235 return this.baseCommitRef;
236 } catch (error) {
Autoformatter8c463622025-05-16 21:54:17 +0000237 console.error("Error fetching base commit reference:", error);
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700238 throw error;
239 }
240 }
Josh Bleecher Snydera8561f72025-07-15 23:47:59 +0000241
242 async getUntrackedFiles(): Promise<string[]> {
243 try {
244 const response = await fetch("git/untracked");
245
246 if (!response.ok) {
247 throw new Error(
248 `Failed to fetch untracked files: ${response.statusText}`,
249 );
250 }
251
252 const data = await response.json();
253 return data.untracked_files || [];
254 } catch (error) {
255 console.error("Error fetching untracked files:", error);
256 throw error;
257 }
258 }
Autoformatter8c463622025-05-16 21:54:17 +0000259}