Skip to content

Commit d2cd39f

Browse files
authored
feat: impl revparse, commit, signature, object (#17)
* add revparse test fixture * impl revparse * revparse mode to bitflag * remove unused fixtures * docs, object
1 parent 3e40606 commit d2cd39f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+778
-797
lines changed

Cargo.toml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ version = "0.0.0"
77
crate-type = ["cdylib"]
88

99
[dependencies]
10-
git2 = { version = "0.19.0", features = ["vendored-libgit2", "vendored-openssl"] }
11-
# Default enable napi4 feature, see https://nodejs.org/api/n-api.html#node-api-version-matrix
12-
napi = { version = "2.16.13", default-features = false, features = ["napi4"] }
10+
bitflags = "2.1.0"
11+
chrono = "0.4"
12+
git2 = { version = "0.19.0", features = ["vendored-libgit2", "vendored-openssl"] }
13+
napi = { version = "2.16.13", default-features = false, features = ["napi4", "chrono_date"] }
1314
napi-derive = "2.16.12"
1415
serde = { version = "1", features = ["derive"] }
1516
thiserror = "2.0.3"

index.d.ts

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,10 +184,129 @@ export interface RepositoryCloneOptions {
184184
recursive?: boolean;
185185
fetch?: FetchOptions;
186186
}
187+
/** Creates a new repository in the specified folder. */
187188
export declare function initRepository(path: string, options?: RepositoryInitOptions | undefined | null, signal?: AbortSignal | undefined | null): Promise<Repository>
189+
/** Attempt to open an already-existing repository at `path`. */
188190
export declare function openRepository(path: string, options?: RepositoryOpenOptions | undefined | null, signal?: AbortSignal | undefined | null): Promise<Repository>
191+
/**
192+
* Attempt to open an already-existing repository at or above `path`
193+
*
194+
* This starts at `path` and looks up the filesystem hierarchy
195+
* until it finds a repository.
196+
*/
189197
export declare function discoverRepository(path: string, signal?: AbortSignal | undefined | null): Promise<Repository>
198+
/**
199+
* Clone a remote repository.
200+
*
201+
* This will use the options configured so far to clone the specified URL
202+
* into the specified local path.
203+
*/
190204
export declare function cloneRepository(url: string, path: string, options?: RepositoryCloneOptions | undefined | null, signal?: AbortSignal | undefined | null): Promise<Repository>
205+
/** Flags for the Revspec. */
206+
export const enum RevparseMode {
207+
/** The spec targeted a single object (1 << 0) */
208+
Single = 1,
209+
/** The spec targeted a range of commits (1 << 1) */
210+
Range = 2,
211+
/** The spec used the `...` operator, which invokes special semantics. (1 << 2) */
212+
MergeBase = 4
213+
}
214+
/** Check revparse mode contains specific flags. */
215+
export declare function revparseModeContains(source: number, target: number): boolean
216+
/** A revspec represents a range of revisions within a repository. */
217+
export interface Revspec {
218+
/** Access the `from` range of this revspec. */
219+
from?: string
220+
/** Access the `to` range of this revspec. */
221+
to?: string
222+
/** Returns the intent of the revspec. */
223+
mode: number
224+
}
225+
/**
226+
* A Signature is used to indicate authorship of various actions throughout the
227+
* library.
228+
*
229+
* Signatures contain a name, email, and timestamp.
230+
*/
231+
export interface Signature {
232+
/** Name on the signature. */
233+
name: string
234+
/** Email on the signature. */
235+
email: string
236+
/** Time in seconds, from epoch */
237+
timestamp: number
238+
}
239+
export interface CreateSignatureOptions {
240+
/** Time in seconds, from epoch */
241+
timestamp: number
242+
/** Timezone offset, in minutes */
243+
offset?: number
244+
}
245+
/** Create a new action signature. */
246+
export declare function createSignature(name: string, email: string, options?: CreateSignatureOptions | undefined | null): Signature
247+
/** An enumeration all possible kinds objects may have. */
248+
export const enum ObjectType {
249+
/** Any kind of git object */
250+
Any = 0,
251+
/** An object which corresponds to a git commit */
252+
Commit = 1,
253+
/** An object which corresponds to a git tree */
254+
Tree = 2,
255+
/** An object which corresponds to a git blob */
256+
Blob = 3,
257+
/** An object which corresponds to a git tag */
258+
Tag = 4
259+
}
260+
/** A structure to represent a git commit */
261+
export declare class Commit {
262+
/** Get the id (SHA1) of a repository commit */
263+
id(): string
264+
/** Get the author of this commit. */
265+
author(): Signature
266+
/** Get the committer of this commit. */
267+
committer(): Signature
268+
/**
269+
* Get the full message of a commit.
270+
*
271+
* The returned message will be slightly prettified by removing any
272+
* potential leading newlines.
273+
*
274+
* Throws error if the message is not valid utf-8
275+
*/
276+
message(): string
277+
/**
278+
* Get the short "summary" of the git commit message.
279+
*
280+
* The returned message is the summary of the commit, comprising the first
281+
* paragraph of the message with whitespace trimmed and squashed.
282+
*
283+
* Throws error if the summary is not valid utf-8.
284+
*/
285+
summary(): string | null
286+
/**
287+
* Get the long "body" of the git commit message.
288+
*
289+
* The returned message is the body of the commit, comprising everything
290+
* but the first paragraph of the message. Leading and trailing whitespaces
291+
* are trimmed.
292+
*
293+
* Throws error if the summary is not valid utf-8.
294+
*/
295+
body(): string | null
296+
/**
297+
* Get the commit time (i.e. committer time) of a commit.
298+
*
299+
* The first element of the tuple is the time, in seconds, since the epoch.
300+
* The second element is the offset, in minutes, of the time zone of the
301+
* committer's preferred time zone.
302+
*/
303+
time(): Date
304+
}
305+
/**
306+
* A structure representing a [remote][1] of a git repository.
307+
*
308+
* [1]: http://git-scm.com/book/en/Git-Basics-Working-with-Remotes
309+
*/
191310
export declare class Remote {
192311
/**
193312
* Get the remote's name.
@@ -233,7 +352,32 @@ export declare class Remote {
233352
/** Get the remote’s default branch. */
234353
defaultBranch(signal?: AbortSignal | undefined | null): Promise<string>
235354
}
355+
/**
356+
* An owned git repository, representing all state associated with the
357+
* underlying filesystem.
358+
*
359+
* This structure corresponds to a `git_repository` in libgit2.
360+
*
361+
* When a repository goes out of scope, it is freed in memory but not deleted
362+
* from the filesystem.
363+
*/
236364
export declare class Repository {
365+
/**
366+
* Lookup a reference to one of the commits in a repository.
367+
*
368+
* Returns `null` if the commit does not exist.
369+
*/
370+
findCommit(oid: string): Commit | null
371+
/** Lookup a reference to one of the commits in a repository. */
372+
getCommit(oid: string): Commit
373+
/**
374+
* Lookup a reference to one of the objects in a repository.
375+
*
376+
* Returns `null` if the object does not exist.
377+
*/
378+
findObject(oid: string): GitObject | null
379+
/** Lookup a reference to one of the objects in a repository. */
380+
getObject(oid: string): GitObject
237381
/** List all remotes for a given repository */
238382
remoteNames(): Array<string>
239383
/**
@@ -246,11 +390,65 @@ export declare class Repository {
246390
findRemote(name: string): Remote | null
247391
/** Add a remote with the default fetch refspec to the repository’s configuration. */
248392
createRemote(name: string, url: string, options?: CreateRemoteOptions | undefined | null): Remote
393+
/** Tests whether this repository is a bare repository or not. */
249394
isBare(): boolean
395+
/** Tests whether this repository is a shallow clone. */
250396
isShallow(): boolean
397+
/** Tests whether this repository is a worktree. */
251398
isWorktree(): boolean
399+
/** Tests whether this repository is empty. */
252400
isEmpty(): boolean
401+
/**
402+
* Returns the path to the `.git` folder for normal repositories or the
403+
* repository itself for bare repositories.
404+
*/
253405
path(): string
406+
/** Returns the current state of this repository */
254407
state(): RepositoryState
408+
/**
409+
* Get the path of the working directory for this repository.
410+
*
411+
* If this repository is bare, then `null` is returned.
412+
*/
255413
workdir(): string | null
414+
/**
415+
* Execute a rev-parse operation against the `spec` listed.
416+
*
417+
* The resulting revision specification is returned, or an error is
418+
* returned if one occurs.
419+
*/
420+
revparse(spec: string): Revspec
421+
/** Find a single object, as specified by a revision string. */
422+
revparseSingle(spec: string): string
423+
}
424+
/**
425+
* A structure to represent a git [object][1]
426+
*
427+
* [1]: http://git-scm.com/book/en/Git-Internals-Git-Objects
428+
*/
429+
export declare class GitObject {
430+
/** Get the id (SHA1) of a repository object */
431+
id(): string
432+
/**
433+
* Get the object type of object.
434+
*
435+
* If the type is unknown, then `null` is returned.
436+
*/
437+
type(): ObjectType | null
438+
/**
439+
* Recursively peel an object until an object of the specified type is met.
440+
*
441+
* If you pass `Any` as the target type, then the object will be
442+
* peeled until the type changes (e.g. a tag will be chased until the
443+
* referenced object is no longer a tag).
444+
*/
445+
peel(objType: ObjectType): GitObject
446+
/** Recursively peel an object until a commit is found */
447+
peelToCommit(): Commit
448+
/**
449+
* Attempt to view this object as a commit.
450+
*
451+
* Returns `null` if the object is not actually a commit.
452+
*/
453+
asCommit(): Commit | null
256454
}

index.js

Lines changed: 7 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/commit.rs

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
use crate::repository::Repository;
2+
use crate::signature::Signature;
3+
use chrono::{DateTime, Utc};
4+
use napi::bindgen_prelude::*;
5+
use napi_derive::napi;
6+
use std::ops::Deref;
7+
8+
pub(crate) enum CommitInner {
9+
Repo(SharedReference<Repository, git2::Commit<'static>>),
10+
Commit(git2::Commit<'static>),
11+
}
12+
13+
impl Deref for CommitInner {
14+
type Target = git2::Commit<'static>;
15+
16+
fn deref(&self) -> &Self::Target {
17+
match self {
18+
Self::Repo(repo) => repo.deref(),
19+
Self::Commit(commit) => commit,
20+
}
21+
}
22+
}
23+
24+
#[napi]
25+
/// A structure to represent a git commit
26+
pub struct Commit {
27+
pub(crate) inner: CommitInner,
28+
}
29+
30+
#[napi]
31+
impl Commit {
32+
#[napi]
33+
/// Get the id (SHA1) of a repository commit
34+
pub fn id(&self) -> String {
35+
self.inner.id().to_string()
36+
}
37+
38+
#[napi]
39+
/// Get the author of this commit.
40+
pub fn author(&self) -> crate::Result<Signature> {
41+
let signature = Signature::try_from(self.inner.author())?;
42+
Ok(signature)
43+
}
44+
45+
#[napi]
46+
/// Get the committer of this commit.
47+
pub fn committer(&self) -> crate::Result<Signature> {
48+
let signature = Signature::try_from(self.inner.committer())?;
49+
Ok(signature)
50+
}
51+
52+
#[napi]
53+
/// Get the full message of a commit.
54+
///
55+
/// The returned message will be slightly prettified by removing any
56+
/// potential leading newlines.
57+
///
58+
/// Throws error if the message is not valid utf-8
59+
pub fn message(&self) -> crate::Result<String> {
60+
let message = std::str::from_utf8(self.inner.message_raw_bytes())?.to_string();
61+
Ok(message)
62+
}
63+
64+
#[napi]
65+
/// Get the short "summary" of the git commit message.
66+
///
67+
/// The returned message is the summary of the commit, comprising the first
68+
/// paragraph of the message with whitespace trimmed and squashed.
69+
///
70+
/// Throws error if the summary is not valid utf-8.
71+
pub fn summary(&self) -> crate::Result<Option<String>> {
72+
let summary = match self.inner.summary_bytes() {
73+
Some(bytes) => Some(std::str::from_utf8(bytes)?.to_string()),
74+
None => None,
75+
};
76+
Ok(summary)
77+
}
78+
79+
#[napi]
80+
/// Get the long "body" of the git commit message.
81+
///
82+
/// The returned message is the body of the commit, comprising everything
83+
/// but the first paragraph of the message. Leading and trailing whitespaces
84+
/// are trimmed.
85+
///
86+
/// Throws error if the summary is not valid utf-8.
87+
pub fn body(&self) -> crate::Result<Option<String>> {
88+
let body = match self.inner.body_bytes() {
89+
Some(bytes) => Some(std::str::from_utf8(bytes)?.to_string()),
90+
None => None,
91+
};
92+
Ok(body)
93+
}
94+
95+
#[napi]
96+
/// Get the commit time (i.e. committer time) of a commit.
97+
///
98+
/// The first element of the tuple is the time, in seconds, since the epoch.
99+
/// The second element is the offset, in minutes, of the time zone of the
100+
/// committer's preferred time zone.
101+
pub fn time(&self) -> crate::Result<DateTime<Utc>> {
102+
let time = DateTime::from_timestamp(self.inner.time().seconds(), 0).ok_or(crate::Error::InvalidTime)?;
103+
Ok(time)
104+
}
105+
}
106+
107+
#[napi]
108+
impl Repository {
109+
#[napi]
110+
/// Lookup a reference to one of the commits in a repository.
111+
///
112+
/// Returns `null` if the commit does not exist.
113+
pub fn find_commit(&self, this: Reference<Repository>, env: Env, oid: String) -> Option<Commit> {
114+
self.get_commit(this, env, oid).ok()
115+
}
116+
117+
#[napi]
118+
/// Lookup a reference to one of the commits in a repository.
119+
pub fn get_commit(&self, this: Reference<Repository>, env: Env, oid: String) -> crate::Result<Commit> {
120+
let commit = this.share_with(env, |repo| {
121+
repo
122+
.inner
123+
.find_commit_by_prefix(&oid)
124+
.map_err(crate::Error::from)
125+
.map_err(|e| e.into())
126+
})?;
127+
Ok(Commit {
128+
inner: CommitInner::Repo(commit),
129+
})
130+
}
131+
}

0 commit comments

Comments
 (0)