| 567 | } |
| 568 | |
| 569 | async *cloneWithProgress( |
| 570 | input: CloneProjectParams, |
| 571 | signal?: AbortSignal |
| 572 | ): AsyncGenerator<CloneEvent> { |
| 573 | const prepared = this.validateAndPrepareClone(input); |
| 574 | if (!prepared.success) { |
| 575 | yield { type: "error", code: "clone_failed", error: prepared.error }; |
| 576 | return; |
| 577 | } |
| 578 | |
| 579 | const { cloneUrl, normalizedPath, cloneParentDir } = prepared.data; |
| 580 | const cloneWorkPath = `${normalizedPath}.mux-clone-${randomBytes(6).toString("hex")}`; |
| 581 | let cloneSucceeded = false; |
| 582 | // Preserve full stderr so failed clones can surface git's fatal message instead of only exit code 128. |
| 583 | let collectedStderr = ""; |
| 584 | let askpass: Awaited<ReturnType<typeof createMediatedAskpassSession>> | undefined; |
| 585 | |
| 586 | const cleanupPartialClone = async () => { |
| 587 | if (cloneSucceeded) { |
| 588 | return; |
| 589 | } |
| 590 | |
| 591 | try { |
| 592 | // Only clean up the temp clone path we created so we never delete |
| 593 | // a destination directory that another process created concurrently. |
| 594 | await fsPromises.rm(cloneWorkPath, { recursive: true, force: true }); |
| 595 | } catch { |
| 596 | // Ignore cleanup errors — the original error is more important. |
| 597 | } |
| 598 | }; |
| 599 | |
| 600 | try { |
| 601 | if (signal?.aborted) { |
| 602 | yield { type: "error", code: "clone_failed", error: "Clone cancelled" }; |
| 603 | return; |
| 604 | } |
| 605 | |
| 606 | let cloneParentStat: Stats | null = null; |
| 607 | try { |
| 608 | cloneParentStat = await fsPromises.stat(cloneParentDir); |
| 609 | } catch (error) { |
| 610 | const err = error as NodeJS.ErrnoException; |
| 611 | if (err.code !== "ENOENT") { |
| 612 | throw error; |
| 613 | } |
| 614 | } |
| 615 | |
| 616 | if (cloneParentStat && !cloneParentStat.isDirectory()) { |
| 617 | yield { |
| 618 | type: "error", |
| 619 | code: "clone_failed", |
| 620 | error: "Clone destination parent directory is not a directory", |
| 621 | }; |
| 622 | return; |
| 623 | } |
| 624 | |
| 625 | let destinationStat: Stats | null = null; |
| 626 | try { |