History

feat(mcp): add project management tools

Julius Marminge committed 2 months ago

Commit 04bd6a81 parent 415ed0f29 files · +2143 −121Browse files

Showing the changes to apps/server/src/sourceControl/SourceControlRepositoryService.ts (1 of 29 files). Show all

Unified · Split

apps/server/src/sourceControl/SourceControlRepositoryService.ts+155−43
@@ -17,9 +17,9 @@ import { · 16 unmodified lines
17 type SourceControlRepositoryInfo,17 type SourceControlRepositoryInfo,
18 type SourceControlRepositoryLookupInput,18 type SourceControlRepositoryLookupInput,
19} from "@t3tools/contracts";19} from "@t3tools/contracts";
20
21import { ServerConfig } from "../config.ts";20import { ServerConfig } from "../config.ts";
22import { expandHomePathWith } from "../pathExpansion.ts";21import { expandHomePathWith } from "../pathExpansion.ts";
22import { makeKeyedSerialExecutor } from "../orchestration-v2/KeyedSerialExecutor.ts";
23import * as GitVcsDriver from "../vcs/GitVcsDriver.ts";23import * as GitVcsDriver from "../vcs/GitVcsDriver.ts";
24import * as SourceControlProviderRegistry from "./SourceControlProviderRegistry.ts";24import * as SourceControlProviderRegistry from "./SourceControlProviderRegistry.ts";
25const isSourceControlRepositoryError = Schema.is(SourceControlRepositoryError);25const isSourceControlRepositoryError = Schema.is(SourceControlRepositoryError);
@@ -83,6 +83,7 @@ export const make = Effect.gen(function* () { · 57 unmodified lines
83 const git = yield* GitVcsDriver.GitVcsDriver;83 const git = yield* GitVcsDriver.GitVcsDriver;
84 const path = yield* Path.Path;84 const path = yield* Path.Path;
85 const providers = yield* SourceControlProviderRegistry.SourceControlProviderRegistry;85 const providers = yield* SourceControlProviderRegistry.SourceControlProviderRegistry;
86 const cloneOperations = yield* makeKeyedSerialExecutor<string>();
86 87
87 const ensureConcreteProvider = (input: {88 const ensureConcreteProvider = (input: {
88 readonly operation: string;89 readonly operation: string;
@@ -131,9 +132,26 @@ export const make = Effect.gen(function* () { · 42 unmodified lines
131 },132 },
132 );133 );
133 134
135 const canonicalDestinationKey = Effect.fn(
136 "SourceControlRepositoryService.canonicalDestinationKey",
137 )(function* (destinationPath: string) {
138 if (yield* fileSystem.exists(destinationPath)) {
139 return yield* fileSystem.realPath(destinationPath);
140 }
141
142 const suffix = [path.basename(destinationPath)];
143 let ancestor = path.dirname(destinationPath);
144 while (!(yield* fileSystem.exists(ancestor))) {
145 const parent = path.dirname(ancestor);
146 if (parent === ancestor) break;
147 suffix.unshift(path.basename(ancestor));
148 ancestor = parent;
149 }
150 return path.join(yield* fileSystem.realPath(ancestor), ...suffix);
151 });
152
134 const prepareDestination = Effect.fn("SourceControlRepositoryService.prepareDestination")(153 const prepareDestination = Effect.fn("SourceControlRepositoryService.prepareDestination")(
135 function* (destinationPath: string) {154 function* (normalizedDestination: string) {
136 const normalizedDestination = yield* normalizeDestinationPath(destinationPath);
137 if (yield* fileSystem.exists(normalizedDestination)) {155 if (yield* fileSystem.exists(normalizedDestination)) {
138 const entries = yield* fileSystem156 const entries = yield* fileSystem
139 .readDirectory(normalizedDestination, { recursive: false })157 .readDirectory(normalizedDestination, { recursive: false })
@@ -148,13 +166,12 @@ export const make = Effect.gen(function* () { · 8 unmodified lines
148 }),166 }),
149 ),167 ),
150 );168 );
151 if (entries.length > 0) {169 return {
152 return yield* new SourceControlRepositoryError({170 destinationPath: normalizedDestination,
153 operation: "cloneRepository",171 parentPath: path.dirname(normalizedDestination),
154 provider: "unknown",172 directoryName: path.basename(normalizedDestination),
155 detail: "Destination path already exists and is not empty.",173 occupied: entries.length > 0,
156 });174 } as const;
157 }
158 } else {175 } else {
159 yield* fileSystem.makeDirectory(path.dirname(normalizedDestination), { recursive: true });176 yield* fileSystem.makeDirectory(path.dirname(normalizedDestination), { recursive: true });
160 }177 }
@@ -163,49 +180,144 @@ export const make = Effect.gen(function* () { · 2 unmodified lines
163 destinationPath: normalizedDestination,180 destinationPath: normalizedDestination,
164 parentPath: path.dirname(normalizedDestination),181 parentPath: path.dirname(normalizedDestination),
165 directoryName: path.basename(normalizedDestination),182 directoryName: path.basename(normalizedDestination),
166 };183 occupied: false,
184 } as const;
167 },185 },
168 );186 );
169 187
188 const isMatchingClone = Effect.fn("SourceControlRepositoryService.isMatchingClone")(function* (
189 destinationPath: string,
190 remoteUrl: string,
191 ) {
192 const repositoryRoot = yield* git.execute({
193 operation: "SourceControlRepositoryService.cloneRepository.verifyRoot",
194 cwd: destinationPath,
195 args: ["rev-parse", "--show-toplevel"],
196 timeoutMs: 5_000,
197 maxOutputBytes: 16 * 1024,
198 });
199 const reportedRoot = repositoryRoot.stdout.trim();
200 if (reportedRoot.length === 0) return false;
201 const [canonicalDestination, canonicalRoot] = yield* Effect.all([
202 fileSystem.realPath(destinationPath),
203 fileSystem.realPath(reportedRoot),
204 ]);
205 if (canonicalDestination !== canonicalRoot) return false;
206 const existingRemote = yield* git.readConfigValue(destinationPath, "remote.origin.url");
207 if (existingRemote === null || existingRemote.trim() !== remoteUrl.trim()) {
208 return false;
209 }
210
211 // A repository shell with only an origin, or with fetched refs but an
212 // unfinished checkout, can be left behind by an interrupted clone.
213 const headCommit = yield* git
214 .execute({
215 operation: "SourceControlRepositoryService.cloneRepository.verifyHead",
216 cwd: destinationPath,
217 args: ["rev-parse", "--verify", "HEAD^{commit}"],
218 timeoutMs: 5_000,
219 maxOutputBytes: 16 * 1024,
220 })
221 .pipe(
222 Effect.map((result) => result.stdout.trim() || null),
223 Effect.orElseSucceed(() => null),
224 );
225 if (headCommit === null) return false;
226
227 const remoteCommits = yield* git
228 .execute({
229 operation: "SourceControlRepositoryService.cloneRepository.verifyRemoteRefs",
230 cwd: destinationPath,
231 args: ["for-each-ref", "--format=%(objectname)", "refs/remotes/origin"],
232 timeoutMs: 5_000,
233 maxOutputBytes: 64 * 1024,
234 })
235 .pipe(
236 Effect.map((result) => new Set(result.stdout.split("\n").map((line) => line.trim()))),
237 Effect.orElseSucceed(() => new Set<string>()),
238 );
239 if (!remoteCommits.has(headCommit)) return false;
240
241 return yield* git
242 .execute({
243 operation: "SourceControlRepositoryService.cloneRepository.verifyCheckout",
244 cwd: destinationPath,
245 args: ["status", "--porcelain=v1", "--untracked-files=no"],
246 timeoutMs: 5_000,
247 maxOutputBytes: 16 * 1024,
248 })
249 .pipe(
250 Effect.map((result) => result.stdout.trim().length === 0),
251 Effect.orElseSucceed(() => false),
252 );
253 });
254
170 const cloneRepository = Effect.fn("SourceControlRepositoryService.cloneRepository")(function* (255 const cloneRepository = Effect.fn("SourceControlRepositoryService.cloneRepository")(function* (
171 input: SourceControlCloneRepositoryInput,256 input: SourceControlCloneRepositoryInput,
172 ) {257 ) {
173 const preparedDestination = yield* prepareDestination(input.destinationPath);258 const destinationPath = yield* normalizeDestinationPath(input.destinationPath);
174 let repository: SourceControlRepositoryInfo | null = null;259 const destinationKey = yield* canonicalDestinationKey(destinationPath);
175 let remoteUrl = input.remoteUrl?.trim() ?? null;260 return yield* cloneOperations.withLock(
176 let provider: SourceControlProviderKind = input.provider ?? "unknown";261 destinationKey,
262 Effect.gen(function* () {
263 let repository: SourceControlRepositoryInfo | null = null;
264 let remoteUrl = input.remoteUrl?.trim() ?? null;
265 let provider: SourceControlProviderKind = input.provider ?? "unknown";
177 266
178 if (input.provider && input.repository) {267 if (!remoteUrl && !(input.provider && input.repository)) {
179 repository = yield* lookupRepository({268 return yield* new SourceControlRepositoryError({
180 provider: input.provider,269 operation: "cloneRepository",
181 repository: input.repository,270 provider,
182 cwd: preparedDestination.parentPath,271 detail: "Enter a repository path or clone URL before cloning.",
183 });272 });
184 remoteUrl = selectRemoteUrl(repository, input.protocol);273 }
185 provider = input.provider;
186 }
187 274
188 if (!remoteUrl) {275 const preparedDestination = yield* prepareDestination(destinationPath);
189 return yield* new SourceControlRepositoryError({
190 operation: "cloneRepository",
191 provider,
192 detail: "Enter a repository path or clone URL before cloning.",
193 });
194 }
195 276
196 yield* git.execute({277 if (input.provider && input.repository) {
197 operation: "SourceControlRepositoryService.cloneRepository",278 repository = yield* lookupRepository({
198 cwd: preparedDestination.parentPath,279 provider: input.provider,
199 args: ["clone", remoteUrl, preparedDestination.directoryName],280 repository: input.repository,
200 timeoutMs: 120_000,281 cwd: preparedDestination.parentPath,
201 maxOutputBytes: 256 * 1024,282 });
202 });283 remoteUrl = selectRemoteUrl(repository, input.protocol);
284 provider = input.provider;
285 }
286
287 if (!remoteUrl) {
288 return yield* new SourceControlRepositoryError({
289 operation: "cloneRepository",
290 provider,
291 detail: "Enter a repository path or clone URL before cloning.",
292 });
293 }
203 294
204 return {295 if (preparedDestination.occupied) {
205 cwd: preparedDestination.destinationPath,296 const matches = yield* isMatchingClone(destinationPath, remoteUrl).pipe(
206 remoteUrl,297 Effect.orElseSucceed(() => false),
207 repository,298 );
208 };299 if (!matches) {
300 return yield* new SourceControlRepositoryError({
301 operation: "cloneRepository",
302 provider,
303 detail:
304 "Destination path already exists and is not a clone of the requested repository.",
305 });
306 }
307 return { cwd: destinationPath, remoteUrl, repository };
308 }
309
310 yield* git.execute({
311 operation: "SourceControlRepositoryService.cloneRepository",
312 cwd: preparedDestination.parentPath,
313 args: ["clone", remoteUrl, preparedDestination.directoryName],
314 timeoutMs: 120_000,
315 maxOutputBytes: 256 * 1024,
316 });
317
318 return { cwd: destinationPath, remoteUrl, repository };
319 }),
320 );
209 });321 });
210 322
211 const publishRepository = Effect.fn("SourceControlRepositoryService.publishRepository")(323 const publishRepository = Effect.fn("SourceControlRepositoryService.publishRepository")(