Skip to content

Commit 3d819b8

Browse files
authored
feat: add git apply features (#174)
1 parent 44a2a2d commit 3d819b8

File tree

5 files changed

+205
-0
lines changed

5 files changed

+205
-0
lines changed

index.d.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2640,6 +2640,43 @@ export declare class Repository {
26402640
* @returns An Annotated Commit created from `FETCH_HEAD`.
26412641
*/
26422642
getAnnotatedCommitFromFetchHead(branchName: string, remoteUrl: string, id: string): AnnotatedCommit
2643+
/**
2644+
* Apply a Diff to the given repo, making changes directly in the working directory, the index, or both.
2645+
*
2646+
* @category Repository/Methods
2647+
* ```ts
2648+
* class Repository {
2649+
* apply(diff: Diff, location: ApplyLocation, options?: ApplyOptions | null | undefined): void;
2650+
* }
2651+
* ```
2652+
*
2653+
* @param {Diff} diff - The diff to apply
2654+
* @param {ApplyLocation} location - The location to apply
2655+
* @param {ApplyOptions} [options] - The options for the apply
2656+
*/
2657+
apply(diff: Diff, location: ApplyLocation, options?: ApplyOptions | undefined | null): void
2658+
/**
2659+
* Apply a Diff to the provided tree, and return the resulting Index.
2660+
*
2661+
* @category Repository/Methods
2662+
* @signature
2663+
* ```ts
2664+
* class Repository {
2665+
* applyToTree(
2666+
* tree: Tree,
2667+
* diff: Diff,
2668+
* options?: ApplyOptions | null | undefined
2669+
* ): Index;
2670+
* }
2671+
* ```
2672+
*
2673+
* @param {Tree} tree - The tree to apply the diff to
2674+
* @param {Diff} diff - The diff to apply
2675+
* @param {ApplyOptions} [options] - The options for the apply
2676+
*
2677+
* @returns The postimage of the application
2678+
*/
2679+
applyToTree(tree: Tree, diff: Diff, options?: ApplyOptions | undefined | null): Index
26432680
/**
26442681
* Creates a blame object for the file at the given path
26452682
*
@@ -5673,6 +5710,23 @@ export interface AmendOptions {
56735710
messageEncoding?: string
56745711
}
56755712

5713+
/**
5714+
* Possible application locations for git_apply
5715+
* see <https://libgit2.org/libgit2/#HEAD/type/git_apply_options>
5716+
*/
5717+
export type ApplyLocation = /** Apply the patch to the workdir */
5718+
'WorkDir'|
5719+
/** Apply the patch to the index */
5720+
'Index'|
5721+
/** Apply the patch to both the working directory and the index */
5722+
'Both';
5723+
5724+
/** Options to specify when applying a diff */
5725+
export interface ApplyOptions {
5726+
/** Don't actually make changes, just test that the patch applies. */
5727+
check?: boolean
5728+
}
5729+
56765730
/**
56775731
* - `Unspecified` : Use the setting from the remote's configuration
56785732
* - `Auto` : Ask the server for tags pointing to objects we're already downloading

index.js

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/apply.rs

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
use crate::diff::Diff;
2+
use crate::index::Index;
3+
use crate::repository::Repository;
4+
use crate::tree::Tree;
5+
use napi_derive::napi;
6+
7+
#[derive(Debug, Copy, Clone)]
8+
#[napi(string_enum)]
9+
/// Possible application locations for git_apply
10+
/// see <https://libgit2.org/libgit2/#HEAD/type/git_apply_options>
11+
pub enum ApplyLocation {
12+
/// Apply the patch to the workdir
13+
WorkDir,
14+
/// Apply the patch to the index
15+
Index,
16+
/// Apply the patch to both the working directory and the index
17+
Both,
18+
}
19+
20+
impl From<ApplyLocation> for git2::ApplyLocation {
21+
fn from(value: ApplyLocation) -> Self {
22+
match value {
23+
ApplyLocation::Both => Self::Both,
24+
ApplyLocation::Index => Self::Index,
25+
ApplyLocation::WorkDir => Self::WorkDir,
26+
}
27+
}
28+
}
29+
30+
#[napi(object, object_to_js = false)]
31+
/// Options to specify when applying a diff
32+
pub struct ApplyOptions {
33+
/// Don't actually make changes, just test that the patch applies.
34+
pub check: Option<bool>,
35+
// TODO(@seokju-na): Consider node.js is single-thread so the calling callback from Rust side
36+
// will make dead-lock. May be we should make `apply` function as async?
37+
//
38+
// #[napi(ts_type = "(data: DiffHunkData | null | undefined) => boolean")]
39+
// pub hunk_callback: Option<HunkCallback>,
40+
// #[napi(ts_type = "(data: DeltaData | null | undefined) => boolean")]
41+
// pub delta_callback: Option<DeltaCallback>,
42+
}
43+
44+
impl From<ApplyOptions> for git2::ApplyOptions<'_> {
45+
fn from(value: ApplyOptions) -> Self {
46+
let mut options = git2::ApplyOptions::new();
47+
if let Some(check) = value.check {
48+
options.check(check);
49+
}
50+
options
51+
}
52+
}
53+
54+
#[napi]
55+
impl Repository {
56+
#[napi]
57+
/// Apply a Diff to the given repo, making changes directly in the working directory, the index, or both.
58+
///
59+
/// @category Repository/Methods
60+
/// ```ts
61+
/// class Repository {
62+
/// apply(diff: Diff, location: ApplyLocation, options?: ApplyOptions | null | undefined): void;
63+
/// }
64+
/// ```
65+
///
66+
/// @param {Diff} diff - The diff to apply
67+
/// @param {ApplyLocation} location - The location to apply
68+
/// @param {ApplyOptions} [options] - The options for the apply
69+
pub fn apply(&self, diff: &Diff, location: ApplyLocation, options: Option<ApplyOptions>) -> crate::Result<()> {
70+
self
71+
.inner
72+
.apply(&diff.inner, location.into(), options.map(|x| x.into()).as_mut())?;
73+
Ok(())
74+
}
75+
76+
#[napi]
77+
/// Apply a Diff to the provided tree, and return the resulting Index.
78+
///
79+
/// @category Repository/Methods
80+
/// @signature
81+
/// ```ts
82+
/// class Repository {
83+
/// applyToTree(
84+
/// tree: Tree,
85+
/// diff: Diff,
86+
/// options?: ApplyOptions | null | undefined
87+
/// ): Index;
88+
/// }
89+
/// ```
90+
///
91+
/// @param {Tree} tree - The tree to apply the diff to
92+
/// @param {Diff} diff - The diff to apply
93+
/// @param {ApplyOptions} [options] - The options for the apply
94+
///
95+
/// @returns The postimage of the application
96+
pub fn apply_to_tree(&self, tree: &Tree, diff: &Diff, options: Option<ApplyOptions>) -> crate::Result<Index> {
97+
let inner = self
98+
.inner
99+
.apply_to_tree(&tree.inner, &diff.inner, options.map(|x| x.into()).as_mut())?;
100+
Ok(Index { inner })
101+
}
102+
}

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#![deny(clippy::all)]
22
#![allow(clippy::len_without_is_empty)]
33
pub mod annotated_commit;
4+
pub mod apply;
45
pub mod blame;
56
pub mod blob;
67
pub mod branch;

tests/apply.spec.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import fs from 'node:fs/promises';
2+
import path from 'node:path';
3+
import { describe, expect, it } from 'vitest';
4+
import { openRepository } from '../index';
5+
import { useFixture } from './fixtures';
6+
7+
describe('apply', () => {
8+
it('apply at working directory', async () => {
9+
const p = await useFixture('diff');
10+
const repo = await openRepository(p);
11+
12+
const headTree = repo.head().peelToTree();
13+
await fs.writeFile(path.join(p, 'A'), 'A modified');
14+
15+
const index = repo.index();
16+
index.addPath('A');
17+
index.write();
18+
const treeId = repo.getTree(index.writeTree());
19+
20+
const diff = repo.diffTreeToTree(headTree, treeId);
21+
repo.checkoutHead({ force: true });
22+
repo.apply(diff, 'WorkDir');
23+
24+
const content = await fs.readFile(path.join(p, 'A'), 'utf-8');
25+
expect(content).toEqual('A modified');
26+
});
27+
28+
it('will not actually apply if check option is enabled', async () => {
29+
const p = await useFixture('diff');
30+
const repo = await openRepository(p);
31+
32+
const headTree = repo.head().peelToTree();
33+
await fs.writeFile(path.join(p, 'A'), 'A modified');
34+
35+
const index = repo.index();
36+
index.addPath('A');
37+
index.write();
38+
const treeId = repo.getTree(index.writeTree());
39+
40+
const diff = repo.diffTreeToTree(headTree, treeId);
41+
repo.checkoutHead({ force: true });
42+
repo.apply(diff, 'WorkDir', { check: true });
43+
44+
const content = await fs.readFile(path.join(p, 'A'), 'utf-8');
45+
expect(content).not.toEqual('A modified');
46+
});
47+
});

0 commit comments

Comments
 (0)