( gitUrl: string, targetPath: string, ref?: string, sparsePaths?: string[], )
| 801 | * automatically; for explicit git@host:… URLs, users see an actionable error. |
| 802 | */ |
| 803 | export async function gitClone( |
| 804 | gitUrl: string, |
| 805 | targetPath: string, |
| 806 | ref?: string, |
| 807 | sparsePaths?: string[], |
| 808 | ): Promise<{ code: number; stderr: string }> { |
| 809 | const useSparse = sparsePaths && sparsePaths.length > 0 |
| 810 | const args = [ |
| 811 | '-c', |
| 812 | 'core.sshCommand=ssh -o BatchMode=yes -o StrictHostKeyChecking=yes', |
| 813 | 'clone', |
| 814 | '--depth', |
| 815 | '1', |
| 816 | ] |
| 817 | |
| 818 | if (useSparse) { |
| 819 | // Partial clone: skip blob download until checkout, defer checkout until |
| 820 | // after sparse-checkout is configured. Submodules are intentionally dropped |
| 821 | // for sparse clones — sparse monorepos rarely need them, and recursing |
| 822 | // submodules would defeat the partial-clone bandwidth savings. |
| 823 | args.push('--filter=blob:none', '--no-checkout') |
| 824 | } else { |
| 825 | args.push('--recurse-submodules', '--shallow-submodules') |
| 826 | } |
| 827 | |
| 828 | if (ref) { |
| 829 | args.push('--branch', ref) |
| 830 | } |
| 831 | |
| 832 | args.push(gitUrl, targetPath) |
| 833 | |
| 834 | const timeoutMs = getPluginGitTimeoutMs() |
| 835 | logForDebugging( |
| 836 | `git clone: url=${redactUrlCredentials(gitUrl)} ref=${ref ?? 'default'} timeout=${timeoutMs}ms`, |
| 837 | ) |
| 838 | |
| 839 | const result = await execFileNoThrowWithCwd(gitExe(), args, { |
| 840 | timeout: timeoutMs, |
| 841 | stdin: 'ignore', |
| 842 | env: { ...process.env, ...GIT_NO_PROMPT_ENV }, |
| 843 | }) |
| 844 | |
| 845 | // Scrub credentials from execa's error/stderr fields before any logging or |
| 846 | // returning. execa's shortMessage embeds the full command line (including |
| 847 | // the credentialed URL), and result.stderr may also contain it on some git |
| 848 | // versions. |
| 849 | const redacted = redactUrlCredentials(gitUrl) |
| 850 | if (gitUrl !== redacted) { |
| 851 | if (result.error) result.error = result.error.replaceAll(gitUrl, redacted) |
| 852 | if (result.stderr) |
| 853 | result.stderr = result.stderr.replaceAll(gitUrl, redacted) |
| 854 | } |
| 855 | |
| 856 | if (result.code === 0) { |
| 857 | if (useSparse) { |
| 858 | // Configure the sparse cone, then materialize only those paths. |
| 859 | // `sparse-checkout set --cone` handles both init and path selection |
| 860 | // in a single step on git >= 2.25. |
no test coverage detected