From 1bc06a2b941dbeef4c52b2c73d659fdd1ed910ba Mon Sep 17 00:00:00 2001 From: "YSTYLE-L.X.Y" Date: Wed, 20 May 2026 15:34:39 +0800 Subject: [PATCH 1/4] add OpenHarmony (OHOS) target support On OpenHarmony the user/process model is sandbox-based. libgit2's owner validation (GIT_OPT_SET_OWNER_VALIDATION) compares file UID with process EUID, which is not applicable and causes spurious "not owned by current user" errors (code=Owner -36). This disables owner validation on all OHOS targets (cfg(target_env = "ohos"): aarch64/armv7/x86_64) via a std::sync::Once in the repository opening entry points. Also adds cross-compilation instructions to CONTRIBUTING.md. --- CONTRIBUTING.md | 29 +++++++++++++++++++++++++++++ asyncgit/src/sync/repository.rs | 19 +++++++++++++++++++ asyncgit/src/sync/utils.rs | 3 +++ 3 files changed, 51 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c443e502a5..e023dbadaf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -22,5 +22,34 @@ good first issue, you can take a look at [issues labelled with too much context so that people not familiar with the codebase yet can still make a contribution. +## Cross-compiling for OpenHarmony + +GitUI can be built for OpenHarmony (target `aarch64-unknown-linux-ohos`). This requires the [OpenHarmony SDK](https://www.openharmony.cn/) native toolchain. + +First, install the `aarch64-unknown-linux-ohos` Rust target: + +```sh +rustup target add aarch64-unknown-linux-ohos +``` + +Set the following environment variables (adjust paths to your OHOS SDK location): + +```sh +export OHOS_SDK=/path/to/ohos-sdk +export CC_aarch64_unknown_linux_ohos="$OHOS_SDK/native/llvm/bin/clang" +export CXX_aarch64_unknown_linux_ohos="$OHOS_SDK/native/llvm/bin/clang++" +export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_OHOS_LINKER="$OHOS_SDK/native/llvm/bin/clang" +export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_OHOS_AR="$OHOS_SDK/native/llvm/bin/llvm-ar" +export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_OHOS_RUSTFLAGS="-C link-arg=--sysroot=$OHOS_SDK/native/sysroot -C link-arg=-L$OHOS_SDK/native/sysroot/usr/lib/aarch64-linux-ohos -C link-arg=-Wl,--allow-multiple-definition -C link-arg=-Wl,--undefined-version -C link-arg=-Wl,--defsym=__xpg_strerror_r=0" +``` + +Then build: + +```sh +cargo build --target aarch64-unknown-linux-ohos --release +``` + +> **Note:** On OpenHarmony the user/process model is sandbox-based. GitUI automatically disables libgit2's owner validation on OHOS targets to avoid spurious "not owned by current user" errors. + [discord-server]: https://discord.gg/rZv4uxSQx3 [good-first-issues]: https://github.com/gitui-org/gitui/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22 diff --git a/asyncgit/src/sync/repository.rs b/asyncgit/src/sync/repository.rs index c49795fb7c..ecfbcdb55c 100644 --- a/asyncgit/src/sync/repository.rs +++ b/asyncgit/src/sync/repository.rs @@ -7,6 +7,22 @@ use git2::{Repository, RepositoryOpenFlags}; use crate::error::Result; +#[cfg(target_env = "ohos")] +use std::sync::Once; + +#[cfg(target_env = "ohos")] +static INIT_OHOS: Once = Once::new(); + +#[cfg(target_env = "ohos")] +pub(crate) fn init_ohos_owner_validation() { + INIT_OHOS.call_once(|| { + #[allow(unsafe_code)] + unsafe { + git2::opts::set_verify_owner_validation(false).ok(); + } + }); +} + /// pub type RepoPathRef = RefCell; @@ -55,6 +71,9 @@ impl From<&str> for RepoPath { } pub fn repo(repo_path: &RepoPath) -> Result { + #[cfg(target_env = "ohos")] + init_ohos_owner_validation(); + let repo = Repository::open_ext( repo_path.gitpath(), RepositoryOpenFlags::FROM_ENV, diff --git a/asyncgit/src/sync/utils.rs b/asyncgit/src/sync/utils.rs index 148e29baa9..8b15fff54d 100644 --- a/asyncgit/src/sync/utils.rs +++ b/asyncgit/src/sync/utils.rs @@ -26,6 +26,9 @@ pub struct Head { /// pub fn repo_open_error(repo_path: &RepoPath) -> Option { + #[cfg(target_env = "ohos")] + super::repository::init_ohos_owner_validation(); + if let Err(e) = Repository::open_ext( repo_path.gitpath(), RepositoryOpenFlags::FROM_ENV, From 5b1e7565b376e347f867154f6e1489ae687a95f6 Mon Sep 17 00:00:00 2001 From: "YSTYLE-L.X.Y" Date: Wed, 20 May 2026 15:37:49 +0800 Subject: [PATCH 2/4] docs: add OHOS SDK download link and default install paths --- CONTRIBUTING.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e023dbadaf..54a53216c3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,7 +24,12 @@ make a contribution. ## Cross-compiling for OpenHarmony -GitUI can be built for OpenHarmony (target `aarch64-unknown-linux-ohos`). This requires the [OpenHarmony SDK](https://www.openharmony.cn/) native toolchain. +GitUI can be built for OpenHarmony (target `aarch64-unknown-linux-ohos`). This requires the OpenHarmony SDK native toolchain. + +Download the SDK command-line tools from [HarmonyOS Developer](https://developer.huawei.com/consumer/cn/download/command-line-tools-for-hmos). After installation, the default SDK path is typically: + +- Linux: `~/command-line-tools/sdk/default/openharmony` +- macOS: `~/Library/command-line-tools/sdk/default/openharmony` First, install the `aarch64-unknown-linux-ohos` Rust target: From 621ff5b4bfc392eaa2dfeff83e9e14fb3dd15cad Mon Sep 17 00:00:00 2001 From: "YSTYLE-L.X.Y" Date: Thu, 21 May 2026 21:54:53 +0800 Subject: [PATCH 3/4] fix: use git2-based LogWalker on OHOS to avoid gix InsufficientSlots panic On OpenHarmony, gix-odb fails with InsufficientSlots { current: 32, needed: 4 } when loading pack indices during commit log walking. This happens because LogWalkerWithoutFilter uses gix which has a limited slot map for managing ODB index files. Fall back to git2-based LogWalker on OHOS targets, which can handle both filtered and unfiltered log walking without this issue. --- asyncgit/src/revlog.rs | 64 ++++++++++++++++++++++++++++-------------- 1 file changed, 43 insertions(+), 21 deletions(-) diff --git a/asyncgit/src/revlog.rs b/asyncgit/src/revlog.rs index 774d5140ef..93d15c8103 100644 --- a/asyncgit/src/revlog.rs +++ b/asyncgit/src/revlog.rs @@ -1,11 +1,12 @@ use crate::{ error::Result, sync::{ - gix_repo, repo, CommitId, LogWalker, LogWalkerWithoutFilter, - RepoPath, SharedCommitFilterFn, + repo, CommitId, LogWalker, RepoPath, SharedCommitFilterFn, }, AsyncGitNotification, Error, }; +#[cfg(not(target_env = "ohos"))] +use crate::sync::{gix_repo, LogWalkerWithoutFilter}; use crossbeam_channel::Sender; use scopetime::scope_time; use std::{ @@ -200,25 +201,45 @@ impl AsyncLog { sender: &Sender, filter: Option, ) -> Result<()> { - filter.map_or_else( - || { - Self::fetch_helper_without_filter( - repo_path, - arc_current, - arc_background, - sender, - ) - }, - |filter| { - Self::fetch_helper_with_filter( - repo_path, - arc_current, - arc_background, - sender, - filter, - ) - }, - ) + #[cfg(target_env = "ohos")] + { + Self::fetch_helper_with_filter( + repo_path, + arc_current, + arc_background, + sender, + filter.unwrap_or_else(|| { + Arc::new(Box::new( + |_: &git2::Repository, _: &CommitId| { + Ok(true) + }, + )) + }), + ) + } + + #[cfg(not(target_env = "ohos"))] + { + filter.map_or_else( + || { + Self::fetch_helper_without_filter( + repo_path, + arc_current, + arc_background, + sender, + ) + }, + |filter| { + Self::fetch_helper_with_filter( + repo_path, + arc_current, + arc_background, + sender, + filter, + ) + }, + ) + } } fn fetch_helper_with_filter( @@ -265,6 +286,7 @@ impl AsyncLog { Ok(()) } + #[cfg(not(target_env = "ohos"))] fn fetch_helper_without_filter( repo_path: &RepoPath, arc_current: &Arc>, From ad4281f4bdf2d3d1d104df7eb4f95b41a9175ca6 Mon Sep 17 00:00:00 2001 From: "YSTYLE-L.X.Y" Date: Sun, 31 May 2026 18:35:07 +0800 Subject: [PATCH 4/4] fix: use git2-based fallbacks for gix-only functions on OHOS On OpenHarmony, gix-odb's slot map for pack indices can overflow (InsufficientSlots), causing panics in status, tags, and commit info operations. This makes gitui work on the status/files, tags, and commit detail views. Affected functions (now use git2 on OHOS via cfg): - get_status() in status.rs - get_tags() in tags.rs - get_commit_info() in commits_info.rs gix_repo() and related gix imports are cfg-gated for OHOS to avoid dead code warnings. --- asyncgit/src/sync/commits_info.rs | 54 ++++++++----- asyncgit/src/sync/mod.rs | 4 +- asyncgit/src/sync/repository.rs | 1 + asyncgit/src/sync/status.rs | 122 +++++++++++++++++++++++++++++- asyncgit/src/sync/tags.rs | 47 +++++++++++- 5 files changed, 208 insertions(+), 20 deletions(-) diff --git a/asyncgit/src/sync/commits_info.rs b/asyncgit/src/sync/commits_info.rs index 89d847c8c2..295b0580b0 100644 --- a/asyncgit/src/sync/commits_info.rs +++ b/asyncgit/src/sync/commits_info.rs @@ -5,9 +5,11 @@ use crate::{ error::Result, sync::{ commit_details::get_author_of_commit, - repository::{gix_repo, repo}, + repository::repo, }, }; +#[cfg(not(target_env = "ohos"))] +use crate::sync::repository::gix_repo; use git2::{Commit, Error, Oid}; use scopetime::scope_time; use unicode_truncate::UnicodeTruncateStr; @@ -169,27 +171,44 @@ pub fn get_commit_info( ) -> Result { scope_time!("get_commit_info"); - let repo: gix::Repository = gix_repo(repo_path)?; - let mailmap = repo.open_mailmap(); + #[cfg(target_env = "ohos")] + { + let mut infos = get_commits_info( + repo_path, + &[*commit_id], + usize::MAX, + )?; + return infos.pop().ok_or_else(|| { + crate::error::Error::Generic( + "commit not found".into(), + ) + }); + } + + #[cfg(not(target_env = "ohos"))] + { + let repo: gix::Repository = gix_repo(repo_path)?; + let mailmap = repo.open_mailmap(); - let commit = repo.find_commit(*commit_id)?; - let commit_ref = commit.decode()?; + let commit = repo.find_commit(*commit_id)?; + let commit_ref = commit.decode()?; - let message = gix_get_message(&commit_ref, None); + let message = gix_get_message(&commit_ref, None); - let author = commit_ref.author()?; + let author = commit_ref.author()?; - let author = mailmap.try_resolve(author).map_or_else( - || author.name.into(), - |signature| signature.name, - ); + let author = mailmap.try_resolve(author).map_or_else( + || author.name.into(), + |signature| signature.name, + ); - Ok(CommitInfo { - message, - author: author.to_string(), - time: commit_ref.time()?.seconds, - id: commit.id().detach().into(), - }) + Ok(CommitInfo { + message, + author: author.to_string(), + time: commit_ref.time()?.seconds, + id: commit.id().detach().into(), + }) + } } /// if `message_limit` is set the message will be @@ -212,6 +231,7 @@ pub fn get_message( /// if `message_limit` is set the message will be /// limited to the first line and truncated to fit +#[cfg(not(target_env = "ohos"))] pub fn gix_get_message( commit_ref: &gix::objs::CommitRef, message_limit: Option, diff --git a/asyncgit/src/sync/mod.rs b/asyncgit/src/sync/mod.rs index 2a5f413e8f..a33306c3bf 100644 --- a/asyncgit/src/sync/mod.rs +++ b/asyncgit/src/sync/mod.rs @@ -85,7 +85,9 @@ pub use remotes::{ get_remote_url, get_remotes, push::AsyncProgress, rename_remote, tags::PushTagsProgress, update_remote_url, validate_remote_name, }; -pub(crate) use repository::{gix_repo, repo}; +#[cfg(not(target_env = "ohos"))] +pub(crate) use repository::gix_repo; +pub(crate) use repository::repo; pub use repository::{RepoPath, RepoPathRef}; pub use reset::{reset_repo, reset_stage, reset_workdir}; pub use reword::reword; diff --git a/asyncgit/src/sync/repository.rs b/asyncgit/src/sync/repository.rs index ecfbcdb55c..cd6403994b 100644 --- a/asyncgit/src/sync/repository.rs +++ b/asyncgit/src/sync/repository.rs @@ -87,6 +87,7 @@ pub fn repo(repo_path: &RepoPath) -> Result { Ok(repo) } +#[cfg(not(target_env = "ohos"))] pub fn gix_repo(repo_path: &RepoPath) -> Result { let mut repo: gix::Repository = gix::ThreadSafeRepository::discover_with_environment_overrides( repo_path.gitpath(), diff --git a/asyncgit/src/sync/status.rs b/asyncgit/src/sync/status.rs index 8afdf2dd09..9e042b046f 100644 --- a/asyncgit/src/sync/status.rs +++ b/asyncgit/src/sync/status.rs @@ -4,9 +4,11 @@ use crate::{ error::Result, sync::{ config::untracked_files_config_repo, - repository::{gix_repo, repo}, + repository::repo, }, }; +#[cfg(not(target_env = "ohos"))] +use crate::sync::repository::gix_repo; use git2::{Delta, Status, StatusOptions, StatusShow}; use scopetime::scope_time; use std::path::Path; @@ -64,6 +66,38 @@ impl From> for StatusItemType { } } +impl StatusItemType { + /// Convert from worktree-only status flags. + pub fn from_wt(s: Status) -> Self { + if s.is_wt_new() { + Self::New + } else if s.is_wt_deleted() { + Self::Deleted + } else if s.is_wt_renamed() { + Self::Renamed + } else if s.is_wt_typechange() { + Self::Typechange + } else { + Self::Modified + } + } + + /// Convert from index-only status flags. + pub fn from_index(s: Status) -> Self { + if s.is_index_new() { + Self::New + } else if s.is_index_deleted() { + Self::Deleted + } else if s.is_index_renamed() { + Self::Renamed + } else if s.is_index_typechange() { + Self::Typechange + } else { + Self::Modified + } + } +} + impl From for StatusItemType { fn from(s: Status) -> Self { if s.is_index_new() || s.is_wt_new() { @@ -168,6 +202,7 @@ impl From for gix::status::UntrackedFiles { } /// guarantees sorting +#[cfg(not(target_env = "ohos"))] pub fn get_status( repo_path: &RepoPath, status_type: StatusType, @@ -285,6 +320,91 @@ pub fn get_status( Ok(res) } +/// git2-based status for OpenHarmony targets. +/// On OHOS, gix-odb's slot map for pack indices can overflow +/// (InsufficientSlots), so we fall back to git2. +#[cfg(target_env = "ohos")] +pub fn get_status( + repo_path: &RepoPath, + status_type: StatusType, + show_untracked: Option, +) -> Result> { + scope_time!("get_status"); + + let repo = repo(repo_path)?; + + let show_untracked = if let Some(config) = show_untracked { + config + } else { + untracked_files_config_repo(&repo)? + }; + + let mut options = StatusOptions::default(); + options + .show(status_type.into()) + .update_index(true) + .include_untracked(show_untracked.include_untracked()) + .renames_head_to_index(true) + .recurse_untracked_dirs( + show_untracked.recurse_untracked_dirs(), + ); + + let statuses = repo.statuses(Some(&mut options))?; + + let mut res = Vec::with_capacity(statuses.len()); + + for entry in statuses.iter() { + let Some(path) = entry.path() else { + continue; + }; + + let status = entry.status(); + + if status_type == StatusType::Both { + if status.is_wt_new() + || status.is_wt_deleted() + || status.is_wt_modified() + || status.is_wt_renamed() + || status.is_wt_typechange() + { + res.push(StatusItem { + path: path.to_string(), + status: StatusItemType::from_wt(status), + }); + } + if status.is_index_new() + || status.is_index_deleted() + || status.is_index_modified() + || status.is_index_renamed() + || status.is_index_typechange() + { + res.push(StatusItem { + path: path.to_string(), + status: StatusItemType::from_index(status), + }); + } + if status.is_conflicted() { + res.push(StatusItem { + path: path.to_string(), + status: StatusItemType::Conflicted, + }); + } + } else { + let status: StatusItemType = status.into(); + res.push(StatusItem { + path: path.to_string(), + status, + }); + } + } + + res.sort_by(|a, b| { + Path::new(a.path.as_str()).cmp(Path::new(b.path.as_str())) + }); + + Ok(res) +} + /// discard all changes in the working directory pub fn discard_status(repo_path: &RepoPath) -> Result { let repo = repo(repo_path)?; diff --git a/asyncgit/src/sync/tags.rs b/asyncgit/src/sync/tags.rs index 7630b6dc90..48d955558a 100644 --- a/asyncgit/src/sync/tags.rs +++ b/asyncgit/src/sync/tags.rs @@ -1,8 +1,10 @@ use super::{get_commits_info, CommitId, RepoPath}; use crate::{ error::Result, - sync::{gix_repo, repository::repo}, + sync::repository::repo, }; +#[cfg(not(target_env = "ohos"))] +use crate::sync::gix_repo; use scopetime::scope_time; use std::collections::{BTreeMap, HashMap, HashSet}; @@ -49,6 +51,7 @@ pub struct TagWithMetadata { static MAX_MESSAGE_WIDTH: usize = 100; /// returns `Tags` type filled with all tags found in repo +#[cfg(not(target_env = "ohos"))] pub fn get_tags(repo_path: &RepoPath) -> Result { scope_time!("get_tags"); @@ -86,6 +89,48 @@ pub fn get_tags(repo_path: &RepoPath) -> Result { Ok(res) } +/// git2-based tag listing for OpenHarmony targets. +/// On OHOS, gix-odb's slot map for pack indices can overflow +/// (InsufficientSlots), so we fall back to git2. +#[cfg(target_env = "ohos")] +pub fn get_tags(repo_path: &RepoPath) -> Result { + scope_time!("get_tags"); + + let mut res = Tags::new(); + let mut adder = |key, value: Tag| { + if let Some(key) = res.get_mut(&key) { + key.push(value); + } else { + res.insert(key, vec![value]); + } + }; + + let repo = repo(repo_path)?; + let tag_names = repo.tag_names(None)?; + + for name in tag_names.iter().flatten() { + let reference = match repo.find_reference( + &format!("refs/tags/{name}"), + ) { + Ok(r) => r, + Err(_) => continue, + }; + + let target = reference.target(); + let tag = reference.peel_to_tag(); + + if let (Some(commit_id), Ok(tag)) = (target, tag) { + let name = tag.name().unwrap_or(name).to_string(); + let annotation = tag.message().map(|s| s.to_string()); + adder(commit_id.into(), Tag { name, annotation }); + } else if let Some(commit_id) = target { + adder(commit_id.into(), Tag::new(name)); + } + } + + Ok(res) +} + /// pub fn get_tags_with_metadata( repo_path: &RepoPath,