', '.', {
},
});
```
+
+### 얕은 클론하기
+
+히스토리의 최근 일부만 클론하려면 `fetch` 옵션에 `depth`를 설정하세요. `depth`가 `1`이면 각 브랜치의 최신 커밋만
+가져와요. 리포지토리가 얕은 리포지토리인지는 [`isShallow()`](../reference/Repository/Methods/isShallow.md)로 확인할 수 있어요.
+
+```ts
+import { cloneRepository } from 'es-git';
+
+const repo = await cloneRepository('https://github.com/toss/es-git', '/path/to/clone', {
+ fetch: { depth: 1 },
+});
+console.log(repo.isShallow()); // true
+```
+
+::: warning
+얕은 클론은 `https://`, `git://`, SSH 같은 네트워크 프로토콜에서만 동작해요. libgit2의 로컬 전송은 얕은 fetch를
+지원하지 않기 때문에, 로컬 경로나 `file://` URL에서 `depth`를 설정해 클론하면 실패해요.
+:::
+
+나머지 히스토리를 나중에 가져오려면 `unshallow: true`로 리모트에서 fetch하세요. `git fetch --unshallow`와 같아요.
+
+```ts
+const remote = repo.getRemote('origin');
+await remote.fetch([], { fetch: { unshallow: true } });
+console.log(repo.isShallow()); // false
+```
+
+더 큰 `depth`로 fetch하면 히스토리를 그만큼 더 가져와요. `depth: 0`으로 fetch하거나 `depth`를 지정하지 않으면 얕은
+리포지토리가 전체 리포지토리로 바뀌지 않아요.
diff --git a/docs/reference/Remote/Methods/fetch.md b/docs/reference/Remote/Methods/fetch.md
index da7589c..19af7b2 100644
--- a/docs/reference/Remote/Methods/fetch.md
+++ b/docs/reference/Remote/Methods/fetch.md
@@ -88,7 +88,7 @@ class Remote {
depthnumber
- Set fetch depth, a value less or equal to 0 is interpreted as pull everything (effectively the same as not declaring a limit depth).
+ Set fetch depth, the number of commits to fetch from the tip of each remote branch. A depth greater than 0 creates a shallow repository (see Repository.isShallow()). A value less or equal to 0 is interpreted as pull everything (effectively the same as not declaring a limit depth). This does not unshallow an existing shallow repository; use unshallow for that. Shallow fetches are not supported by the local transport, so depth fails for local paths and file:// URLs.
downloadTagsAutotagOption
@@ -125,6 +125,11 @@ class Remote {
Set whether to perform a prune after the fetch.
- Unspecified : Use the setting from the configuration.
- On : Force pruning on.
- Off : Force pruning off
+
+ unshallowboolean
+
+ Fetch the history missing from a shallow repository, like git fetch --unshallow. If the remote repository is itself shallow, fetch as much history as it has. When true, depth is ignored.
+
diff --git a/docs/reference/Repository/cloneRepository.md b/docs/reference/Repository/cloneRepository.md
index 134a57e..aedca74 100644
--- a/docs/reference/Repository/cloneRepository.md
+++ b/docs/reference/Repository/cloneRepository.md
@@ -103,7 +103,7 @@ function cloneRepository(
depthnumber
- Set fetch depth, a value less or equal to 0 is interpreted as pull everything (effectively the same as not declaring a limit depth).
+ Set fetch depth, the number of commits to fetch from the tip of each remote branch. A depth greater than 0 creates a shallow repository (see Repository.isShallow()). A value less or equal to 0 is interpreted as pull everything (effectively the same as not declaring a limit depth). This does not unshallow an existing shallow repository; use unshallow for that. Shallow fetches are not supported by the local transport, so depth fails for local paths and file:// URLs.
downloadTagsAutotagOption
@@ -140,6 +140,11 @@ function cloneRepository(
Set whether to perform a prune after the fetch.
- Unspecified : Use the setting from the configuration.
- On : Force pruning on.
- Off : Force pruning off
+
+ unshallowboolean
+
+ Fetch the history missing from a shallow repository, like git fetch --unshallow. If the remote repository is itself shallow, fetch as much history as it has. When true, depth is ignored.
+
@@ -203,4 +208,19 @@ const repo = await cloneRepository('git@github.com:toss/es-git', '.', {
},
},
});
+```
+
+Shallow clone repository with only the latest commit.
+
+```ts
+import { cloneRepository } from 'es-git';
+
+const repo = await cloneRepository('https://github.com/toss/es-git', '/path/to/clone', {
+ fetch: { depth: 1 },
+});
+console.log(repo.isShallow()); // true
+
+// Fetch the rest of the history later.
+await repo.getRemote('origin').fetch([], { fetch: { unshallow: true } });
+console.log(repo.isShallow()); // false
```
\ No newline at end of file
diff --git a/docs/reference/Submodule/Methods/clone.md b/docs/reference/Submodule/Methods/clone.md
index ac4bd02..2be192b 100644
--- a/docs/reference/Submodule/Methods/clone.md
+++ b/docs/reference/Submodule/Methods/clone.md
@@ -178,7 +178,7 @@ class Submodule {
depthnumber
- Set fetch depth, a value less or equal to 0 is interpreted as pull everything (effectively the same as not declaring a limit depth).
+ Set fetch depth, the number of commits to fetch from the tip of each remote branch. A depth greater than 0 creates a shallow repository (see Repository.isShallow()). A value less or equal to 0 is interpreted as pull everything (effectively the same as not declaring a limit depth). This does not unshallow an existing shallow repository; use unshallow for that. Shallow fetches are not supported by the local transport, so depth fails for local paths and file:// URLs.
downloadTagsAutotagOption
@@ -215,12 +215,17 @@ class Submodule {
Set whether to perform a prune after the fetch.
- Unspecified : Use the setting from the configuration.
- On : Force pruning on.
- Off : Force pruning off
+
+ unshallowboolean
+
+ Fetch the history missing from a shallow repository, like git fetch --unshallow. If the remote repository is itself shallow, fetch as much history as it has. When true, depth is ignored.
+
- signalnull | AbortSignal
+ signalAbortSignal | null
Optional AbortSignal to cancel the operation.
diff --git a/docs/reference/Submodule/Methods/update.md b/docs/reference/Submodule/Methods/update.md
index c62db13..b7737eb 100644
--- a/docs/reference/Submodule/Methods/update.md
+++ b/docs/reference/Submodule/Methods/update.md
@@ -187,7 +187,7 @@ class Submodule {
depthnumber
- Set fetch depth, a value less or equal to 0 is interpreted as pull everything (effectively the same as not declaring a limit depth).
+ Set fetch depth, the number of commits to fetch from the tip of each remote branch. A depth greater than 0 creates a shallow repository (see Repository.isShallow()). A value less or equal to 0 is interpreted as pull everything (effectively the same as not declaring a limit depth). This does not unshallow an existing shallow repository; use unshallow for that. Shallow fetches are not supported by the local transport, so depth fails for local paths and file:// URLs.
downloadTagsAutotagOption
@@ -224,12 +224,17 @@ class Submodule {
Set whether to perform a prune after the fetch.
- Unspecified : Use the setting from the configuration.
- On : Force pruning on.
- Off : Force pruning off
+
+ unshallowboolean
+
+ Fetch the history missing from a shallow repository, like git fetch --unshallow. If the remote repository is itself shallow, fetch as much history as it has. When true, depth is ignored.
+
- signalnull | AbortSignal
+ signalAbortSignal | null
Optional AbortSignal to cancel the operation.
diff --git a/docs/usage/repository.md b/docs/usage/repository.md
index 31d15b8..1b7f397 100644
--- a/docs/usage/repository.md
+++ b/docs/usage/repository.md
@@ -82,3 +82,35 @@ const repo = await cloneRepository('https://github.com//', '.', {
},
});
```
+
+### Shallow Clone
+
+To clone only the latest part of the history, set `depth` in the `fetch` option. A depth of `1` fetches only the latest
+commit of each branch. Use [`isShallow()`](../reference/Repository/Methods/isShallow.md) to check whether a repository is
+shallow.
+
+```ts
+import { cloneRepository } from 'es-git';
+
+const repo = await cloneRepository('https://github.com/toss/es-git', '/path/to/clone', {
+ fetch: { depth: 1 },
+});
+console.log(repo.isShallow()); // true
+```
+
+::: warning
+Shallow clones need a network protocol such as `https://`, `git://` or SSH. Cloning from a local path or a `file://` URL
+with `depth` fails, because libgit2's local transport does not support shallow fetches.
+:::
+
+To fetch the rest of the history later, fetch from the remote with `unshallow: true`. This works like
+`git fetch --unshallow`.
+
+```ts
+const remote = repo.getRemote('origin');
+await remote.fetch([], { fetch: { unshallow: true } });
+console.log(repo.isShallow()); // false
+```
+
+Fetching with a larger `depth` deepens the history instead. Fetching with `depth: 0` or without `depth` does not unshallow
+a shallow repository.
diff --git a/index.d.ts b/index.d.ts
index 8f692c0..6fe3af1 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -7132,6 +7132,21 @@ export interface CherrypickOptions {
* },
* });
* ```
+ *
+ * Shallow clone repository with only the latest commit.
+ *
+ * ```ts
+ * import { cloneRepository } from 'es-git';
+ *
+ * const repo = await cloneRepository('https://github.com/toss/es-git', '/path/to/clone', {
+ * fetch: { depth: 1 },
+ * });
+ * console.log(repo.isShallow()); // true
+ *
+ * // Fetch the rest of the history later.
+ * await repo.getRemote('origin').fetch([], { fetch: { unshallow: true } });
+ * console.log(repo.isShallow()); // false
+ * ```
*/
export declare function cloneRepository(url: string, path: string, options?: RepositoryCloneOptions | undefined | null, signal?: AbortSignal | undefined | null): Promise
@@ -7780,10 +7795,26 @@ export interface FetchOptions {
/** Set whether to perform a prune after the fetch. */
prune?: FetchPrune
/**
- * Set fetch depth, a value less or equal to 0 is interpreted as pull
- * everything (effectively the same as not declaring a limit depth).
+ * Set fetch depth, the number of commits to fetch from the tip of each
+ * remote branch. A depth greater than 0 creates a shallow repository
+ * (see `Repository.isShallow()`).
+ *
+ * A value less or equal to 0 is interpreted as pull everything
+ * (effectively the same as not declaring a limit depth). This does not
+ * unshallow an existing shallow repository; use `unshallow` for that.
+ *
+ * Shallow fetches are not supported by the local transport, so `depth`
+ * fails for local paths and `file://` URLs.
*/
depth?: number
+ /**
+ * Fetch the history missing from a shallow repository, like
+ * `git fetch --unshallow`. If the remote repository is itself shallow,
+ * fetch as much history as it has.
+ *
+ * When `true`, `depth` is ignored.
+ */
+ unshallow?: boolean
/**
* Set how to behave regarding tags on the remote, such as auto-downloading
* tags for objects we're downloading or downloading all of them.
diff --git a/src/remote.rs b/src/remote.rs
index 115ef31..92f5e50 100644
--- a/src/remote.rs
+++ b/src/remote.rs
@@ -322,9 +322,23 @@ pub struct FetchOptions {
pub proxy: Option,
/// Set whether to perform a prune after the fetch.
pub prune: Option,
- /// Set fetch depth, a value less or equal to 0 is interpreted as pull
- /// everything (effectively the same as not declaring a limit depth).
+ /// Set fetch depth, the number of commits to fetch from the tip of each
+ /// remote branch. A depth greater than 0 creates a shallow repository
+ /// (see `Repository.isShallow()`).
+ ///
+ /// A value less or equal to 0 is interpreted as pull everything
+ /// (effectively the same as not declaring a limit depth). This does not
+ /// unshallow an existing shallow repository; use `unshallow` for that.
+ ///
+ /// Shallow fetches are not supported by the local transport, so `depth`
+ /// fails for local paths and `file://` URLs.
pub depth: Option,
+ /// Fetch the history missing from a shallow repository, like
+ /// `git fetch --unshallow`. If the remote repository is itself shallow,
+ /// fetch as much history as it has.
+ ///
+ /// When `true`, `depth` is ignored.
+ pub unshallow: Option,
/// Set how to behave regarding tags on the remote, such as auto-downloading
/// tags for objects we're downloading or downloading all of them.
///
@@ -427,7 +441,10 @@ impl<'a> FetchOptions {
if let Some(prune) = self.prune {
fetch.prune(prune.into());
}
- if let Some(depth) = self.depth {
+ if self.unshallow == Some(true) {
+ // libgit2's `GIT_FETCH_DEPTH_UNSHALLOW`
+ fetch.depth(i32::MAX);
+ } else if let Some(depth) = self.depth {
fetch.depth(depth);
}
if let Some(download_tags) = self.download_tags {
diff --git a/src/repository.rs b/src/repository.rs
index cb25b10..d744e19 100644
--- a/src/repository.rs
+++ b/src/repository.rs
@@ -853,6 +853,21 @@ impl Task for CloneRepositoryTask {
/// },
/// });
/// ```
+///
+/// Shallow clone repository with only the latest commit.
+///
+/// ```ts
+/// import { cloneRepository } from 'es-git';
+///
+/// const repo = await cloneRepository('https://github.com/toss/es-git', '/path/to/clone', {
+/// fetch: { depth: 1 },
+/// });
+/// console.log(repo.isShallow()); // true
+///
+/// // Fetch the rest of the history later.
+/// await repo.getRemote('origin').fetch([], { fetch: { unshallow: true } });
+/// console.log(repo.isShallow()); // false
+/// ```
pub fn clone_repository(
url: String,
path: String,
diff --git a/tests/git-server.ts b/tests/git-server.ts
new file mode 100644
index 0000000..25b738f
--- /dev/null
+++ b/tests/git-server.ts
@@ -0,0 +1,86 @@
+import { type ChildProcess, spawn, spawnSync } from 'node:child_process';
+import http from 'node:http';
+import type { AddressInfo } from 'node:net';
+
+export const hasGit = spawnSync('git', ['--version']).status === 0;
+
+/**
+ * Serve git repositories under `root` over the smart HTTP protocol, using `git http-backend`.
+ *
+ * libgit2's local transport (local paths and `file://` URLs) does not support shallow fetches,
+ * so tests for shallow clones need a real protocol.
+ */
+export async function serveGitRepositories(root: string) {
+ const children = new Set();
+ const server = http.createServer((req, res) => {
+ const url = new URL(req.url ?? '/', 'http://localhost');
+ const child = spawn('git', ['http-backend'], {
+ env: {
+ ...process.env,
+ GIT_PROJECT_ROOT: root,
+ GIT_HTTP_EXPORT_ALL: '1',
+ PATH_INFO: url.pathname,
+ QUERY_STRING: url.search.slice(1),
+ REQUEST_METHOD: req.method,
+ CONTENT_TYPE: req.headers['content-type'] ?? '',
+ },
+ });
+ children.add(child);
+ child.on('close', () => children.delete(child));
+ child.on('error', error => {
+ res.destroy(error);
+ });
+ // Ignore EPIPE when the backend exits before reading the whole request; 'error' above handles failures.
+ child.stdin.on('error', () => {});
+ req.pipe(child.stdin);
+
+ // The CGI response is headers, a blank line, then the (binary) body.
+ let head: Buffer | null = Buffer.alloc(0);
+ child.stdout.on('data', (chunk: Buffer) => {
+ if (head == null) {
+ res.write(chunk);
+ return;
+ }
+ head = Buffer.concat([head, chunk]);
+ const end = head.indexOf('\r\n\r\n');
+ if (end === -1) {
+ return;
+ }
+ let status = 200;
+ for (const line of head.subarray(0, end).toString('utf8').split('\r\n')) {
+ const separator = line.indexOf(':');
+ const name = line.slice(0, separator).trim();
+ const value = line.slice(separator + 1).trim();
+ if (name.toLowerCase() === 'status') {
+ status = Number.parseInt(value, 10);
+ } else {
+ res.setHeader(name, value);
+ }
+ }
+ res.writeHead(status);
+ res.write(head.subarray(end + 4));
+ head = null;
+ });
+ child.stdout.on('end', () => {
+ if (head != null) {
+ res.destroy(new Error('git http-backend exited without a response'));
+ return;
+ }
+ res.end();
+ });
+ });
+
+ await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
+ const { port } = server.address() as AddressInfo;
+
+ return {
+ url: `http://127.0.0.1:${port}`,
+ close: async () => {
+ for (const child of children) {
+ child.kill();
+ }
+ server.closeAllConnections();
+ await new Promise(resolve => server.close(resolve));
+ },
+ };
+}
diff --git a/tests/remote.spec.ts b/tests/remote.spec.ts
index 6cfc31c..d8b1b8c 100644
--- a/tests/remote.spec.ts
+++ b/tests/remote.spec.ts
@@ -2,6 +2,7 @@ import { describe, expect, it, vi } from 'vitest';
import { cloneRepository, initRepository, openRepository } from '../index';
import { isTarget } from './env';
import { useFixture } from './fixtures';
+import { hasGit, serveGitRepositories } from './git-server';
import { makeTmpDir } from './tmp';
describe('remote', () => {
@@ -43,6 +44,26 @@ describe('remote', () => {
await remote.fetch(['main']);
});
+ it('unshallow shallow repository', { skip: !hasGit }, async () => {
+ const root = await useFixture('commits');
+ const server = await serveGitRepositories(root);
+ try {
+ const p = await makeTmpDir('clone');
+ const repo = await cloneRepository(`${server.url}/.git`, p, { fetch: { depth: 1 } });
+ expect(repo.isShallow()).toBe(true);
+
+ await repo.getRemote('origin').fetch([], { fetch: { unshallow: true } });
+ expect(repo.isShallow()).toBe(false);
+ const revwalk = repo.revwalk();
+ revwalk.pushHead();
+ expect(revwalk.next()).toEqual('a01e9888e46729ef4aa68953ba19b02a7a64eb82');
+ expect(revwalk.next()).toEqual('b33e0101b828225f77eeff4dfa31259dcf379002');
+ expect(revwalk.next()).toBeNull();
+ } finally {
+ await server.close();
+ }
+ });
+
it('get remote default branch', { skip: isTarget('linux', undefined, 'gnu') }, async () => {
const p = await makeTmpDir('clone');
const repo = await cloneRepository('https://github.com/seokju-na/dummy-repo', p);
diff --git a/tests/repository.spec.ts b/tests/repository.spec.ts
index 1ddd508..6574b68 100644
--- a/tests/repository.spec.ts
+++ b/tests/repository.spec.ts
@@ -4,6 +4,7 @@ import { describe, expect, it, vi } from 'vitest';
import { cloneRepository, initRepository, openRepository } from '../index';
import { isTarget } from './env';
import { useFixture } from './fixtures';
+import { hasGit, serveGitRepositories } from './git-server';
import { makeTmpDir } from './tmp';
describe('Repository', () => {
@@ -51,6 +52,30 @@ describe('Repository', () => {
await expect(fs.readFile(path.join(p, 'first'), 'utf8')).resolves.toEqual(expect.stringContaining('first'));
});
+ it('shallow clone', { skip: !hasGit }, async () => {
+ const root = await useFixture('commits');
+ const server = await serveGitRepositories(root);
+ try {
+ const p = await makeTmpDir('clone');
+ const repo = await cloneRepository(`${server.url}/.git`, p, { fetch: { depth: 1 } });
+ expect(repo.isShallow()).toBe(true);
+ const revwalk = repo.revwalk();
+ revwalk.pushHead();
+ expect(revwalk.next()).toEqual('a01e9888e46729ef4aa68953ba19b02a7a64eb82');
+ expect(revwalk.next()).toBeNull();
+ } finally {
+ await server.close();
+ }
+ });
+
+ it('shallow clone is not supported from local path', async () => {
+ const localPath = await useFixture('commits');
+ const p = await makeTmpDir('clone');
+ await expect(cloneRepository(localPath, p, { fetch: { depth: 1 } })).rejects.toThrowError(
+ /shallow fetch is not supported by the local transport/
+ );
+ });
+
it('clone from remote', { skip: isTarget('linux', undefined, 'gnu') }, async () => {
const p = await makeTmpDir('clone');
const repo = await cloneRepository('https://github.com/seokju-na/dummy-repo', p);