Skip to content

Commit 983e266

Browse files
feat: implement Repository.getFileCreatedDate method with async support (#100)
* Initial plan * Initial analysis of simple-git repository for implementing getFileCreatedDate Co-authored-by: Brooooooklyn <3468483+Brooooooklyn@users.noreply.github.com> * Implement getFileCreatedDate and getFileCreatedDateAsync methods with comprehensive tests Co-authored-by: Brooooooklyn <3468483+Brooooooklyn@users.noreply.github.com> * Fix cargo fmt and cargo clippy warnings Co-authored-by: Brooooooklyn <3468483+Brooooooklyn@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Brooooooklyn <3468483+Brooooooklyn@users.noreply.github.com>
1 parent 7d01607 commit 983e266

File tree

3 files changed

+172
-10
lines changed

3 files changed

+172
-10
lines changed

__test__/repo.spec.mjs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,58 @@ test("Date should be equal with cli", (t) => {
3333
}
3434
});
3535

36+
test("Created date should be equal with cli", (t) => {
37+
const { repo } = t.context;
38+
if (process.env.CI) {
39+
t.notThrows(() => repo.getFileCreatedDate(join("src", "lib.rs")));
40+
} else {
41+
t.deepEqual(
42+
new Date(
43+
execSync("git log --reverse --format=%cd --date=iso src/lib.rs", {
44+
cwd: workDir,
45+
})
46+
.toString("utf8")
47+
.split('\n')[0]
48+
.trim(),
49+
).valueOf(),
50+
repo.getFileCreatedDate(join("src", "lib.rs")),
51+
);
52+
}
53+
});
54+
55+
test("Created date async should work", async (t) => {
56+
const { repo } = t.context;
57+
if (process.env.CI) {
58+
await t.notThrowsAsync(() => repo.getFileCreatedDateAsync(join("src", "lib.rs")));
59+
} else {
60+
const expectedDate = new Date(
61+
execSync("git log --reverse --format=%cd --date=iso src/lib.rs", {
62+
cwd: workDir,
63+
})
64+
.toString("utf8")
65+
.split('\n')[0]
66+
.trim(),
67+
).valueOf();
68+
69+
const actualDate = await repo.getFileCreatedDateAsync(join("src", "lib.rs"));
70+
t.deepEqual(expectedDate, actualDate);
71+
}
72+
});
73+
74+
test("Created date should throw for non-existent file", (t) => {
75+
const { repo } = t.context;
76+
t.throws(() => repo.getFileCreatedDate("non-existent-file.txt"), {
77+
message: /Failed to get created date for/
78+
});
79+
});
80+
81+
test("Created date async should throw for non-existent file", async (t) => {
82+
const { repo } = t.context;
83+
await t.throwsAsync(() => repo.getFileCreatedDateAsync("non-existent-file.txt"), {
84+
message: /Failed to get created date for/
85+
});
86+
});
87+
3688
test("Should be able to resolve head", (t) => {
3789
const { repo } = t.context;
3890
t.is(

index.d.ts

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ export declare class Commit {
2323
/** Get the tree pointed to by this commit. */
2424
tree(): Tree
2525
/**
26-
*
2726
* The returned message will be slightly prettified by removing any
2827
* potential leading newlines.
2928
*
@@ -182,9 +181,18 @@ export declare class Cred {
182181
credtype(): CredentialType
183182
}
184183

185-
/** An iterator over the diffs in a delta */
186-
export declare class Deltas {
187-
[Symbol.iterator](): Iterator<DiffDelta, void, void>
184+
/**
185+
* An iterator over the diffs in a delta
186+
*
187+
* This type extends JavaScript's `Iterator`, and so has the iterator helper
188+
* methods. It may extend the upcoming TypeScript `Iterator` class in the future.
189+
*
190+
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Iterator#iterator_helper_methods
191+
* @see https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-6.html#iterator-helper-methods
192+
*/
193+
export declare class Deltas extends Iterator<DiffDelta, void, void> {
194+
195+
next(value?: void): IteratorResult<DiffDelta, void>
188196
}
189197

190198
export declare class Diff {
@@ -276,7 +284,6 @@ export declare class FetchOptions {
276284
/**
277285
* Set fetch depth, a value less or equal to 0 is interpreted as pull
278286
* everything (effectively the same as not declaring a limit depth).
279-
*
280287
*/
281288
depth(depth: number): this
282289
/**
@@ -474,7 +481,6 @@ export declare class Remote {
474481
*
475482
* Convenience function to connect to a remote, download the data,
476483
* disconnect and update the remote-tracking branches.
477-
*
478484
*/
479485
fetch(refspecs: Array<string>, fetchOptions?: FetchOptions | undefined | null): void
480486
/** Update the tips to the new state */
@@ -838,10 +844,18 @@ export declare class Repository {
838844
revWalk(): RevWalk
839845
getFileLatestModifiedDate(filepath: string): number
840846
getFileLatestModifiedDateAsync(filepath: string, signal?: AbortSignal | undefined | null): Promise<number>
847+
getFileCreatedDate(filepath: string): number
848+
getFileCreatedDateAsync(filepath: string, signal?: AbortSignal | undefined | null): Promise<number>
841849
}
842850

843-
export declare class RevWalk {
844-
[Symbol.iterator](): Iterator<string, void, void>
851+
/**
852+
* This type extends JavaScript's `Iterator`, and so has the iterator helper
853+
* methods. It may extend the upcoming TypeScript `Iterator` class in the future.
854+
*
855+
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Iterator#iterator_helper_methods
856+
* @see https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-6.html#iterator-helper-methods
857+
*/
858+
export declare class RevWalk extends Iterator<string, void, void> {
845859
/**
846860
* Reset a revwalk to allow re-configuring it.
847861
*
@@ -927,6 +941,7 @@ export declare class RevWalk {
927941
* The reference must point to a commitish.
928942
*/
929943
hideRef(reference: string): this
944+
next(value?: void): IteratorResult<string, void>
930945
}
931946

932947
/**
@@ -1035,8 +1050,16 @@ export declare class TreeEntry {
10351050
toObject(repo: Repository): GitObject
10361051
}
10371052

1038-
export declare class TreeIter {
1039-
[Symbol.iterator](): Iterator<TreeEntry, void, void>
1053+
/**
1054+
* This type extends JavaScript's `Iterator`, and so has the iterator helper
1055+
* methods. It may extend the upcoming TypeScript `Iterator` class in the future.
1056+
*
1057+
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Iterator#iterator_helper_methods
1058+
* @see https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-6.html#iterator-helper-methods
1059+
*/
1060+
export declare class TreeIter extends Iterator<TreeEntry, void, void> {
1061+
1062+
next(value?: void): IteratorResult<TreeEntry, void>
10401063
}
10411064

10421065
/** Automatic tag following options. */

src/repo.rs

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,13 @@ pub struct GitDateTask {
108108

109109
unsafe impl Send for GitDateTask {}
110110

111+
pub struct GitCreatedDateTask {
112+
repo: RwLock<napi::bindgen_prelude::Reference<Repository>>,
113+
filepath: String,
114+
}
115+
116+
unsafe impl Send for GitCreatedDateTask {}
117+
111118
#[napi]
112119
impl Task for GitDateTask {
113120
type Output = i64;
@@ -133,6 +140,34 @@ impl Task for GitDateTask {
133140
}
134141
}
135142

143+
#[napi]
144+
impl Task for GitCreatedDateTask {
145+
type Output = i64;
146+
type JsValue = i64;
147+
148+
fn compute(&mut self) -> napi::Result<Self::Output> {
149+
get_file_created_date(
150+
&self
151+
.repo
152+
.read()
153+
.map_err(|err| napi::Error::new(Status::GenericFailure, format!("{err}")))?
154+
.inner,
155+
&self.filepath,
156+
)
157+
.convert_without_message()
158+
.and_then(|value| {
159+
value.expect_not_null(format!(
160+
"Failed to get created date for [{}]",
161+
&self.filepath
162+
))
163+
})
164+
}
165+
166+
fn resolve(&mut self, _env: napi::Env, output: Self::Output) -> napi::Result<Self::JsValue> {
167+
Ok(output)
168+
}
169+
}
170+
136171
#[napi]
137172
pub struct Repository {
138173
pub(crate) inner: git2::Repository,
@@ -876,6 +911,31 @@ impl Repository {
876911
signal,
877912
))
878913
}
914+
915+
#[napi]
916+
pub fn get_file_created_date(&self, filepath: String) -> Result<i64> {
917+
get_file_created_date(&self.inner, &filepath)
918+
.convert_without_message()
919+
.and_then(|value| {
920+
value.expect_not_null(format!("Failed to get created date for [{filepath}]"))
921+
})
922+
}
923+
924+
#[napi]
925+
pub fn get_file_created_date_async(
926+
&self,
927+
self_ref: Reference<Repository>,
928+
filepath: String,
929+
signal: Option<AbortSignal>,
930+
) -> Result<AsyncTask<GitCreatedDateTask>> {
931+
Ok(AsyncTask::with_optional_signal(
932+
GitCreatedDateTask {
933+
repo: RwLock::new(self_ref),
934+
filepath,
935+
},
936+
signal,
937+
))
938+
}
879939
}
880940

881941
fn get_file_modified_date(
@@ -923,3 +983,30 @@ fn get_file_modified_date(
923983
}),
924984
)
925985
}
986+
987+
fn get_file_created_date(
988+
repo: &git2::Repository,
989+
filepath: &str,
990+
) -> std::result::Result<Option<i64>, git2::Error> {
991+
// TODO: Add rename detection support using git2::DiffFindOptions for full `git log --follow` semantics
992+
let mut rev_walk = repo.revwalk()?;
993+
rev_walk.push_head()?;
994+
rev_walk.set_sorting(git2::Sort::TIME & git2::Sort::TOPOLOGICAL)?;
995+
let path = PathBuf::from(filepath);
996+
997+
let mut earliest_commit_time: Option<i64> = None;
998+
999+
// Traverse all commits to find the earliest one that contains the file
1000+
for oid in rev_walk.by_ref().filter_map(|oid| oid.ok()) {
1001+
if let Ok(commit) = repo.find_commit(oid)
1002+
&& let Ok(tree) = commit.tree()
1003+
{
1004+
// Check if the file exists in this commit's tree
1005+
if tree.get_path(&path).is_ok() {
1006+
earliest_commit_time = Some(commit.time().seconds() * 1000);
1007+
}
1008+
}
1009+
}
1010+
1011+
Ok(earliest_commit_time)
1012+
}

0 commit comments

Comments
 (0)