-
Notifications
You must be signed in to change notification settings - Fork 0
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/ios-local/rust/libgitnotes_git2.a (simulator) + modules/GitEngine/target/aarch64-apple-ios/release/libgitnotes_git2.a (device)
./scripts/build-rust.sh --androidOutputs: modules/GitEngine/android/src/main/jniLibs/<abi>/libgitnotes_git2.so
yarn android builds the Rust Android libraries before running expo run:android.
The libraries are copied into the ignored modules/GitEngine/android/src/main/jniLibs/<abi>/
directories and packaged automatically by the GitEngine Android module. Run
yarn build:rust:android directly when rebuilding the native libraries without
launching the app.
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 5.18.0 (net.java.dev.jna:jna:5.18.0@aar), and the UniFFI Kotlin bindings use JNA direct mapping through Native.register(...). R8 stripping JNA internals can make GitEngine unavailable at runtime, including errors such as Can't obtain peer field ID for class com.sun.jna.Pointer or Can't obtain static method fromNative(Class, Object) from class com.sun.jna.Native. The complete JNA Android keep rules are required:
-dontwarn java.awt.**
-dontwarn com.sun.jna.**
-keep class com.sun.jna.** { *; }
-keep class * extends com.sun.jna.** { *; }
-keepclassmembers class * extends com.sun.jna.** { public *; }
The rules are configured through expo-build-properties.extraProguardRules in app.json, so Expo prebuild regenerates them into android/app/proguard-rules.pro. Keeping the full JNA package also preserves the private reflection targets required by JNA's native initialization and the JNA types used by generated UniFFI bindings.
GitHub HTTPS clones use TLS certificate verification. The vendored OpenSSL build in the Rust cdylib does not auto-detect Android's system CA certificate directories. On Android, git2's OpenSSL adapter must be explicitly pointed at the platform's CA store before any network operation.
The Kotlin GitEngineModule.OnCreate calls configureAndroidCaBundle() which:
- Checks for
/apex/com.android.conscrypt/cacerts(Android 10+, preferred — Conscrypt APEX module). - Falls back to
/system/etc/security/cacerts(legacy path) only if the APEX path does not exist. - If neither directory exists (not on Android), returns early — no configuration needed.
- Reads all readable regular files from the selected directory (Android CA files are named like
01419da9.0), sorted by name. - For each file, extracts the
-----BEGIN CERTIFICATE-----through-----END CERTIFICATE-----block (excluding any trailingCertificate:metadata and fingerprints that Android appends), appends a newline, and writes to the bundle. - Concatenates them into a single PEM bundle file in app-private storage (
filesDir/gitnotes_ca_bundle.pem), written atomically (temp file + rename). - Skips rebuild if the bundle already exists and is non-empty.
- Fails silently (returns null, no configuration) if the bundle would be empty — no invalid bundle is passed to Rust.
- Passes the bundle path to
setSslCertFile()— the UniFFI-exportedset_ssl_cert_file(cert_file)inrust/src/api/bridge.rs.
Rust then calls git2::opts::set_ssl_cert_file with the bundle path, which configures libgit2's OpenSSL adapter to use the PEM bundle for certificate verification.
libgit2 routes set_ssl_cert_dir to OpenSSL's SSL_CTX_load_verify_locations directory mode, which expects hash-named certificate files (e.g. a88126e5.0). Android uses this naming convention, but OpenSSL's directory-mode hash lookup is incompatible with Android's filesystem layout in this context. Using set_ssl_cert_file with a concatenated PEM bundle avoids this incompatibility.
git2::opts::set_ssl_cert_file calls into git_libgit2_opts(GIT_OPT_SET_SSL_CERT_LOCATIONS, …) which mutates a C global (git__ssl_ctx) inside libgit2's OpenSSL adapter. This is process-global state, not thread-local. The function is therefore unsafe, and the Kotlin call site must satisfy these conditions:
- The call runs once, on the main thread, before any async engine operations are dispatched.
- No other thread can be inside a git2 call at the moment of invocation.
GitEngineModule.OnCreate satisfies both: it runs on the Android main thread during app startup, before the React Native JS thread has posted any engine work.
No certificate bypass is involved. set_ssl_cert_file directs OpenSSL to the Android CA bundle; it does not disable verification. GitHub's TLS certificate remains validated against trusted system CAs.
// Errors from Android CA configuration.
pub enum AndroidCaError {
SetCertFile(git2::Error),
NotReadable(std::io::Error),
}
// Returns the best available Android CA certificate directory.
pub fn android_ca_dir(apex_override: Option<&Path>, legacy_override: Option<&Path>) -> Option<PathBuf>
// Configures git2's SSL CA file for Android.
// SAFETY: must be called from main thread before any git2 operations.
pub fn set_ssl_cert_file(cert_file: &Path) -> Result<(), AndroidCaError>The #[uniffi::export] wrapper in rust/src/api/bridge.rs exposes set_ssl_cert_file as setSslCertFile(certFile: String) to Kotlin. The UniFFI layer maps AndroidCaError to BridgeError::Other, so Kotlin callers can catch BridgeException if the CA configuration fails.
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