* Transfer a git bundle to the remote and return its path. * Callers are responsible for cleanup of the remote bundle file.
(
projectPath: string,
remoteBundlePath: string,
initLogger: InitLogger,
abortSignal?: AbortSignal
)
| 1849 | * Callers are responsible for cleanup of the remote bundle file. |
| 1850 | */ |
| 1851 | private async transferBundleToRemote( |
| 1852 | projectPath: string, |
| 1853 | remoteBundlePath: string, |
| 1854 | initLogger: InitLogger, |
| 1855 | abortSignal?: AbortSignal |
| 1856 | ): Promise<string> { |
| 1857 | await this.transport.acquireConnection({ |
| 1858 | abortSignal, |
| 1859 | onWait: (waitMs) => logSSHBackoffWait(initLogger, waitMs), |
| 1860 | }); |
| 1861 | |
| 1862 | if (abortSignal?.aborted) { |
| 1863 | throw new Error("Bundle creation aborted"); |
| 1864 | } |
| 1865 | |
| 1866 | initLogger.logStep("Creating git bundle..."); |
| 1867 | // Use --branches --tags instead of --all to exclude refs/remotes/origin/* |
| 1868 | // from the bundle. Those tracking refs are from the local machine's last |
| 1869 | // fetch and can be arbitrarily stale — importing them into the shared bare |
| 1870 | // base repo would give worktrees a wrong "commits behind" count. |
| 1871 | const gitProc = spawn( |
| 1872 | "git", |
| 1873 | ["-C", projectPath, "bundle", "create", "-", "--branches", "--tags"], |
| 1874 | { |
| 1875 | stdio: ["ignore", "pipe", "pipe"], |
| 1876 | windowsHide: true, |
| 1877 | } |
| 1878 | ); |
| 1879 | |
| 1880 | // Handle stderr manually - do NOT use streamProcessToLogger here. |
| 1881 | // It attaches a stdout listener that drains data before pipeReadableToWebWritable |
| 1882 | // can consume it, corrupting the bundle. |
| 1883 | let stderr = ""; |
| 1884 | gitProc.stderr?.on("data", (data: Buffer) => { |
| 1885 | const chunk = data.toString(); |
| 1886 | stderr += chunk; |
| 1887 | for (const line of chunk.split("\n").filter(Boolean)) { |
| 1888 | initLogger.logStderr(line); |
| 1889 | } |
| 1890 | }); |
| 1891 | |
| 1892 | const remoteAbortController = createAbortController(300_000, abortSignal); |
| 1893 | const remoteStream = await this.exec(`cat > ${this.quoteForRemote(remoteBundlePath)}`, { |
| 1894 | cwd: "~", |
| 1895 | abortSignal: remoteAbortController.signal, |
| 1896 | }); |
| 1897 | |
| 1898 | try { |
| 1899 | try { |
| 1900 | await pipeReadableToWebWritable(gitProc.stdout, remoteStream.stdin, abortSignal); |
| 1901 | } catch (error) { |
| 1902 | gitProc.kill(); |
| 1903 | throw error; |
| 1904 | } |
| 1905 | |
| 1906 | const [gitExitCode, remoteExitCode] = await Promise.all([ |
| 1907 | waitForProcessExit(gitProc), |
| 1908 | remoteStream.exitCode, |
no test coverage detected