diff --git a/Cargo.lock b/Cargo.lock index 9b4c38b..49fd12c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -145,6 +145,95 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + [[package]] name = "git2" version = "0.20.2" @@ -409,15 +498,22 @@ version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + [[package]] name = "napi" -version = "3.5.0" +version = "3.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d00c1a7ffcf62e0889630f122f8920383f5a9ce4b54377b05c2833fb6123857" +checksum = "000f205daae6646003fdc38517be6232af2b150bad4b67bdaf4c5aadb119d738" dependencies = [ "bitflags", "chrono", "ctor", + "futures", "napi-build", "napi-sys", "nohash-hasher", @@ -426,9 +522,9 @@ dependencies = [ [[package]] name = "napi-build" -version = "2.3.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68064c4cf827376751236ee6785e0e38a6461f83a7a7f227c89f6256f3e96cc2" +checksum = "d376940fd5b723c6893cd1ee3f33abbfd86acb1cd1ec079f3ab04a2a3bc4d3b1" [[package]] name = "napi-derive" @@ -459,9 +555,9 @@ dependencies = [ [[package]] name = "napi-sys" -version = "3.1.0" +version = "3.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f200fd782433de18d46d496223be780837b2f3772e5816f4425e0520bff26c2" +checksum = "8eb602b84d7c1edae45e50bbf1374696548f36ae179dfa667f577e384bb90c2b" dependencies = [ "libloading", ] @@ -521,6 +617,18 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "pkg-config" version = "0.3.31" @@ -589,6 +697,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + [[package]] name = "smallvec" version = "1.15.0" diff --git a/Cargo.toml b/Cargo.toml index 8edda7a..37a60a3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ crate-type = ["cdylib"] bitflags = "2.1.0" chrono = "0.4" git2 = { version = "0.20.2", features = ["vendored-libgit2", "vendored-openssl"] } -napi = { version = "3.5.0", default-features = false, features = ["napi6", "chrono_date"] } +napi = { version = "3.7.1", default-features = false, features = ["napi6", "chrono_date"] } napi-derive = "3.3.0" thiserror = "2.0.3" diff --git a/index.d.ts b/index.d.ts index cb86dfe..b09479d 100644 --- a/index.d.ts +++ b/index.d.ts @@ -4127,20 +4127,6 @@ export declare class Repository { * @returns Returns `true` if repository is a shallow clone. */ isShallow(): boolean - /** - * Tests whether this repository is a worktree. - * - * @category Repository/Methods - * @signature - * ```ts - * class Repository { - * isWorktree(): boolean; - * } - * ``` - * - * @returns Returns `true` if repository is a worktree. - */ - isWorktree(): boolean /** * Tests whether this repository is empty. * @@ -5185,6 +5171,72 @@ export declare class Repository { * @returns If it does not exist, returns `null`. */ findTree(oid: string): Tree | null + /** + * Add a new worktree to the repository. + * + * @category Repository/Methods + * + * @signature + * ```ts + * class Repository { + * worktree(name: string, path: string, options?: WorktreeAddOptions | null | undefined): Worktree; + * } + * ``` + * + * @param {string} name - Name of the worktree to add. + * @param {string} path - Path where the worktree should be created. + * @param {WorktreeAddOptions} [options] - Options for adding the worktree. + * @returns New worktree instance. + * @throws Throws error if adding the worktree fails (e.g., path already exists, invalid reference name, or filesystem errors). + */ + worktree(name: string, path: string, options?: WorktreeAddOptions | undefined | null): Worktree + /** + * List all worktrees in the repository. + * + * @category Repository/Methods + * + * @signature + * ```ts + * class Repository { + * worktrees(): string[]; + * } + * ``` + * + * @returns Array of worktree names. + * @throws Throws error if listing worktrees fails (e.g., filesystem errors or repository corruption). + */ + worktrees(): Array + /** + * Tests whether this repository is a worktree. + * + * @category Repository/Methods + * @signature + * ```ts + * class Repository { + * isWorktree(): boolean; + * } + * ``` + * + * @returns Returns `true` if repository is a worktree. + */ + isWorktree(): boolean + /** + * Find a worktree by name. + * + * @category Repository/Methods + * + * @signature + * ```ts + * class Repository { + * findWorktree(name: string): Worktree; + * } + * ``` + * + * @param {string} name - Name of the worktree to find. + * @returns Worktree instance. + * @throws Throws error if the worktree is not found or if opening fails. + */ + findWorktree(name: string): Worktree } /** @@ -6451,6 +6503,135 @@ export declare class TreeIter extends Iterator { next(value?: void): IteratorResult } +/** A class to represent a git worktree. */ +export declare class Worktree { + /** + * Get the name of this worktree. + * + * @category Worktree/Methods + * + * @signature + * ```ts + * class Worktree { + * name(): string | null; + * } + * ``` + * + * @returns Name of this worktree. Returns `null` if the worktree has no name. + */ + name(): string | null + /** + * Get the path of this worktree. + * + * @category Worktree/Methods + * + * @signature + * ```ts + * class Worktree { + * path(): string; + * } + * ``` + * + * @returns Path of this worktree. + */ + path(): string + /** + * Validate that the worktree is in a valid state. + * + * @category Worktree/Methods + * + * @signature + * ```ts + * class Worktree { + * validate(): void; + * } + * ``` + * + * @throws Throws error if the worktree is in an invalid state. + */ + validate(): void + /** + * Lock the worktree. + * + * @category Worktree/Methods + * + * @signature + * ```ts + * class Worktree { + * lock(reason?: string | null | undefined): void; + * } + * ``` + * + * @param {string} [reason] - Optional reason for locking the worktree. + * @throws Throws error if locking fails. + */ + lock(reason?: string | undefined | null): void + /** + * Unlock the worktree. + * + * @category Worktree/Methods + * + * @signature + * ```ts + * class Worktree { + * unlock(): void; + * } + * ``` + * + * @throws Throws error if unlocking fails. + */ + unlock(): void + /** + * Check if the worktree is locked. + * + * @category Worktree/Methods + * + * @signature + * ```ts + * class Worktree { + * isLocked(): WorktreeLockStatus; + * } + * ``` + * + * @returns Lock status of the worktree. + * @throws Throws error if checking the lock status fails. + */ + isLocked(): WorktreeLockStatus + /** + * Prune the worktree. + * + * @category Worktree/Methods + * + * @signature + * ```ts + * class Worktree { + * prune(options?: WorktreePruneOptions | null | undefined): void; + * } + * ``` + * + * @param {WorktreePruneOptions} [options] - Options for pruning the worktree. + * @throws Throws error if pruning fails. + */ + prune(worktreePruneOptions?: WorktreePruneOptions | undefined | null): void + /** + * Check if the worktree is prunable. + * + * @category Worktree/Methods + * + * @signature + * ```ts + * class Worktree { + * isPrunable(options?: WorktreePruneOptions | null | undefined): boolean; + * } + * ``` + * + * @param {WorktreePruneOptions} [options] - Options for checking if the worktree is prunable. + * @returns `true` if the worktree is prunable, `false` otherwise. + * @throws Throws error if checking fails. + */ + isPrunable(worktreePruneOptions?: WorktreePruneOptions | undefined | null): boolean +} + export interface AddMailmapEntryData { realName?: string realEmail?: string @@ -8122,6 +8303,65 @@ export declare function openDefaultConfig(): Config */ export declare function openRepository(path: string, options?: RepositoryOpenOptions | undefined | null, signal?: AbortSignal | undefined | null): Promise +/** + * Open a repository from a worktree. + * + * This will open the repository associated with the given worktree. + * + * @category Repository + * + * @signature + * ```ts + * function openRepositoryFromWorktree(worktree: Worktree): Promise; + * ``` + * + * @param {Worktree} worktree - Worktree to open repository from. + * @returns {Promise} Promise that resolves to a Repository instance. + * @throws Throws error if opening the repository fails. + * + * @example + * + * Open a repository from a worktree. + * + * ```ts + * import { openWorktreeFromRepository, openRepositoryFromWorktree } from 'es-git'; + * + * const worktree = await openWorktreeFromRepository(repo); + * const repo = await openRepositoryFromWorktree(worktree); + * ``` + */ +export declare function openRepositoryFromWorktree(worktree: Worktree): Promise + +/** + * Open a worktree from a repository. + * + * This will open the worktree associated with the given repository if the + * repository is a worktree. + * + * @category Worktree + * + * @signature + * ```ts + * function openWorktreeFromRepository(repo: Repository): Promise; + * ``` + * + * @param {Repository} repo - Repository to open worktree from. + * @returns {Promise} Promise that resolves to a Worktree instance. + * @throws Throws error if the repository is not a worktree or if opening fails. + * + * @example + * + * Open a worktree from a repository. + * + * ```ts + * import { openRepository, openWorktreeFromRepository } from 'es-git'; + * + * const repo = await openRepository('.'); + * const worktree = await openWorktreeFromRepository(repo); + * ``` + */ +export declare function openWorktreeFromRepository(repo: Repository): Promise + /** * Parse a string as a bool. * @@ -8997,6 +9237,64 @@ export declare function traceSet(level: TraceLevel, callback: (level: TraceLevel export type TreeWalkMode = 'PreOrder'| 'PostOrder'; +/** Options for adding a worktree. */ +export interface WorktreeAddOptions { + /** + * If enabled, this will cause the newly added worktree to be locked. + * + * Defaults to `false`. + */ + lock?: boolean + /** + * If enabled, this will checkout the existing branch matching the worktree name. + * + * Defaults to `false`. + */ + checkoutExisting?: boolean + /** + * reference name to use for the new worktree HEAD + * + * Defaults to `null`. + */ + refName?: string +} + +/** Lock Status of a worktree */ +export interface WorktreeLockStatus { + /** Worktree is Unlocked */ + status: WorktreeLockStatusType + /** Worktree is locked with the optional message */ + reason?: string +} + +/** Lock Status of a worktree */ +export type WorktreeLockStatusType = /** Worktree is Unlocked */ +'Unlocked'| +/** Worktree is locked with the optional message */ +'Locked'; + +/** Options to configure how worktree pruning is performed. */ +export interface WorktreePruneOptions { + /** + * Controls whether valid (still existing on the filesystem) worktrees will be pruned. + * + * Defaults to `false`. + */ + valid?: boolean + /** + * Controls whether locked worktrees will be pruned. + * + * Defaults to `false`. + */ + locked?: boolean + /** + * Controls whether the actual working tree on the filesystem is recursively removed. + * + * Defaults to `false`. + */ + workingTree?: boolean +} + /** * Creates an all zero Oid structure. * diff --git a/index.js b/index.js index 61e2712..e25c710 100644 --- a/index.js +++ b/index.js @@ -77,8 +77,8 @@ function requireNative() { try { const binding = require('es-git-android-arm64') const bindingPackageVersion = require('es-git-android-arm64/package.json').version - if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -93,8 +93,8 @@ function requireNative() { try { const binding = require('es-git-android-arm-eabi') const bindingPackageVersion = require('es-git-android-arm-eabi/package.json').version - if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -114,8 +114,8 @@ function requireNative() { try { const binding = require('es-git-win32-x64-gnu') const bindingPackageVersion = require('es-git-win32-x64-gnu/package.json').version - if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -130,8 +130,8 @@ function requireNative() { try { const binding = require('es-git-win32-x64-msvc') const bindingPackageVersion = require('es-git-win32-x64-msvc/package.json').version - if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -147,8 +147,8 @@ function requireNative() { try { const binding = require('es-git-win32-ia32-msvc') const bindingPackageVersion = require('es-git-win32-ia32-msvc/package.json').version - if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -163,8 +163,8 @@ function requireNative() { try { const binding = require('es-git-win32-arm64-msvc') const bindingPackageVersion = require('es-git-win32-arm64-msvc/package.json').version - if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -182,8 +182,8 @@ function requireNative() { try { const binding = require('es-git-darwin-universal') const bindingPackageVersion = require('es-git-darwin-universal/package.json').version - if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -198,8 +198,8 @@ function requireNative() { try { const binding = require('es-git-darwin-x64') const bindingPackageVersion = require('es-git-darwin-x64/package.json').version - if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -214,8 +214,8 @@ function requireNative() { try { const binding = require('es-git-darwin-arm64') const bindingPackageVersion = require('es-git-darwin-arm64/package.json').version - if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -234,8 +234,8 @@ function requireNative() { try { const binding = require('es-git-freebsd-x64') const bindingPackageVersion = require('es-git-freebsd-x64/package.json').version - if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -250,8 +250,8 @@ function requireNative() { try { const binding = require('es-git-freebsd-arm64') const bindingPackageVersion = require('es-git-freebsd-arm64/package.json').version - if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -271,8 +271,8 @@ function requireNative() { try { const binding = require('es-git-linux-x64-musl') const bindingPackageVersion = require('es-git-linux-x64-musl/package.json').version - if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -287,8 +287,8 @@ function requireNative() { try { const binding = require('es-git-linux-x64-gnu') const bindingPackageVersion = require('es-git-linux-x64-gnu/package.json').version - if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -305,8 +305,8 @@ function requireNative() { try { const binding = require('es-git-linux-arm64-musl') const bindingPackageVersion = require('es-git-linux-arm64-musl/package.json').version - if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -321,8 +321,8 @@ function requireNative() { try { const binding = require('es-git-linux-arm64-gnu') const bindingPackageVersion = require('es-git-linux-arm64-gnu/package.json').version - if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -339,8 +339,8 @@ function requireNative() { try { const binding = require('es-git-linux-arm-musleabihf') const bindingPackageVersion = require('es-git-linux-arm-musleabihf/package.json').version - if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -355,8 +355,8 @@ function requireNative() { try { const binding = require('es-git-linux-arm-gnueabihf') const bindingPackageVersion = require('es-git-linux-arm-gnueabihf/package.json').version - if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -373,8 +373,8 @@ function requireNative() { try { const binding = require('es-git-linux-loong64-musl') const bindingPackageVersion = require('es-git-linux-loong64-musl/package.json').version - if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -389,8 +389,8 @@ function requireNative() { try { const binding = require('es-git-linux-loong64-gnu') const bindingPackageVersion = require('es-git-linux-loong64-gnu/package.json').version - if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -407,8 +407,8 @@ function requireNative() { try { const binding = require('es-git-linux-riscv64-musl') const bindingPackageVersion = require('es-git-linux-riscv64-musl/package.json').version - if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -423,8 +423,8 @@ function requireNative() { try { const binding = require('es-git-linux-riscv64-gnu') const bindingPackageVersion = require('es-git-linux-riscv64-gnu/package.json').version - if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -440,8 +440,8 @@ function requireNative() { try { const binding = require('es-git-linux-ppc64-gnu') const bindingPackageVersion = require('es-git-linux-ppc64-gnu/package.json').version - if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -456,8 +456,8 @@ function requireNative() { try { const binding = require('es-git-linux-s390x-gnu') const bindingPackageVersion = require('es-git-linux-s390x-gnu/package.json').version - if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -476,8 +476,8 @@ function requireNative() { try { const binding = require('es-git-openharmony-arm64') const bindingPackageVersion = require('es-git-openharmony-arm64/package.json').version - if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -492,8 +492,8 @@ function requireNative() { try { const binding = require('es-git-openharmony-x64') const bindingPackageVersion = require('es-git-openharmony-x64/package.json').version - if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -508,8 +508,8 @@ function requireNative() { try { const binding = require('es-git-openharmony-arm') const bindingPackageVersion = require('es-git-openharmony-arm/package.json').version - if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -613,6 +613,7 @@ module.exports.Tag = nativeBinding.Tag module.exports.Tree = nativeBinding.Tree module.exports.TreeEntry = nativeBinding.TreeEntry module.exports.TreeIter = nativeBinding.TreeIter +module.exports.Worktree = nativeBinding.Worktree module.exports.ApplyLocation = nativeBinding.ApplyLocation module.exports.AutotagOption = nativeBinding.AutotagOption module.exports.BranchType = nativeBinding.BranchType @@ -647,6 +648,8 @@ module.exports.ObjectType = nativeBinding.ObjectType module.exports.openConfig = nativeBinding.openConfig module.exports.openDefaultConfig = nativeBinding.openDefaultConfig module.exports.openRepository = nativeBinding.openRepository +module.exports.openRepositoryFromWorktree = nativeBinding.openRepositoryFromWorktree +module.exports.openWorktreeFromRepository = nativeBinding.openWorktreeFromRepository module.exports.parseConfigBool = nativeBinding.parseConfigBool module.exports.parseConfigI32 = nativeBinding.parseConfigI32 module.exports.parseConfigI64 = nativeBinding.parseConfigI64 @@ -668,4 +671,5 @@ module.exports.traceClear = nativeBinding.traceClear module.exports.TraceLevel = nativeBinding.TraceLevel module.exports.traceSet = nativeBinding.traceSet module.exports.TreeWalkMode = nativeBinding.TreeWalkMode +module.exports.WorktreeLockStatusType = nativeBinding.WorktreeLockStatusType module.exports.zeroOid = nativeBinding.zeroOid diff --git a/src/lib.rs b/src/lib.rs index 6540597..5ccbcbf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -37,6 +37,7 @@ pub mod tag; pub mod tracing; pub mod tree; pub(crate) mod util; +pub mod worktree; pub use error::Error; pub type Result = std::result::Result; diff --git a/src/repository.rs b/src/repository.rs index 9160bc2..84c414d 100644 --- a/src/repository.rs +++ b/src/repository.rs @@ -1,7 +1,8 @@ use crate::annotated_commit::AnnotatedCommit; use crate::commit::Commit; use crate::remote::FetchOptions; -use crate::util; +use crate::worktree::Worktree; +use crate::{napi_promise, util}; use napi::bindgen_prelude::*; use napi_derive::napi; use std::path::Path; @@ -278,22 +279,6 @@ impl Repository { self.inner.is_shallow() } - #[napi] - /// Tests whether this repository is a worktree. - /// - /// @category Repository/Methods - /// @signature - /// ```ts - /// class Repository { - /// isWorktree(): boolean; - /// } - /// ``` - /// - /// @returns Returns `true` if repository is a worktree. - pub fn is_worktree(&self) -> bool { - self.inner.is_worktree() - } - #[napi] /// Tests whether this repository is empty. /// @@ -876,3 +861,36 @@ pub fn clone_repository( ) -> AsyncTask { AsyncTask::with_optional_signal(CloneRepositoryTask { url, path, options }, signal) } + +#[napi] +/// Open a repository from a worktree. +/// +/// This will open the repository associated with the given worktree. +/// +/// @category Repository +/// +/// @signature +/// ```ts +/// function openRepositoryFromWorktree(worktree: Worktree): Promise; +/// ``` +/// +/// @param {Worktree} worktree - Worktree to open repository from. +/// @returns {Promise} Promise that resolves to a Repository instance. +/// @throws Throws error if opening the repository fails. +/// +/// @example +/// +/// Open a repository from a worktree. +/// +/// ```ts +/// import { openWorktreeFromRepository, openRepositoryFromWorktree } from 'es-git'; +/// +/// const worktree = await openWorktreeFromRepository(repo); +/// const repo = await openRepositoryFromWorktree(worktree); +/// ``` +pub fn open_repository_from_worktree(worktree: &Worktree, env: Env) -> PromiseRaw<'_, Repository> { + napi_promise!(&env, || { + let git2_repository = git2::Repository::open_from_worktree(&worktree.inner)?; + Ok(Repository { inner: git2_repository }) + }) +} diff --git a/src/util.rs b/src/util.rs index 5000a66..2983ee4 100644 --- a/src/util.rs +++ b/src/util.rs @@ -18,3 +18,31 @@ pub(crate) fn path_to_string(p: &Path) -> String { pub(crate) fn bitflags_contain(source: T, target: T) -> bool { source.contains(target) } + +/// Macro to create a `PromiseRaw` from a closure. +/// +/// - The closure should return `crate::Result` +/// - `Ok(value)` becomes `PromiseRaw::resolve` +/// - `Err(error)` becomes `PromiseRaw::reject` +/// - Errors from `?` operator are automatically converted to rejected promises +/// +/// # Example +/// ```rust,ignore +/// use napi::bindgen_prelude::PromiseRaw; +/// +/// fn my_async_fn(env: Env) -> PromiseRaw<'_, MyType> { +/// napi_promise!(&env, || { +/// let value = some_operation()?; // errors are caught and rejected +/// Ok(MyType { value }) +/// }) +/// } +/// ``` +#[macro_export] +macro_rules! napi_promise { + ($env:expr, || $($body:tt)*) => {{ + match (|| -> $crate::Result<_> { $($body)* })() { + Ok(value) => PromiseRaw::resolve($env, value).expect("napi_promise: failed to create resolved promise"), + Err(error) => PromiseRaw::reject($env, error.to_string()).expect("napi_promise: failed to create rejected promise") + } + }}; +} diff --git a/src/worktree.rs b/src/worktree.rs new file mode 100644 index 0000000..a6fbb25 --- /dev/null +++ b/src/worktree.rs @@ -0,0 +1,411 @@ +use crate::napi_promise; +use crate::repository::Repository; +use crate::util::path_to_string; +use napi::bindgen_prelude::PromiseRaw; +use napi::Env; +use napi_derive::napi; +use std::ops::Deref; +use std::path::Path; + +#[napi(object)] +/// Lock Status of a worktree +pub struct WorktreeLockStatus { + /// Worktree is Unlocked + pub status: WorktreeLockStatusType, + /// Worktree is locked with the optional message + pub reason: Option, +} +#[napi(string_enum)] +/// Lock Status of a worktree +pub enum WorktreeLockStatusType { + /// Worktree is Unlocked + Unlocked, + /// Worktree is locked with the optional message + Locked, +} + +#[napi(object)] +/// Options for adding a worktree. +pub struct WorktreeAddOptions { + /// If enabled, this will cause the newly added worktree to be locked. + /// + /// Defaults to `false`. + pub lock: Option, + + /// If enabled, this will checkout the existing branch matching the worktree name. + /// + /// Defaults to `false`. + pub checkout_existing: Option, + + /// reference name to use for the new worktree HEAD + /// + /// Defaults to `null`. + pub ref_name: Option, +} + +#[napi(object)] +/// Options to configure how worktree pruning is performed. +pub struct WorktreePruneOptions { + /// Controls whether valid (still existing on the filesystem) worktrees will be pruned. + /// + /// Defaults to `false`. + pub valid: Option, + + /// Controls whether locked worktrees will be pruned. + /// + /// Defaults to `false`. + pub locked: Option, + + /// Controls whether the actual working tree on the filesystem is recursively removed. + /// + /// Defaults to `false`. + pub working_tree: Option, +} + +impl From for git2::WorktreePruneOptions { + fn from(value: WorktreePruneOptions) -> Self { + let mut git2_worktree_prune_options = git2::WorktreePruneOptions::new(); + if let Some(valid) = value.valid { + git2_worktree_prune_options.valid(valid); + } + if let Some(locked) = value.locked { + git2_worktree_prune_options.locked(locked); + } + if let Some(working_tree) = value.working_tree { + git2_worktree_prune_options.working_tree(working_tree); + } + git2_worktree_prune_options + } +} + +impl From for WorktreeLockStatus { + fn from(value: git2::WorktreeLockStatus) -> Self { + match value { + git2::WorktreeLockStatus::Unlocked => WorktreeLockStatus { + status: WorktreeLockStatusType::Unlocked, + reason: None, + }, + git2::WorktreeLockStatus::Locked(reason) => WorktreeLockStatus { + status: WorktreeLockStatusType::Locked, + reason, + }, + } + } +} + +pub(crate) enum WorktreeInner { + Owned(git2::Worktree), +} + +impl Deref for WorktreeInner { + type Target = git2::Worktree; + + fn deref(&self) -> &Self::Target { + match self { + Self::Owned(inner) => inner, + } + } +} + +#[napi] +/// A class to represent a git worktree. +pub struct Worktree { + pub(crate) inner: WorktreeInner, +} + +#[napi] +impl Worktree { + #[napi] + /// Get the name of this worktree. + /// + /// @category Worktree/Methods + /// + /// @signature + /// ```ts + /// class Worktree { + /// name(): string | null; + /// } + /// ``` + /// + /// @returns Name of this worktree. Returns `null` if the worktree has no name. + pub fn name(&self) -> Option { + self.inner.name().map(String::from) + } + + #[napi] + /// Get the path of this worktree. + /// + /// @category Worktree/Methods + /// + /// @signature + /// ```ts + /// class Worktree { + /// path(): string; + /// } + /// ``` + /// + /// @returns Path of this worktree. + pub fn path(&self) -> String { + path_to_string(self.inner.path()) + } + + #[napi] + /// Validate that the worktree is in a valid state. + /// + /// @category Worktree/Methods + /// + /// @signature + /// ```ts + /// class Worktree { + /// validate(): void; + /// } + /// ``` + /// + /// @throws Throws error if the worktree is in an invalid state. + pub fn validate(&self) -> crate::Result<()> { + self.inner.validate().map_err(crate::Error::from) + } + + #[napi] + /// Lock the worktree. + /// + /// @category Worktree/Methods + /// + /// @signature + /// ```ts + /// class Worktree { + /// lock(reason?: string | null | undefined): void; + /// } + /// ``` + /// + /// @param {string} [reason] - Optional reason for locking the worktree. + /// @throws Throws error if locking fails. + pub fn lock(&self, reason: Option) -> crate::Result<()> { + self.inner.lock(reason.as_deref()).map_err(crate::Error::from) + } + + #[napi] + /// Unlock the worktree. + /// + /// @category Worktree/Methods + /// + /// @signature + /// ```ts + /// class Worktree { + /// unlock(): void; + /// } + /// ``` + /// + /// @throws Throws error if unlocking fails. + pub fn unlock(&self) -> crate::Result<()> { + self.inner.unlock().map_err(crate::Error::from) + } + + #[napi] + /// Check if the worktree is locked. + /// + /// @category Worktree/Methods + /// + /// @signature + /// ```ts + /// class Worktree { + /// isLocked(): WorktreeLockStatus; + /// } + /// ``` + /// + /// @returns Lock status of the worktree. + /// @throws Throws error if checking the lock status fails. + pub fn is_locked(&self) -> crate::Result { + let git2_lock_status = self.inner.is_locked()?; + Ok(git2_lock_status.into()) + } + + #[napi] + /// Prune the worktree. + /// + /// @category Worktree/Methods + /// + /// @signature + /// ```ts + /// class Worktree { + /// prune(options?: WorktreePruneOptions | null | undefined): void; + /// } + /// ``` + /// + /// @param {WorktreePruneOptions} [options] - Options for pruning the worktree. + /// @throws Throws error if pruning fails. + pub fn prune(&self, worktree_prune_options: Option) -> crate::Result<()> { + let mut git2_worktree_prune_options = worktree_prune_options.map(git2::WorktreePruneOptions::from); + self + .inner + .prune(git2_worktree_prune_options.as_mut()) + .map_err(crate::Error::from) + } + + #[napi] + /// Check if the worktree is prunable. + /// + /// @category Worktree/Methods + /// + /// @signature + /// ```ts + /// class Worktree { + /// isPrunable(options?: WorktreePruneOptions | null | undefined): boolean; + /// } + /// ``` + /// + /// @param {WorktreePruneOptions} [options] - Options for checking if the worktree is prunable. + /// @returns `true` if the worktree is prunable, `false` otherwise. + /// @throws Throws error if checking fails. + pub fn is_prunable(&self, worktree_prune_options: Option) -> crate::Result { + let mut git2_worktree_prune_options = worktree_prune_options.map(git2::WorktreePruneOptions::from); + self + .inner + .is_prunable(git2_worktree_prune_options.as_mut()) + .map_err(crate::Error::from) + } +} + +#[napi] +impl Repository { + #[napi] + /// Add a new worktree to the repository. + /// + /// @category Repository/Methods + /// + /// @signature + /// ```ts + /// class Repository { + /// worktree(name: string, path: string, options?: WorktreeAddOptions | null | undefined): Worktree; + /// } + /// ``` + /// + /// @param {string} name - Name of the worktree to add. + /// @param {string} path - Path where the worktree should be created. + /// @param {WorktreeAddOptions} [options] - Options for adding the worktree. + /// @returns New worktree instance. + /// @throws Throws error if adding the worktree fails (e.g., path already exists, invalid reference name, or filesystem errors). + pub fn worktree(&self, name: String, path: String, options: Option) -> crate::Result { + let mut git2_opts = git2::WorktreeAddOptions::new(); + // add non reference options + if let Some(ref _options) = options { + if let Some(lock) = _options.lock { + git2_opts.lock(lock); + } + if let Some(checkout_existing) = _options.checkout_existing { + git2_opts.checkout_existing(checkout_existing); + } + } + + // add reference option + let git2_reference = options + .as_ref() + .and_then(|opts| opts.ref_name.as_ref()) + .map(|ref_name| self.inner.find_reference(ref_name)) + .transpose()?; + git2_opts.reference(git2_reference.as_ref()); + + // add worktree + let git2_worktree = self.inner.worktree(&name, Path::new(&path), Some(&git2_opts))?; + Ok(Worktree { + inner: WorktreeInner::Owned(git2_worktree), + }) + } + + #[napi] + /// List all worktrees in the repository. + /// + /// @category Repository/Methods + /// + /// @signature + /// ```ts + /// class Repository { + /// worktrees(): string[]; + /// } + /// ``` + /// + /// @returns Array of worktree names. + /// @throws Throws error if listing worktrees fails (e.g., filesystem errors or repository corruption). + pub fn worktrees(&self) -> crate::Result> { + let git2_worktrees = self.inner.worktrees()?; + let worktree_names: Vec = git2_worktrees + .iter() + .filter_map(|name| name.map(ToString::to_string)) + .collect::>(); + Ok(worktree_names) + } + + #[napi] + /// Tests whether this repository is a worktree. + /// + /// @category Repository/Methods + /// @signature + /// ```ts + /// class Repository { + /// isWorktree(): boolean; + /// } + /// ``` + /// + /// @returns Returns `true` if repository is a worktree. + pub fn is_worktree(&self) -> bool { + self.inner.is_worktree() + } + + #[napi] + /// Find a worktree by name. + /// + /// @category Repository/Methods + /// + /// @signature + /// ```ts + /// class Repository { + /// findWorktree(name: string): Worktree; + /// } + /// ``` + /// + /// @param {string} name - Name of the worktree to find. + /// @returns Worktree instance. + /// @throws Throws error if the worktree is not found or if opening fails. + pub fn find_worktree(&self, name: String) -> crate::Result { + let git2_worktree = self.inner.find_worktree(&name)?; + Ok(Worktree { + inner: WorktreeInner::Owned(git2_worktree), + }) + } +} + +#[napi] +/// Open a worktree from a repository. +/// +/// This will open the worktree associated with the given repository if the +/// repository is a worktree. +/// +/// @category Worktree +/// +/// @signature +/// ```ts +/// function openWorktreeFromRepository(repo: Repository): Promise; +/// ``` +/// +/// @param {Repository} repo - Repository to open worktree from. +/// @returns {Promise} Promise that resolves to a Worktree instance. +/// @throws Throws error if the repository is not a worktree or if opening fails. +/// +/// @example +/// +/// Open a worktree from a repository. +/// +/// ```ts +/// import { openRepository, openWorktreeFromRepository } from 'es-git'; +/// +/// const repo = await openRepository('.'); +/// const worktree = await openWorktreeFromRepository(repo); +/// ``` +pub fn open_worktree_from_repository(repo: &Repository, env: Env) -> PromiseRaw<'_, Worktree> { + napi_promise!(&env, || { + let worktree = git2::Worktree::open_from_repository(&repo.inner)?; + Ok(Worktree { + inner: WorktreeInner::Owned(worktree), + }) + }) +} diff --git a/tests/worktree.spec.ts b/tests/worktree.spec.ts new file mode 100644 index 0000000..d8a1271 --- /dev/null +++ b/tests/worktree.spec.ts @@ -0,0 +1,264 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { type Repository, openRepository, openRepositoryFromWorktree, openWorktreeFromRepository } from '../index'; +import { useFixture } from './fixtures'; +import { makeTmpDir } from './tmp'; + +describe('worktree', () => { + const signature = { name: 'racgoo', email: 'racgoo@example.com' }; + let repo: Repository; + let baseWorktreePath: string; + + beforeEach(async () => { + const p = await useFixture('empty'); + repo = await openRepository(p); + const config = repo.config(); + config.setString('user.name', signature.name); + config.setString('user.email', signature.email); + // Create initial commit + const oid = repo.commit(repo.getTree(repo.index().writeTree()), 'initial commit', { + updateRef: 'HEAD', + author: signature, + committer: signature, + parents: [repo.head().target()!], + }); + // Create a branch for worktree + repo.createBranch('worktree-branch', repo.getCommit(oid)); + // Create base temporary directory for worktrees + baseWorktreePath = await makeTmpDir('worktree-test'); + }); + + afterEach(async () => { + // Clean up worktrees + const worktrees = repo.worktrees(); + for (const name of worktrees) { + repo.findWorktree(name).prune({ valid: true, locked: true, workingTree: true }); + } + // Remove base worktree directory + await fs.rm(baseWorktreePath, { recursive: true, force: true }); + repo.cleanupState(); + }); + + it('add worktree', async () => { + // Generate worktree + const worktreeName = 'test'; + const worktreePath = path.join(baseWorktreePath, 'test-dir'); + const worktree = repo.worktree(worktreeName, worktreePath); + expect(worktree).toBeDefined(); + + // Open test-worktree repository (path based) + const originRepoHeadName = repo.head().name(); + const worktreeRepo = await openRepositoryFromWorktree(worktree); + const head = worktreeRepo.head(); + expect(head.name()).include(worktreeName); + expect(head.name()).not.toBe(originRepoHeadName); + }); + + it('add worktree with options', async () => { + const worktreeName = 'test'; + const worktreePath = path.join(baseWorktreePath, 'test-dir'); + const worktree = repo.worktree(worktreeName, worktreePath, { + lock: true, + checkoutExisting: false, + refName: 'refs/heads/worktree-branch', + }); + expect(worktree).toBeDefined(); + expect(worktree.name()).toBe(worktreeName); + + const lockStatus = worktree.isLocked(); + expect(lockStatus).toBeDefined(); + expect(lockStatus.status).toBe('Locked'); + + // Open test-worktree repository(reference based) + const worktreeRepo = await openRepositoryFromWorktree(worktree); + const head = worktreeRepo.head(); + expect(head.name()).toBe('refs/heads/worktree-branch'); + }); + + it('list worktrees', async () => { + const worktree1 = repo.worktree('worktree1', path.join(baseWorktreePath, 'wt1-dir')); + const worktree2 = repo.worktree('worktree2', path.join(baseWorktreePath, 'wt2-dir')); + + const worktrees = repo.worktrees(); + expect(worktrees).toContain('worktree1'); + expect(worktrees).toContain('worktree2'); + }); + + it('get worktree name and path', async () => { + const worktreeName = 'test'; + const worktreePath = path.join(baseWorktreePath, 'test-dir'); + + const worktree = repo.worktree(worktreeName, worktreePath); + expect(worktree.name()).toBe(worktreeName); + + // Normalize paths to handle symlink differences on macOS + expect(await fs.realpath(worktree.path())).toBe(await fs.realpath(worktreePath)); + }); + + it('lock and unlock worktree', async () => { + const worktreeName = 'test'; + const worktreePath = path.join(baseWorktreePath, 'test-dir'); + const worktree = repo.worktree(worktreeName, worktreePath); + + // Initially unlocked + let lockStatus = worktree.isLocked(); + expect(lockStatus.status).toBe('Unlocked'); + + // Lock with reason + const lockReason = 'test reason'; + worktree.lock(lockReason); + lockStatus = worktree.isLocked(); + expect(lockStatus.status).toBe('Locked'); + expect(lockStatus.reason).toBe(lockReason); + + // Unlock + worktree.unlock(); + lockStatus = worktree.isLocked(); + expect(lockStatus.status).toBe('Unlocked'); + }); + + it('lock worktree without reason', async () => { + const worktreePath = path.join(baseWorktreePath, 'test-worktree-lock-null'); + const worktree = repo.worktree('test-worktree', worktreePath); + worktree.lock(null); + const lockStatus = worktree.isLocked(); + expect(lockStatus.status).toBe('Locked'); + expect(lockStatus.reason).toBeUndefined(); + }); + + it('validate worktree', async () => { + const worktreePath = path.join(baseWorktreePath, 'test-worktree-validate'); + const worktree = repo.worktree('test-worktree', worktreePath); + expect(() => worktree.validate()).not.toThrow(); + }); + + it('find worktree by name', async () => { + const worktreePath = path.join(baseWorktreePath, 'test-worktree-find'); + repo.worktree('test-worktree', worktreePath); + const found = repo.findWorktree('test-worktree'); + expect(found).toBeDefined(); + expect(found.name()).toBe('test-worktree'); + // Normalize paths to handle symlink differences on macOS + expect(await fs.realpath(found.path())).toBe(await fs.realpath(worktreePath)); + }); + + it('find worktree throws error if not found', async () => { + expect(() => repo.findWorktree('non-existent-worktree')).toThrow(); + }); + + it('open repository from worktree', async () => { + const worktreePath = path.join(baseWorktreePath, 'test-worktree-open-repo'); + const worktree = repo.worktree('test-worktree', worktreePath); + const worktreeRepo = await openRepositoryFromWorktree(worktree); + expect(worktreeRepo).toBeDefined(); + expect(worktreeRepo.isWorktree()).toBe(true); + }); + + it('open worktree from repository', async () => { + const worktreePath = path.join(baseWorktreePath, 'test-worktree-open-wt'); + const worktree = repo.worktree('test-worktree', worktreePath); + const worktreeRepo = await openRepositoryFromWorktree(worktree); + const openedWorktree = await openWorktreeFromRepository(worktreeRepo); + expect(openedWorktree).toBeDefined(); + expect(openedWorktree.name()).toBe('test-worktree'); + }); + + it('check if repository is worktree', async () => { + expect(repo.isWorktree()).toBe(false); + + const worktreePath = path.join(baseWorktreePath, 'test-worktree-is-wt'); + const worktree = repo.worktree('test-worktree', worktreePath); + const worktreeRepo = await openRepositoryFromWorktree(worktree); + expect(worktreeRepo.isWorktree()).toBe(true); + }); + + it('check if worktree is prunable', async () => { + const worktreePath = path.join(baseWorktreePath, 'test-worktree-prunable'); + const worktree = repo.worktree('test-worktree', worktreePath); + const isPrunable = worktree.isPrunable(); + expect(typeof isPrunable).toBe('boolean'); + }); + + it('check if worktree is prunable with options', async () => { + const worktreePath = path.join(baseWorktreePath, 'test-worktree-prunable-opts'); + const worktree = repo.worktree('test-worktree', worktreePath); + const isPrunable = worktree.isPrunable({ + valid: true, + locked: false, + workingTree: false, + }); + expect(typeof isPrunable).toBe('boolean'); + }); + + it('prune worktree', async () => { + const worktreePath = path.join(baseWorktreePath, 'test-worktree-prune'); + const worktree = repo.worktree('test-worktree', worktreePath); + const worktreeName = worktree.name(); + expect(worktreeName).toBe('test-worktree'); + + // Prune the worktree + worktree.prune({ + valid: true, + locked: false, + workingTree: true, + }); + + // Worktree should be removed from list + const worktrees = repo.worktrees(); + expect(worktrees).not.toContain('test-worktree'); + }); + + it('add worktree with checkoutExisting option', async () => { + // Create a branch with the same name as worktree + const headOid = repo.head().target()!; + repo.createBranch('existing-branch', repo.getCommit(headOid)); + const commitOid = repo.commit(repo.getTree(repo.index().writeTree()), 'test', { + updateRef: 'refs/heads/existing-branch', + author: signature, + committer: signature, + parents: [headOid], + }); + const worktreePath = path.join(baseWorktreePath, 'test-worktree-checkout-existing'); + const worktree = repo.worktree('existing-branch', worktreePath, { + checkoutExisting: true, + }); + // Check worktree + expect(worktree).toBeDefined(); + expect(worktree.name()).toBe('existing-branch'); + // Open worktree repository and check head + const worktreeRepo = await openRepositoryFromWorktree(worktree); + const head = worktreeRepo.head(); + expect(head.name()).toBe('refs/heads/existing-branch'); + expect(head.target()).toBe(commitOid); + }); + + it('add worktree with refName option', async () => { + const worktreePath = path.join(baseWorktreePath, 'test-worktree-refname'); + const worktree = repo.worktree('test-worktree', worktreePath, { + refName: 'refs/heads/worktree-branch', + }); + expect(worktree).toBeDefined(); + + const worktreeRepo = await openRepositoryFromWorktree(worktree); + const head = worktreeRepo.head(); + expect(head.name()).toBe('refs/heads/worktree-branch'); + }); + + it('add worktree throws error if path already exists', async () => { + const worktreePath = path.join(baseWorktreePath, 'test-worktree-exists'); + // Create a file at the path + await fs.writeFile(worktreePath, 'test'); + + expect(() => repo.worktree('test-worktree', worktreePath)).toThrow(); + }); + + it('add worktree throws error if invalid reference name', async () => { + const worktreePath = path.join(baseWorktreePath, 'test-worktree-invalid-ref'); + expect(() => + repo.worktree('test-worktree', worktreePath, { + refName: 'refs/heads/non-existent', + }) + ).toThrow(); + }); +});