Repository navigation
git engine
The Rust native module that powers Git operations in GitNotēs. See Architecture for context.
GitNotēs uses a custom Rust Git library (git2-based) compiled as a native module and exposed to JavaScript via Turbo Module (New Architecture). This provides significant performance benefits over pure-JavaScript git implementations for large repositories.
Package name: gitnotes-git-engine (local npm package at modules/GitEngine/)
gitnotes/
├── modules/
│ └── GitEngine/ # Rust crate (git2-based)
│ ├── Cargo.toml # Rust dependencies (git2, serde, etc.)
│ └── src/ # Rust source (lib.rs + ops modules)
├── src/
│ └── services/
│ └── git/
│ └── engine/
│ └── GitEngine.ts # JavaScript/TypeScript facade
└── package.json
The Rust crate's internal module structure (
lib.rs,git_ops.rs, etc.) is an implementation detail — consult the crate directly for the canonical list.
File: src/services/git/engine/GitEngine.ts
Exports the native module as GitEngine. The JS side imports it as:
import * as GitEngine from './engine/GitEngine';These are the JS facade operations in src/services/git/engine/GitEngine.ts. The facade is a typed wrapper around the native Rust module. All ops run on the native engine queue under a per-repo flock.
Clones a repository to the local dest path.
Parameters:
-
url— Git remote URL -
dest— local destination path -
repoId— optional repo identifier
Returns: Promise<string> — final path after clone
Initializes a new repository (bare = true creates a push-ready local remote).
Removes a cloned repository and its working tree.
Repairs a corrupted repository. Returns a report listing what was corrupted, repaired, and unrecoverable.
Returns the current branch, ahead/behind count, and branch list for a repo.
Returns:
interface RepoStatus {
branch: string;
branches: BranchInfo[];
ahead: number;
behind: number;
currentBranch: string;
}Lists the status of all files — staged, modified, untracked.
Computes a diff of all changed files between HEAD and working tree.
Computes a diff for a single file.
Stages file changes for the next commit.
Unstages files.
Removes files from the repository. keepWorktree preserves the working tree file.
Discards working tree changes for the given files (git checkout --).
Line-level partial staging — stage only selected diff hunks.
Creates a commit with the staged changes.
Parameters:
-
repoPath— local repository path -
message— commit message -
author—{ name: string; email: string }
Returns: Promise<CommitInfo> — commit SHA, message, author, timestamp
Returns recent commits (default limit: 50).
Per-file diff of one commit against its first parent (git show-style).
Detaches HEAD at a commit (git checkout <commit>). Rejected if tracked files have staged/unstaged changes.
Moves HEAD to a commit, keeping index + working tree (git reset --soft).
git revert a commit. Merge commits are rejected.
Lists currently conflicted files.
Marks a conflicted file as resolved.
Returns the ours, theirs, and base blob content for a conflicted file (for unified-editor conflict UI).
Marks a conflicted path resolved by staging its working tree content as final.
Fetches from a remote.
Pulls changes from the remote. The native module returns { kind, message, conflicts }; the facade maps FastForward, UpToDate, and Merged to { ok: true } and all other pull kinds to { ok: false, error }.
Pushes the current branch. Force-push is deliberately NOT exposed — the facade hardcodes force: false. Returns { ok: boolean; error?: string }.
GitEngine.pushWithIntegrate(repoPath: string, remoteName?: string, repoId?: string | null): Promise<PushIntegrateResult>
Pushes with transparent fetch + integrate (rebase or merge) when non-fast-forward. Returns { ok, error?, conflicts, pushed, integrated }.
Lists all branches.
Creates a new branch.
Checks out a branch. If checkout fails because the remote tracking ref is missing, callers should fetch from the remote first and retry.
Note: For remote-to-local checkout, callers (e.g.,
GitBranchCoordinator) handle the fetch-and-retry logic. SeeGitBranchCoordinator.checkout()for the full remote branch checkout flow.
Deletes a branch.
Renames a branch.
Lists configured remotes.
Adds a remote.
Removes a remote.
Updates a remote's URL.
Registers the credential the engine should use for a repo's remotes. Persists to expo-secure-store.
Reads the currently registered credential.
Removes the credential for a repo.
Returns repo metadata — path, branch, commit count, isClean.
Backs up a corrupt repo to a timestamped directory (never deletes). Used by reclone().
Returns whether another op currently holds the flock for the repo.
Returns the native module version string.
Returns the engine name ('git2' when Rust module is active, 'stub' when unavailable).
File: scripts/build-rust.sh
--ios Build for iOS (iOS device + simulator slices)
--android Build for Android (arm64-v8a + armeabi-v7a)
--all Build for all platforms
--bindings Build only the JSI bindings (faster iteration)
Dependencies:
- Rust toolchain (
rustc,cargo) -
cargo-lipo— for iOS fat library -
cargo-ndk— for Android NDK
./scripts/build-rust.sh --iosOutputs: modules/GitEngine/target/aarch64-apple-ios/release/libgitnotes_git_engine.a + modules/GitEngine/target/aarch64-apple-ios-sim/release/libgitnotes_git_engine.a
./scripts/build-rust.sh --androidOutputs: modules/GitEngine/target/aarch64-linux-android/release/libgitnotes_git_engine.a
The module is linked via Expo's autolinking system. The package.json entry:
{
"dependencies": {
"gitnotes-git-engine": "file:./modules/GitEngine"
}
}Expo reads modules/GitEngine/package.json and links the native module automatically during prebuild.
Release builds on Android use R8 minification (enabled via enableMinifyInReleaseBuilds: true in app.json under expo-build-properties). The GitEngine module depends on JNA (declared as net.java.dev.jna:jna:5.17.0@aar in modules/GitEngine/android/build.gradle), and the UniFFI Kotlin bindings call into the native cdylib through JNA's Pointer class. R8 stripping the Pointer class or its peer field causes GitEngine native library to be unavailable at runtime with the error Can't obtain peer field ID for class com.sun.jna.Pointer. A ProGuard/R8 keep rule -keep class com.sun.jna.Pointer { protected long peer; } is required to preserve the class and the field — -keepclassmembers alone is insufficient because it does not retain the class itself.
The TypeScript facade at src/services/git/engine/GitEngine.ts is the single import point for all native Git operations. Services call it directly — there is no intermediate stub layer for real operations.
// CloneSyncService.save() — the actual clone-mode write path
import * as GitEngine from './git/engine/GitEngine';
await FileSystem.writeAsStringAsync(fullPath, content);
// The user stages and commits from the Git workspace or floating Git button.Key integration points:
-
CloneSyncService.save()(src/services/cloneSyncServiceImpl.ts) — writes the file without staging it -
CommitServiceorcommitOps.ts— stages and creates explicit commits -
ConflictResolverScreen— callsGitEngine.conflicts(),GitEngine.getConflictBlobs(),GitEngine.markConflictResolved() -
BackgroundSyncService/ForegroundSyncService— callGitEngine.push()andGitEngine.pull()
- Performance: Git operations on large repos (thousands of files) are fast
- Memory: Rust's zero-cost abstractions keep memory footprint low
- Safety: No garbage collection pauses during sync operations
- Portability: Rust compiles to iOS, Android, and desktop from the same codebase
- Sync Architecture — How GitEngine fits into sync
- Services — CloneSyncService that uses GitEngine