//! Git integration. Reads (status, diff, blame, log) use gitoxide in-process; //! writes (commit, checkout, push/pull, stash, merge) shell out to `git` so //! hooks, credentials and signing behave exactly as on the command line. use ide_core::ignored::IgnoredNames; use ide_core::{CancellationToken, IdeError, IdeResult}; use serde::{Deserialize, Serialize}; use specta::Type; use std::path::{Path, PathBuf}; pub use text_encoding::TextEncoding; #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Type)] #[serde(rename_all = "camelCase")] pub enum ChangeKind { Added, Modified, Deleted, Renamed, Untracked, Ignored, Conflicted, } #[derive(Clone, Debug, Serialize, Deserialize, Type)] #[serde(rename_all = "camelCase")] pub struct FileStatus { pub path: ide_core::IpcPath, pub rel_path: String, /// Staged change, if any. pub staged: Option, /// Unstaged (working tree) change, if any. pub unstaged: Option, pub renamed_from: Option, } #[derive(Clone, Debug, Serialize, Deserialize, Type)] #[serde(rename_all = "camelCase")] pub struct BranchInfo { /// `None` when HEAD is detached. pub head: Option, pub head_commit: Option, pub upstream: Option, pub ahead: u32, pub behind: u32, /// Merge / rebase / cherry-pick in progress. pub operation: Option, } #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Type)] #[serde(rename_all = "camelCase")] pub enum HunkKind { Added, Modified, Deleted, } /// Line-level change against HEAD for editor gutter markers (0-based lines). #[derive(Clone, Debug, Serialize, Deserialize, Type)] #[serde(rename_all = "camelCase")] pub struct LineHunk { pub kind: HunkKind, pub start: u32, /// Lines in the current text (0 for pure deletions). pub lines: u32, pub old_start: u32, pub old_lines: u32, /// Original text of the hunk, for the inline diff popup / rollback. pub old_text: String, } #[derive(Clone, Debug, Serialize, Deserialize, Type)] #[serde(rename_all = "camelCase")] pub struct BlameLine { pub commit: String, pub author: String, pub time_ms: f64, pub summary: String, } /// History for Selection: the commits that changed a block of lines, newest first. #[derive(Clone, Debug, Serialize, Deserialize, Type)] #[serde(rename_all = "camelCase")] pub struct BlockHistory { /// The selected lines in HEAD (0-based, inclusive); None when none of them is committed. pub head_lines: Option<(u32, u32)>, pub revisions: Vec, } /// A commit that changed the block, with the block in its first parent and in it. #[derive(Clone, Debug, Serialize, Deserialize, Type)] #[serde(rename_all = "camelCase")] pub struct BlockRevision { pub id: String, pub short_id: String, pub author: String, pub email: String, pub time_ms: f64, pub summary: String, /// Repository-relative path of the file in this commit (before a later rename). pub path: String, /// The block before the commit; empty when the commit added it. pub before: String, pub after: String, /// 0-based first line of the block before and after the commit. pub before_line: u32, pub after_line: u32, } #[derive(Clone, Debug, Serialize, Deserialize, Type)] #[serde(rename_all = "camelCase")] pub struct Commit { pub id: String, pub short_id: String, pub parents: Vec, /// Connections in the filtered graph; never use these as revision/diff parents. #[serde(default, skip_serializing_if = "Option::is_none")] pub graph_parents: Option>, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub graph_dashed: Vec, /// Repository-relative filename at this revision in rename-following history. #[serde(default, skip_serializing_if = "Option::is_none")] pub history_path: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub log_position: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub log_total: Option, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub graph_boundary_parents: Vec, /// Reachability from HEAD, computed independently of the visible filters. #[serde(default, skip_serializing_if = "Option::is_none")] pub on_current_branch: Option, pub author: String, pub email: String, #[serde(default)] pub committer: String, #[serde(default)] pub committer_email: String, pub time_ms: f64, pub summary: String, pub message: String, #[serde(default)] pub message_truncated: bool, pub refs: Vec, /// Committer time (Log › View Options › Commit Timestamp). #[serde(default)] pub commit_time_ms: f64, /// Legacy alias of `graph_dashed`, indexed by displayed parents. #[serde(default)] #[specta(optional)] pub dashed: Vec, } #[derive(Clone, Debug, Serialize, Deserialize, Type)] #[serde(rename_all = "camelCase")] pub struct LogDetails { pub message: String, pub containing_branches: Vec, /// Git's verification code: G/U good, B bad, E error, N unsigned, X/Y expired, R revoked. pub signature: String, pub signer: String, pub key: String, pub fingerprint: String, } #[derive(Clone, Debug, Default, Serialize, Deserialize, Type)] #[serde(rename_all = "camelCase")] pub struct LogIndexProgress { pub indexed: u32, pub total: u32, pub running: bool, } /// Order of the Git Log (Graph Options › Sort). #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, Type)] #[serde(rename_all = "camelCase")] pub enum LogOrder { /// `--topo-order`: lines of history are not intermixed. #[default] Topo, /// `--date-order`: commit time, but never a parent before its children. Date, // Topological grouping with incoming merge parents visited before the main line. Standard, } #[derive(Clone, Debug, Default, Serialize, Deserialize, Type)] #[serde(rename_all = "camelCase")] pub struct LogQuery { #[serde(default)] pub speed_search: Option, #[serde(default)] pub search_fields: Vec, /// Jump directly to an indexed window containing this commit (50 rows of context). #[serde(default)] pub seek: Option, #[serde(default)] pub retain_until: Option, pub branch: Option, pub path: Option, #[serde(default)] pub paths: Vec, /// File history follows renames; structure filters retain fixed paths. #[serde(default)] pub follow: bool, pub author: Option, pub text: Option, pub skip: u32, pub limit: u32, /// A revision range such as `@{u}..HEAD` (commits reachable from the right side but /// not the left). Takes precedence over `branch`. #[serde(default)] pub range: Option, /// Additional ranges unioned with each other and with selected branches. #[serde(default)] pub ranges: Vec, /// `text` is a regular expression (Log filter `.*`). #[serde(default)] pub text_regex: bool, /// `text` matches case-sensitively (Log filter `Cc`). #[serde(default)] pub text_match_case: bool, /// More starting revisions next to `branch` (Log Branch filter with several branches). #[serde(default)] pub branches: Vec, /// More author needles next to `author`; a commit matches any of them (Log User filter). #[serde(default)] pub authors: Vec, #[serde(default)] pub authors_exact: bool, #[serde(default)] pub order: LogOrder, /// Follow only the first parent of merges (`--first-parent`). #[serde(default)] pub first_parent: bool, /// Leave out merge commits (`--no-merges`); their children link to their parents. #[serde(default)] pub no_merges: bool, /// Committer time bounds (epoch ms; `since` inclusive, `until` exclusive). Parents /// outside the range are rewritten to their nearest shown ancestors (or dropped when /// older), so the graph stays connected. #[serde(default)] pub since_ms: Option, #[serde(default)] pub until_ms: Option, #[serde(default)] pub request: Option, #[serde(default)] pub summary_only: bool, /// Inclusive refresh boundary. Clients join only verified linear prefixes. #[serde(default)] pub stop_at: Option, } #[derive(Clone, Debug, Serialize, Deserialize, Type)] #[serde(rename_all = "camelCase")] pub struct Branch { pub name: String, pub remote: bool, pub current: bool, pub upstream: Option, pub ahead: u32, pub behind: u32, pub last_commit_ms: f64, /// Tip commit id. #[serde(default)] pub commit: String, } /// Commit Options (IDEA's commit options popup). #[derive(Clone, Debug, Default, Serialize, Deserialize, Type)] #[serde(rename_all = "camelCase")] pub struct CommitOptions { pub amend: bool, pub sign_off: bool, /// `--author`, e.g. `Name `; `None` or blank uses the configured user. pub author: Option, /// False skips the pre-commit and commit-msg hooks (`--no-verify`). pub run_hooks: bool, /// Files committed with only some of their changes (IDEA's partial commit). They are /// committed through a temporary index, so neither the working tree nor the other /// entries of the index change; afterwards their index entries match the new commit. #[serde(default)] pub partial: Vec, } /// A file of a partial commit: which of its changes against HEAD to commit. #[derive(Clone, Debug, Serialize, Deserialize, Type)] #[serde(rename_all = "camelCase")] pub struct PartialCommitFile { pub path: ide_core::IpcPath, /// The hunks to commit, as [`diff_hunks`] of the HEAD and on-disk texts reports them. pub hunks: Vec, } /// A hunk identified by its line ranges (0-based), see [`LineHunk`]. #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Type)] #[serde(rename_all = "camelCase")] pub struct HunkRange { pub start: u32, pub lines: u32, pub old_start: u32, pub old_lines: u32, } #[derive(Clone, Debug, Serialize, Deserialize, Type)] #[serde(rename_all = "camelCase")] pub struct ChangedFile { pub path: String, pub kind: ChangeKind, pub old_path: Option, } /// A file changed across several commits: its content at `before` ↔ at `after`. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Type)] #[serde(rename_all = "camelCase")] pub struct RevisionChange { /// Repository-relative path after the change (before it, for a deletion). pub path: String, pub kind: ChangeKind, /// Path at `before`, for a rename. pub old_path: Option, /// Commit holding the old content; `None` when the file was added. pub before: Option, /// Commit holding the new content; `None` when the file was deleted. pub after: Option, } #[derive(Clone, Debug, Serialize, Deserialize, Type)] #[serde(rename_all = "camelCase")] pub struct Stash { pub index: u32, pub message: String, pub time_ms: f64, } /// Push dialog › Push tags: every tag, or only annotated tags on the pushed commits. #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Type)] #[serde(rename_all = "camelCase")] pub enum PushTags { All, CurrentBranch, } /// Options of [`Repo::push_branch_to`]: which commits to push and where to. #[derive(Clone, Copy, Debug, Default)] pub struct PushBranchOptions<'a> { /// Push only up to this commit of the branch (`None`: the whole branch). pub revision: Option<&'a str>, /// Remote branch name (`None` keeps the upstream's / the local name). pub target: Option<&'a str>, /// Remote to push to (`None` keeps the upstream's / the default remote). pub remote: Option<&'a str>, /// `git push --force-with-lease`. pub force_with_lease: bool, } /// What an interactive rebase does with one commit. #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Type)] #[serde(rename_all = "camelCase")] pub enum RebaseAction { Pick, /// Pick with a new message ([`RebaseEntry::message`]). Reword, /// Stop after applying, to amend it (Stop to Edit). Edit, /// Meld into the previous commit, combining the messages. Squash, /// Meld into the previous commit, keeping the previous message. Fixup, Drop, } /// One line of an interactive rebase plan. #[derive(Clone, Debug, Serialize, Deserialize, Type)] #[serde(rename_all = "camelCase")] pub struct RebaseEntry { pub commit: String, pub action: RebaseAction, /// New message for a reword, or for the result of a squash group (set on its last line). #[serde(default)] pub message: Option, } /// `git reset` modes offered by Reset Current Branch to Here. #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Type)] #[serde(rename_all = "camelCase")] pub enum ResetMode { Soft, Mixed, Hard, Keep, } /// A change list on the shelf. #[derive(Clone, Debug, Serialize, Deserialize, Type)] #[serde(rename_all = "camelCase")] pub struct ShelvedChangeList { /// Folder name under the shelf directory. pub id: String, pub name: String, pub time_ms: f64, /// Branch checked out when the changes were shelved. pub branch: Option, /// Repository owning the patch; absent in legacy shelves. #[serde(default)] pub repo: Option, /// Repository-relative paths. pub files: Vec, } /// One file of a patch (Apply Patch preview). #[derive(Clone, Debug, Serialize, Deserialize, Type)] #[serde(rename_all = "camelCase")] pub struct PatchFile { /// Path relative to the repository root (after the change). pub path: String, pub old_path: Option, pub kind: ChangeKind, pub added: u32, pub removed: u32, pub binary: bool, /// Applies cleanly to the working tree (otherwise a 3-way merge is attempted). pub applies: bool, } #[derive(Clone, Debug, Default, Serialize, Deserialize, Type)] #[serde(rename_all = "camelCase")] pub struct ApplyOutcome { /// Repository-relative paths changed without conflicts. pub applied: Vec, /// Repository-relative paths left with conflicts (index stages for the merge UI). pub conflicts: Vec, } /// How Checkout treats local changes that the switch would overwrite (IDEA's Smart /// Checkout dialog). #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, Type)] #[serde(rename_all = "camelCase")] pub enum CheckoutMode { /// Fail with [`CheckoutOutcome::LocalChanges`] and change nothing. #[default] Normal, /// Stash local changes, switch, and restore them (conflicts are left to resolve). Smart, /// Discard the local changes in the way (`git switch --discard-changes`). Force, } /// Checking out remote `origin/x` while local `x` has commits `origin/x` lacks. #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Type)] #[serde(rename_all = "camelCase")] pub enum DivergedLocal { /// Rebase local `x` onto `origin/x` (IDEA "Rebase onto Remote"). Rebase, /// Reset local `x` to `origin/x` (IDEA "Drop Local Commits"). Reset, } #[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, Type)] #[serde(rename_all = "camelCase")] pub struct CheckoutOptions { #[serde(default)] pub mode: CheckoutMode, /// Required when checking out a remote branch whose local namesake is ahead of it. #[serde(default)] pub diverged: Option, } /// Result of [`Repo::checkout_with`]; the non-`Done` variants other than `Conflicts` /// left the repository untouched. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Type)] #[serde(tag = "kind", rename_all = "camelCase")] pub enum CheckoutOutcome { Done, /// These local changes (repository-relative paths) would be overwritten. LocalChanges { files: Vec, }, /// Local branch `local` has commits that remote branch `remote` does not. LocalBranchAhead { local: String, remote: String, }, /// Switched, but restoring the local changes (Smart Checkout) or the rebase stopped /// on conflicts in these files. Conflicts { files: Vec, }, } /// A deleted local branch, enough to restore it (IDEA's "Restore" notification action). #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Type)] #[serde(rename_all = "camelCase")] pub struct DeletedBranch { pub name: String, /// The commit the branch pointed at. pub tip: String, /// Its upstream (`origin/x`), restored with it. pub upstream: Option, /// It was not fully merged: its own commits are now unreachable from any branch. pub unmerged: bool, } /// Who stands on each side of the conflicts of the operation in progress (merge editor /// titles, as IDEA labels them). #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, Type)] #[serde(rename_all = "camelCase")] pub struct MergeSides { /// `merge`, `rebase`, `cherry-pick`, `revert`…; `None` outside an operation. pub operation: Option, /// Git's "ours" is not the user's work: during a rebase stage 2 holds the commits /// rebased onto and stage 3 the user's commit being replayed. [`MergeVersions`] is /// already swapped; conflict markers in the file are not. pub reversed: bool, /// The current branch (for a rebase: the branch being rebased). pub branch: Option, /// Short id of the commit being applied (REBASE_HEAD, CHERRY_PICK_HEAD, REVERT_HEAD) /// or merged (MERGE_HEAD). pub commit: Option, /// The other branch: the one being merged, or the one rebased onto. pub other_branch: Option, } /// The versions of a conflicted file as the merge editor shows them. #[derive(Clone, Debug, Serialize, Deserialize, Type)] #[serde(rename_all = "camelCase")] pub struct MergeVersions { pub base: Option, /// The user's side ("Accept Yours"): git's stage 2, or stage 3 when `sides.reversed`. pub ours: Option, pub theirs: Option, pub sides: MergeSides, /// Three-way merge of the versions above: non-conflicting changes merged, each /// conflict left as a diff3 block (yours / base / theirs). `None` when a side is /// missing (deleted on one side). pub merged: Option, } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)] #[serde(rename_all = "camelCase")] pub struct Changelist { pub id: String, pub name: String, #[serde(default)] pub comment: String, } /// User change lists of a project and which list each changed file belongs to. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)] #[serde(rename_all = "camelCase")] pub struct ChangelistState { pub lists: Vec, /// Id of the active list (receives new changes). pub active: String, /// Project-relative path → list id. Files not listed belong to the active list. #[serde(default)] pub files: std::collections::BTreeMap, } mod bek; mod blame; mod block_history; pub mod changelists; mod cli; mod compare; mod diff; mod history; mod ignore; mod index_writes; mod log_index; mod manage; mod ops; mod patch; mod refs; mod remotes; mod repo; mod roots; pub use compare::{CompareResult, CompareScope, RepositoryComparison, WorkingTreeChange}; pub mod shelf; mod status; pub use index_writes::is_own_index_write; mod text_encoding; mod write; pub use cli::{ GitCommandObserver, detected_git_executable, git_version, set_askpass_env, set_git_command_observer, set_git_executable, }; pub use diff::build_partial_patch; pub use ignore::{IgnoreDestination, add_to_ignore}; pub use manage::{MergeOptions, RebaseOptions, RemoteEdit, Worktree}; pub use remotes::Remote; pub use roots::Repos; fn clone_args(url: &str, dest: &str, depth: Option) -> IdeResult> { if depth == Some(0) { return Err(IdeError::invalid("clone depth must be greater than zero")); } let mut args = vec!["-c".into(), "core.hooksPath=".into(), "clone".into(), "--progress".into()]; if let Some(depth) = depth { args.extend(["--depth".into(), depth.to_string()]); } args.extend(["--".into(), url.into(), dest.into()]); Ok(args) } /// Clones `url` into `dest` (which must not exist or be empty), streaming progress. /// `depth` creates a shallow clone containing at most that many commits. pub fn clone( url: &str, dest: &Path, depth: Option, cancel: &CancellationToken, on_output: &dyn Fn(String), ) -> IdeResult<()> { let parent = dest.parent().filter(|p| !p.as_os_str().is_empty()).unwrap_or(Path::new(".")); std::fs::create_dir_all(parent)?; let dest = if dest.is_absolute() { dest.to_path_buf() } else { std::env::current_dir()?.join(dest) }; let dest = dest.to_string_lossy(); // Cloning must not inherit an enclosing project's executable Git settings. let cwd = tempfile::tempdir()?; let args = clone_args(url, &dest, depth)?; let args: Vec<&str> = args.iter().map(String::as_str).collect(); cli::run_streaming(cwd.path(), &args, cancel, on_output) } #[cfg(test)] mod clone_tests { use super::clone_args; #[test] fn builds_full_and_shallow_clone_commands() { assert_eq!( clone_args("https://example.test/repo.git", "/tmp/repo", None).unwrap(), ["-c", "core.hooksPath=", "clone", "--progress", "--", "https://example.test/repo.git", "/tmp/repo"] ); assert_eq!( clone_args("https://example.test/repo.git", "/tmp/repo", Some(3)).unwrap(), [ "-c", "core.hooksPath=", "clone", "--progress", "--depth", "3", "--", "https://example.test/repo.git", "/tmp/repo", ] ); assert!(clone_args("https://example.test/repo.git", "/tmp/repo", Some(0)).is_err()); } } /// Creates an empty repository in `dir` (`git init`). pub fn init(dir: &Path) -> IdeResult<()> { cli::run(dir, &["init", "-q"], None).map(|_| ()) } /// Line hunks of `new_text` against `old_text` (CRLF-normalised, Histogram diff), in the /// order [`build_partial_patch`] indexes them. pub fn diff_hunks(old_text: &str, new_text: &str) -> Vec { diff::line_hunks(old_text, new_text) } /// An open git repository (worktree). Cheap to keep around; all reads re-check the /// on-disk state, so it never needs to be reopened after external changes. pub struct Repo { execution_allowed: std::sync::atomic::AtomicBool, root: PathBuf, git_dir: PathBuf, inner: std::sync::Mutex, ignored: std::sync::RwLock, log_index: std::sync::Mutex, index_progress: std::sync::Mutex, log_signatures: std::sync::Mutex>, } impl Repo { /// Discovers the repository containing `path`; `Ok(None)` when not under git. pub fn discover(path: &Path) -> IdeResult> { repo::discover(path) } /// The worktree root. pub fn root(&self) -> &Path { let _scope = crate::ExecutionScope::enter(self.execution_allowed.load(std::sync::atomic::Ordering::Acquire)); &self.root } /// Untracked files with one of these names (or under such a folder) are not reported. pub fn set_ignored_names(&self, names: IgnoredNames) { let _scope = crate::ExecutionScope::enter(self.execution_allowed.load(std::sync::atomic::Ordering::Acquire)); *self.ignored.write().unwrap_or_else(|p| p.into_inner()) = names; } /// Staged (HEAD↔index) and unstaged (index↔worktree, incl. untracked) changes, /// sorted by path. Conflicted files report `unstaged: Conflicted` and no staged kind. pub fn status(&self) -> IdeResult> { let _scope = crate::ExecutionScope::enter(self.execution_allowed.load(std::sync::atomic::Ordering::Acquire)); status::status(self) } pub fn branch_info(&self) -> IdeResult { let _scope = crate::ExecutionScope::enter(self.execution_allowed.load(std::sync::atomic::Ordering::Acquire)); refs::branch_info(self) } /// Current branch (short name; `None` when detached) and HEAD commit, without /// upstream tracking (cheap enough to call for every repository on each refresh). pub fn head(&self) -> IdeResult<(Option, Option)> { let _scope = crate::ExecutionScope::enter(self.execution_allowed.load(std::sync::atomic::Ordering::Acquire)); refs::head(&self.gix()?) } /// Merge / rebase / cherry-pick… in progress (cheap: no ref walking). pub fn operation(&self) -> IdeResult> { let _scope = crate::ExecutionScope::enter(self.execution_allowed.load(std::sync::atomic::Ordering::Acquire)); Ok(refs::operation(&self.gix()?)) } /// Content of `path` at `rev` (default HEAD); `None` if absent there. /// `rev` may be any revision git understands, or `":"` / `":0"` for the index. pub fn file_at( &self, path: &Path, rev: Option<&str>, encoding: impl Into, ) -> IdeResult> { let encoding = encoding.into(); let _scope = crate::ExecutionScope::enter(self.execution_allowed.load(std::sync::atomic::Ordering::Acquire)); let rel = self.rel_path(path)?; let repo = self.gix()?; Ok(repo::blob_at(&repo, &rel, rev.unwrap_or("HEAD"))?.map(|b| text_encoding::decode(&b, encoding).into_owned())) } /// Raw revision bytes, for encoding detection when the working file is absent. pub fn file_bytes_at(&self, path: &Path, rev: Option<&str>) -> IdeResult>> { let _scope = crate::ExecutionScope::enter(self.execution_allowed.load(std::sync::atomic::Ordering::Acquire)); repo::blob_at(&self.gix()?, &self.rel_path(path)?, rev.unwrap_or("HEAD")) } /// Stage selected decoded hunks, retaining the index encoding, BOM and line endings. pub fn stage_hunks( &self, path: &Path, current: &str, selected: &[usize], encoding: impl Into, ) -> IdeResult<()> { let encoding = encoding.into(); let _scope = crate::ExecutionScope::enter(self.execution_allowed.load(std::sync::atomic::Ordering::Acquire)); write::stage_hunks(self, path, current, selected, encoding) } /// Hunks of `current_text` against HEAD (or the index when `against_index`). /// /// Deletions have `lines == 0` and `start` = the line of the current text that now /// follows the removed block (equal to the line count when removed at the end). /// Files absent from the base yield one `Added` hunk; ignored files, binary files and /// paths outside the worktree yield no hunks. pub fn line_hunks( &self, path: &Path, current_text: &str, against_index: bool, encoding: impl Into, ) -> IdeResult> { let encoding = encoding.into(); let _scope = crate::ExecutionScope::enter(self.execution_allowed.load(std::sync::atomic::Ordering::Acquire)); let Ok(rel) = self.rel_path(path) else { return Ok(Vec::new()) }; if rel.is_empty() || rel == ".git" || rel.starts_with(".git/") { return Ok(Vec::new()); } let repo = self.gix()?; let base = repo::blob_at(&repo, &rel, if against_index { ":0" } else { "HEAD" })?; match base { Some(base) if text_encoding::is_binary(&base, encoding) => Ok(Vec::new()), Some(base) => Ok(diff::line_hunks(&text_encoding::decode(&base, encoding), current_text)), None => { if repo::is_ignored(&repo, &rel)? { return Ok(Vec::new()); } Ok(diff::all_added(current_text)) } } } /// One entry per line of the current text (`current_text`, or the file on disk). /// Lines not yet committed carry an all-zero commit id and author "Not Committed Yet". /// `revision` is the history to blame against (default `HEAD`); the merge view /// blames each side pane on the branch it belongs to (e.g. `MERGE_HEAD`). pub fn blame( &self, path: &Path, current_text: Option<&str>, revision: Option<&str>, encoding: impl Into, ) -> IdeResult> { let encoding = encoding.into(); let _scope = crate::ExecutionScope::enter(self.execution_allowed.load(std::sync::atomic::Ordering::Acquire)); write::blame(self, path, current_text, revision, encoding) } /// History for Selection: commits that changed `lines` (0-based, inclusive, of the /// current text: `current_text`, or the file on disk), mapped to HEAD's lines first. pub fn block_history( &self, path: &Path, current_text: Option<&str>, lines: (u32, u32), encoding: impl Into, cancel: &CancellationToken, ) -> IdeResult { let encoding = encoding.into(); let _scope = crate::ExecutionScope::enter(self.execution_allowed.load(std::sync::atomic::Ordering::Acquire)); block_history::block_history(self, path, current_text, lines, encoding, cancel) } /// Commits newest-first in topological order (children before parents). /// /// `branch` is any revision; when absent, HEAD plus all local and remote branches are /// walked. `path` limits to commits touching the path (file or directory), `author` /// matches name or email and `text` the message (both case-insensitive substrings). /// `skip`/`limit` paginate over the filtered result; `limit == 0` means no limit. /// `refs` decorations are `HEAD`, branch names (`main`, `origin/main`) and `tag: `. pub fn log(&self, query: &LogQuery) -> IdeResult> { let _scope = crate::ExecutionScope::enter(self.execution_allowed.load(std::sync::atomic::Ordering::Acquire)); history::log(self, query, &CancellationToken::new()) } pub fn log_cancellable(&self, query: &LogQuery, cancel: &CancellationToken) -> IdeResult> { let _scope = crate::ExecutionScope::enter(self.execution_allowed.load(std::sync::atomic::Ordering::Acquire)); history::log(self, query, cancel) } /// Enables the disposable, content-addressed log cache outside the worktree. pub fn enable_log_index(&self, cache_dir: &Path) { let key = blake3::hash(self.git_dir.to_string_lossy().as_bytes()).to_hex().to_string(); self.log_index.lock().unwrap_or_else(|p| p.into_inner()).open(&cache_dir.join(key)); } /// Reference names and peeled commit ids, including tags and detached HEAD. pub fn log_refs(&self) -> IdeResult> { let repo = self.gix()?; Ok(history::decorations(&repo)? .into_iter() .flat_map(|(id, names)| names.into_iter().map(move |name| (name, id.to_string()))) .collect()) } pub fn log_users(&self) -> IdeResult> { let _scope = crate::ExecutionScope::enter(self.execution_allowed.load(std::sync::atomic::Ordering::Acquire)); let out = cli::run(self.root(), &["log", "--all", "--format=%an <%ae>"], None)?; Ok(out .stdout_str() .lines() .filter(|line| !line.is_empty()) .map(str::to_string) .collect::>() .into_iter() .collect()) } pub fn log_index_progress(&self) -> LogIndexProgress { self.index_progress.lock().unwrap_or_else(|p| p.into_inner()).clone() } /// Incremental persistent inverted index; completed objects survive cancellation/restart. pub fn index_log(&self, cancel: &CancellationToken) -> IdeResult { { let mut progress = self.index_progress.lock().unwrap_or_else(|p| p.into_inner()); if progress.running { return Ok(progress.clone()); } progress.running = true; } let result = (|| { let repo = self.gix()?; let out = cli::run_cancellable(self.root(), &["rev-list", "--all", "--topo-order"], cancel)?; let ids: Vec<_> = out .stdout_str() .lines() .map(|id| gix::ObjectId::from_hex(id.as_bytes()).map_err(repo::gx)) .collect::>()?; { let mut p = self.index_progress.lock().unwrap_or_else(|p| p.into_inner()); p.total = ids.len() as u32; p.indexed = 0; } for (i, id) in ids.iter().enumerate() { if cancel.is_cancelled() { return Err(ide_core::IdeError::cancelled()); } let indexed = self.log_index.lock().unwrap_or_else(|p| p.into_inner()).indexed(&id.to_string()); if !indexed { let files = self.commit_files(&id.to_string())?; let paths = files.into_iter().flat_map(|f| std::iter::once(f.path).chain(f.old_path)).collect::>(); let mut index = self.log_index.lock().unwrap_or_else(|p| p.into_inner()); let c = index.get(&repo, *id)?; let message = index.message(&repo, *id)?; index.index_commit(&c.id, &message, &c.author, &c.email, &paths); if i % 100 == 0 { index.flush(); } } self.index_progress.lock().unwrap_or_else(|p| p.into_inner()).indexed = i as u32 + 1; } Ok(()) })(); self.log_index.lock().unwrap_or_else(|p| p.into_inner()).flush(); self.index_progress.lock().unwrap_or_else(|p| p.into_inner()).running = false; result?; Ok(self.log_index_progress()) } pub fn log_details(&self, revision: &str) -> IdeResult { self.log_details_cancellable(revision, &CancellationToken::new()) } pub fn log_details_cancellable(&self, revision: &str, cancel: &CancellationToken) -> IdeResult { let allowed = self.execution_allowed.load(std::sync::atomic::Ordering::Acquire); let _scope = crate::ExecutionScope::enter(allowed); let repo = self.gix()?; let object = repo.find_object(repo::resolve(&repo, revision)?).map_err(repo::gx)?.peel_to_commit().map_err(repo::gx)?; let commit_id = object.id; let id = commit_id.to_string(); let contains = format!("--contains={id}"); let branches = cli::run_cancellable(self.root(), &["branch", "-a", &contains, "--format=%(refname:short)"], cancel)?; let cache_key = format!("{allowed}:{id}"); let cached = self.log_signatures.lock().unwrap_or_else(|p| p.into_inner()).get(&cache_key).cloned(); let mut details = if let Some(details) = cached { details } else { let end = object.data.windows(2).position(|s| s == b"\n\n").unwrap_or(object.data.len()); let headers = &object.data[..end]; let signed = headers.windows(8).any(|s| s == b"\ngpgsig ") || headers.windows(8).any(|s| s == b"\ngpgsig-"); let output = if allowed && signed { Some(cli::run_cancellable( self.root(), &["log", "-1", "--format=%G?%x00%GS%x00%GK%x00%GF", "--end-of-options", &id], cancel, )?) } else { None }; let text = output.as_ref().map(|out| out.stdout_str()).unwrap_or_default(); let fields: Vec<_> = text.trim_end().split('\0').collect(); let message = { let mut index = self.log_index.lock().unwrap_or_else(|p| p.into_inner()); let message = index.message(&repo, commit_id)?; index.flush(); message }; let details = LogDetails { message, containing_branches: Vec::new(), signature: if !signed { "N".into() } else { fields.first().filter(|s| !s.is_empty()).unwrap_or(&"E").to_string() }, signer: fields.get(1).unwrap_or(&"").to_string(), key: fields.get(2).unwrap_or(&"").to_string(), fingerprint: fields.get(3).unwrap_or(&"").to_string(), }; self.log_signatures.lock().unwrap_or_else(|p| p.into_inner()).insert(cache_key, details.clone()); details }; details.containing_branches = branches.stdout_str().lines().filter(|s| !s.ends_with("/HEAD")).map(str::to_string).collect(); Ok(details) } /// Lists one immutable revision directory. Directory names end in `/`. pub fn revision_children(&self, revision: &str, directory: &str) -> IdeResult> { let repo = self.gix()?; let id = repo::resolve(&repo, revision)?.to_string(); let directory = self.rel_path(std::path::Path::new(directory))?; let tree = if directory.is_empty() { id } else { format!("{id}:{directory}") }; let out = cli::run(self.root(), &["ls-tree", "-z", "--", &tree], None)?; Ok(out .stdout .split(|b| *b == 0) .filter_map(|line| { let split = line.iter().position(|b| *b == b'\t')?; let name = String::from_utf8_lossy(&line[split + 1..]); let dir = line.starts_with(b"040000 "); Some(format!( "{}{name}{}", if directory.is_empty() { String::new() } else { format!("{directory}/") }, if dir { "/" } else { "" } )) }) .collect()) } pub fn revision_files(&self, revision: &str) -> IdeResult> { let _scope = crate::ExecutionScope::enter(self.execution_allowed.load(std::sync::atomic::Ordering::Acquire)); let repo = self.gix()?; let id = repo::resolve(&repo, revision)?.to_string(); let out = cli::run(self.root(), &["ls-tree", "-r", "-z", "--name-only", "--", &id], None)?; Ok(out .stdout .split(|b| *b == 0) .filter(|p| !p.is_empty()) .map(|p| String::from_utf8_lossy(p).into_owned()) .collect()) } /// Files changed by `commit` relative to its first parent (renames detected). pub fn commit_files(&self, commit: &str) -> IdeResult> { let _scope = crate::ExecutionScope::enter(self.execution_allowed.load(std::sync::atomic::Ordering::Acquire)); history::commit_files(self, commit) } /// Combined changes of several commits (oldest first): per file, before the first commit /// that touched it ↔ after the last one (IDEA's multi-commit Git Log selection). pub fn commits_changes(&self, commits: &[String]) -> IdeResult> { let _scope = crate::ExecutionScope::enter(self.execution_allowed.load(std::sync::atomic::Ordering::Acquire)); history::commits_changes(self, commits) } /// Local and remote branches; the current branch first, then most recent first. pub fn branches(&self) -> IdeResult> { let _scope = crate::ExecutionScope::enter(self.execution_allowed.load(std::sync::atomic::Ordering::Acquire)); refs::branches(self) } /// Stash entries, newest (`stash@{0}`) first. pub fn stashes(&self) -> IdeResult> { let _scope = crate::ExecutionScope::enter(self.execution_allowed.load(std::sync::atomic::Ordering::Acquire)); refs::stashes(self) } /// Unified diff of a file: index↔worktree (`staged == false`, untracked files diff /// against /dev/null) or HEAD↔index (`staged == true`). Empty when unchanged. pub fn diff_text(&self, path: &Path, staged: bool, encoding: impl Into) -> IdeResult { let encoding = encoding.into(); let _scope = crate::ExecutionScope::enter(self.execution_allowed.load(std::sync::atomic::Ordering::Acquire)); write::diff_text(self, path, staged, encoding) } /// `git config --get ` (all scopes); `None` when unset. pub fn config_get(&self, key: &str) -> IdeResult> { let _scope = crate::ExecutionScope::enter(self.execution_allowed.load(std::sync::atomic::Ordering::Acquire)); let out = cli::run_raw(&self.root, &["config", "--get", key], None)?; Ok((out.code == Some(0)).then(|| out.stdout_str().trim().to_string())) } // ---- writes (git CLI) ---- pub fn stage(&self, paths: &[PathBuf]) -> IdeResult<()> { let _scope = crate::ExecutionScope::enter(self.execution_allowed.load(std::sync::atomic::Ordering::Acquire)); write::stage(self, paths) } pub fn unstage(&self, paths: &[PathBuf]) -> IdeResult<()> { let _scope = crate::ExecutionScope::enter(self.execution_allowed.load(std::sync::atomic::Ordering::Acquire)); write::unstage(self, paths) } /// Applies a unified-diff patch to the index (partial staging of hunks/lines). /// Zero-context patches (see [`build_partial_patch`]) are accepted. pub fn stage_patch>( &self, patch: &str, encoding: &dyn Fn(&Path) -> IdeResult, ) -> IdeResult<()> { let _scope = crate::ExecutionScope::enter(self.execution_allowed.load(std::sync::atomic::Ordering::Acquire)); write::stage_patch(self, patch, &|path| encoding(path).map(Into::into)) } /// Discards unstaged changes: tracked paths are restored from the index (staged /// changes are kept), untracked files and directories are deleted. pub fn discard(&self, paths: &[PathBuf]) -> IdeResult<()> { let _scope = crate::ExecutionScope::enter(self.execution_allowed.load(std::sync::atomic::Ordering::Acquire)); write::discard(self, paths) } /// Commits the index; returns the new HEAD commit id. An empty message with /// `amend` keeps the previous message. pub fn commit(&self, message: &str, amend: bool, sign_off: bool) -> IdeResult { let _scope = crate::ExecutionScope::enter(self.execution_allowed.load(std::sync::atomic::Ordering::Acquire)); let options = CommitOptions { amend, sign_off, author: None, run_hooks: true, partial: Vec::new() }; write::commit(self, message, &options, &|_| Ok(encoding_rs::UTF_8.into())) } /// Commits with the Commit Options popup's choices. pub fn commit_with>( &self, message: &str, options: &CommitOptions, encoding: &dyn Fn(&Path) -> IdeResult, ) -> IdeResult { let _scope = crate::ExecutionScope::enter(self.execution_allowed.load(std::sync::atomic::Ordering::Acquire)); write::commit(self, message, options, &|path| encoding(path).map(Into::into)) } /// Switches to `branch`. With `create`, creates it at HEAD. Without, a remote branch /// (`origin/x`) gets a local tracking branch and any other revision is checked out /// detached. pub fn checkout(&self, branch: &str, create: bool) -> IdeResult<()> { let _scope = crate::ExecutionScope::enter(self.execution_allowed.load(std::sync::atomic::Ordering::Acquire)); write::checkout(self, branch, create) } /// [`Repo::checkout`] as IDEA does it: local changes in the way are reported (or /// stashed / discarded per `options.mode`), and a remote branch whose local namesake /// is behind fast-forwards that branch, while one that is ahead is reported (or /// rebased / reset per `options.diverged`). pub fn checkout_with(&self, branch: &str, create: bool, options: &CheckoutOptions) -> IdeResult { let _scope = crate::ExecutionScope::enter(self.execution_allowed.load(std::sync::atomic::Ordering::Acquire)); write::checkout_with(self, branch, create, options) } /// Creates branch `name` at `start` (default HEAD), optionally switching to it. A /// remote-tracking start point (`origin/x`) becomes the new branch's upstream. pub fn create_branch(&self, name: &str, start: Option<&str>, checkout: bool) -> IdeResult<()> { let _scope = crate::ExecutionScope::enter(self.execution_allowed.load(std::sync::atomic::Ordering::Acquire)); write::create_branch(self, name, start, checkout) } /// Undo Commit: resets the current branch to the parent of `commit` (which must be /// HEAD and not a merge) keeping its changes staged; undoing the initial commit /// leaves an unborn branch. Returns the undone commit's message. pub fn undo_commit(&self, commit: &str) -> IdeResult { let _scope = crate::ExecutionScope::enter(self.execution_allowed.load(std::sync::atomic::Ordering::Acquire)); write::undo_commit(self, commit) } /// Deletes a local branch (`git branch -d`, `-D` when `force`). pub fn delete_branch(&self, branch: &str, force: bool) -> IdeResult<()> { let _scope = crate::ExecutionScope::enter(self.execution_allowed.load(std::sync::atomic::Ordering::Acquire)); write::delete_branch(self, branch, force) } /// Deletes a local branch as IDEA does: without asking, force-deleting one that is not /// fully merged (reported as `unmerged`), and returning what [`Repo::restore_branch`] /// needs to undo it. pub fn delete_branch_restorable(&self, branch: &str) -> IdeResult { let _scope = crate::ExecutionScope::enter(self.execution_allowed.load(std::sync::atomic::Ordering::Acquire)); write::delete_branch_restorable(self, branch) } /// Recreates a deleted branch at its old tip, with its upstream. pub fn restore_branch(&self, deleted: &DeletedBranch) -> IdeResult<()> { let _scope = crate::ExecutionScope::enter(self.execution_allowed.load(std::sync::atomic::Ordering::Acquire)); write::restore_branch(self, deleted) } /// Renames a local branch (`git branch -m`); its upstream configuration moves with it. pub fn rename_branch(&self, from: &str, to: &str) -> IdeResult<()> { let _scope = crate::ExecutionScope::enter(self.execution_allowed.load(std::sync::atomic::Ordering::Acquire)); write::rename_branch(self, from, to) } /// Merges `branch` into HEAD; local changes are stashed and restored around it. On /// conflicts an error is returned and the repository is left mid-merge /// (`branch_info().operation == Some("merge")`). pub fn merge(&self, branch: &str) -> IdeResult<()> { let _scope = crate::ExecutionScope::enter(self.execution_allowed.load(std::sync::atomic::Ordering::Acquire)); write::merge(self, branch) } /// Rebases the current branch onto `onto`; local changes are stashed and restored. pub fn rebase(&self, onto: &str) -> IdeResult<()> { let _scope = crate::ExecutionScope::enter(self.execution_allowed.load(std::sync::atomic::Ordering::Acquire)); write::rebase(self, onto) } /// `keep_index`: changes already staged stay in the index (and the working tree). pub fn stash_push(&self, message: Option<&str>, include_untracked: bool, keep_index: bool) -> IdeResult<()> { let _scope = crate::ExecutionScope::enter(self.execution_allowed.load(std::sync::atomic::Ordering::Acquire)); write::stash_push(self, message, include_untracked, keep_index) } /// `reinstate_index`: also restore what was staged when the stash was made. pub fn stash_apply(&self, index: u32, pop: bool, reinstate_index: bool) -> IdeResult<()> { let _scope = crate::ExecutionScope::enter(self.execution_allowed.load(std::sync::atomic::Ordering::Acquire)); write::stash_apply(self, index, pop, reinstate_index) } /// Creates and checks out `branch` at the commit the stash was made on, applies the /// stash there (index included) and drops it. pub fn stash_branch(&self, index: u32, branch: &str) -> IdeResult<()> { let _scope = crate::ExecutionScope::enter(self.execution_allowed.load(std::sync::atomic::Ordering::Acquire)); write::stash_branch(self, index, branch) } /// Removes `stash@{index}` without applying it. pub fn stash_drop(&self, index: u32) -> IdeResult<()> { let _scope = crate::ExecutionScope::enter(self.execution_allowed.load(std::sync::atomic::Ordering::Acquire)); write::stash_drop(self, index) } /// Deletes a branch on a remote (`git push --delete `); `branch` is the /// remote-tracking name, e.g. `origin/feature`. pub fn delete_remote_branch( &self, branch: &str, cancel: &CancellationToken, on_output: &dyn Fn(String), ) -> IdeResult<()> { let _scope = crate::ExecutionScope::enter(self.execution_allowed.load(std::sync::atomic::Ordering::Acquire)); write::delete_remote_branch(self, branch, cancel, on_output) } /// Network operations stream progress lines through `on_output`. /// Fetches all remotes (pruning deleted branches). pub fn fetch(&self, cancel: &CancellationToken, on_output: &dyn Fn(String)) -> IdeResult<()> { let _scope = crate::ExecutionScope::enter(self.execution_allowed.load(std::sync::atomic::Ordering::Acquire)); cli::run_streaming(&self.root, &["fetch", "--progress", "--all", "--prune"], cancel, on_output) } /// Update Project: pulls the upstream by merge or rebase. Local changes are stashed /// and restored; the merge mode fast-forwards when possible whatever `pull.ff` says. pub fn pull(&self, rebase: bool, cancel: &CancellationToken, on_output: &dyn Fn(String)) -> IdeResult<()> { let _scope = crate::ExecutionScope::enter(self.execution_allowed.load(std::sync::atomic::Ordering::Acquire)); let mode: &[&str] = if rebase { &["--rebase"] } else { &["--no-rebase", "--ff"] }; let mut args = vec!["pull", "--progress", "--autostash"]; args.extend_from_slice(mode); cli::run_streaming(&self.root, &args, cancel, on_output) } /// Pushes the current branch. `set_upstream` pushes to the branch's remote (or /// `origin`) under the same name and records it as upstream. pub fn push( &self, force_with_lease: bool, set_upstream: bool, cancel: &CancellationToken, on_output: &dyn Fn(String), ) -> IdeResult<()> { let _scope = crate::ExecutionScope::enter(self.execution_allowed.load(std::sync::atomic::Ordering::Acquire)); write::push(self, force_with_lease, set_upstream, None, cancel, on_output) } /// [`Repo::push`] that also pushes tags (Push dialog › Push tags). pub fn push_with_tags( &self, force_with_lease: bool, set_upstream: bool, tags: Option, cancel: &CancellationToken, on_output: &dyn Fn(String), ) -> IdeResult<()> { let _scope = crate::ExecutionScope::enter(self.execution_allowed.load(std::sync::atomic::Ordering::Acquire)); write::push(self, force_with_lease, set_upstream, tags, cancel, on_output) } /// Pushes a local branch, or its history up to `revision`, to its upstream (Git Log /// › Push…, Push All up to Here). pub fn push_branch( &self, branch: &str, revision: Option<&str>, force_with_lease: bool, cancel: &CancellationToken, on_output: &dyn Fn(String), ) -> IdeResult<()> { let _scope = crate::ExecutionScope::enter(self.execution_allowed.load(std::sync::atomic::Ordering::Acquire)); write::push_branch( self, branch, PushBranchOptions { revision, force_with_lease, ..Default::default() }, cancel, on_output, ) } /// [`Repo::push_branch`] to remote branch `target` (None keeps the upstream's name) /// and/or remote `remote` (None keeps the upstream's remote). The Push dialog's /// editable target and remote picker. pub fn push_branch_to( &self, branch: &str, options: PushBranchOptions<'_>, cancel: &CancellationToken, on_output: &dyn Fn(String), ) -> IdeResult<()> { let _scope = crate::ExecutionScope::enter(self.execution_allowed.load(std::sync::atomic::Ordering::Acquire)); write::push_branch(self, branch, options, cancel, on_output) } /// Fast-forwards a local branch that is not checked out to its upstream (Branches › /// Update). Fails when the branch has diverged. pub fn update_branch(&self, branch: &str, cancel: &CancellationToken, on_output: &dyn Fn(String)) -> IdeResult<()> { let _scope = crate::ExecutionScope::enter(self.execution_allowed.load(std::sync::atomic::Ordering::Acquire)); write::update_branch(self, branch, cancel, on_output) } // ---- in-progress operations, history rewriting, tags ---- /// Continues the merge / rebase / cherry-pick / revert in progress; fails while /// conflicts are unresolved. pub fn continue_operation(&self, cancel: &CancellationToken, on_output: &dyn Fn(String)) -> IdeResult<()> { let _scope = crate::ExecutionScope::enter(self.execution_allowed.load(std::sync::atomic::Ordering::Acquire)); ops::continue_op(self, cancel, on_output) } pub fn abort_operation(&self, cancel: &CancellationToken, on_output: &dyn Fn(String)) -> IdeResult<()> { let _scope = crate::ExecutionScope::enter(self.execution_allowed.load(std::sync::atomic::Ordering::Acquire)); ops::abort_op(self, cancel, on_output) } /// Skips the current commit of a rebase / cherry-pick / revert. pub fn skip_operation(&self, cancel: &CancellationToken, on_output: &dyn Fn(String)) -> IdeResult<()> { let _scope = crate::ExecutionScope::enter(self.execution_allowed.load(std::sync::atomic::Ordering::Acquire)); ops::skip_op(self, cancel, on_output) } /// Cherry-picks `commits` in order (oldest first). Stops mid-way on conflicts. pub fn cherry_pick( &self, commits: &[String], cancel: &CancellationToken, on_output: &dyn Fn(String), ) -> IdeResult<()> { let _scope = crate::ExecutionScope::enter(self.execution_allowed.load(std::sync::atomic::Ordering::Acquire)); ops::cherry_pick(self, commits, cancel, on_output) } /// Reverts `commits` in order (newest first) with git's default messages. pub fn revert(&self, commits: &[String], cancel: &CancellationToken, on_output: &dyn Fn(String)) -> IdeResult<()> { let _scope = crate::ExecutionScope::enter(self.execution_allowed.load(std::sync::atomic::Ordering::Acquire)); ops::revert(self, commits, cancel, on_output) } /// Interactive rebase of `base..HEAD` (`None`: from the root) following `entries`, /// oldest first. Local changes are stashed and restored. Stops (leaving the rebase in /// progress) on conflicts and at `Edit` entries. pub fn interactive_rebase( &self, base: Option<&str>, entries: &[RebaseEntry], cancel: &CancellationToken, on_output: &dyn Fn(String), ) -> IdeResult<()> { let _scope = crate::ExecutionScope::enter(self.execution_allowed.load(std::sync::atomic::Ordering::Acquire)); ops::interactive_rebase(self, base, entries, cancel, on_output) } /// Moves the current branch to `commit` (`git reset --`). pub fn reset( &self, commit: &str, mode: ResetMode, cancel: &CancellationToken, on_output: &dyn Fn(String), ) -> IdeResult<()> { let _scope = crate::ExecutionScope::enter(self.execution_allowed.load(std::sync::atomic::Ordering::Acquire)); ops::reset(self, commit, mode, cancel, on_output) } /// Creates tag `name` on `commit` (annotated when `message` is given). pub fn create_tag(&self, name: &str, commit: &str, message: Option<&str>, force: bool) -> IdeResult<()> { let _scope = crate::ExecutionScope::enter(self.execution_allowed.load(std::sync::atomic::Ordering::Acquire)); ops::create_tag(self, name, commit, message, force) } pub fn tags(&self) -> IdeResult> { let _scope = crate::ExecutionScope::enter(self.execution_allowed.load(std::sync::atomic::Ordering::Acquire)); ops::tags(self) } /// Files that differ between revisions `from` and `to`. pub fn diff_revisions(&self, from: &str, to: &str) -> IdeResult> { let _scope = crate::ExecutionScope::enter(self.execution_allowed.load(std::sync::atomic::Ordering::Acquire)); ops::diff_revisions(self, from, to) } /// Tracked files whose working-tree content differs from `rev`. pub fn diff_with_worktree(&self, rev: &str) -> IdeResult> { let _scope = crate::ExecutionScope::enter(self.execution_allowed.load(std::sync::atomic::Ordering::Acquire)); ops::diff_with_worktree(self, rev) } /// `(repo-relative path, added lines, deleted lines)` of the tracked files whose /// working-tree content differs from `rev`. pub fn numstat_with_worktree(&self, rev: &str) -> IdeResult> { let _scope = crate::ExecutionScope::enter(self.execution_allowed.load(std::sync::atomic::Ordering::Acquire)); ops::numstat_with_worktree(self, rev) } // ---- patches and shelf ---- /// Patch of the local changes of `paths` against HEAD (untracked files included). pub fn create_patch(&self, paths: &[PathBuf], reverse: bool) -> IdeResult { let _scope = crate::ExecutionScope::enter(self.execution_allowed.load(std::sync::atomic::Ordering::Acquire)); let rel = self.rel_paths(paths)?; Ok(patch::working_patch(self, &rel, reverse, true)?.0) } /// Patch of the changes made by `commits` (oldest first). pub fn create_commits_patch(&self, commits: &[String], reverse: bool) -> IdeResult { let _scope = crate::ExecutionScope::enter(self.execution_allowed.load(std::sync::atomic::Ordering::Acquire)); patch::commits_patch(self, commits, reverse) } pub fn patch_preview(&self, patch: &str) -> IdeResult> { let _scope = crate::ExecutionScope::enter(self.execution_allowed.load(std::sync::atomic::Ordering::Acquire)); patch::preview(self, patch) } /// Applies a patch to the working tree, 3-way merging when it does not apply cleanly. pub fn apply_patch(&self, patch: &str) -> IdeResult { let _scope = crate::ExecutionScope::enter(self.execution_allowed.load(std::sync::atomic::Ordering::Acquire)); patch::apply(self, patch) } /// Saves the changes of `paths` under `shelf_dir` and reverts them. pub fn shelve(&self, shelf_dir: &Path, name: &str, paths: &[PathBuf]) -> IdeResult { let _scope = crate::ExecutionScope::enter(self.execution_allowed.load(std::sync::atomic::Ordering::Acquire)); let rel = self.rel_paths(paths)?; shelf::shelve(self, shelf_dir, name, &rel) } pub fn unshelve(&self, shelf_dir: &Path, id: &str, remove: bool) -> IdeResult { let _scope = crate::ExecutionScope::enter(self.execution_allowed.load(std::sync::atomic::Ordering::Acquire)); shelf::unshelve(self, shelf_dir, id, remove) } /// Who is on each side of the conflicts of the operation in progress. pub fn merge_sides(&self) -> IdeResult { let _scope = crate::ExecutionScope::enter(self.execution_allowed.load(std::sync::atomic::Ordering::Acquire)); ops::merge_sides(self) } /// The versions of a conflicted file with the user's work as `ours`, as IDEA's merge /// dialog shows them (sides swapped during a rebase). pub fn merge_versions(&self, path: &Path, encoding: impl Into) -> IdeResult { let encoding = encoding.into(); let _scope = crate::ExecutionScope::enter(self.execution_allowed.load(std::sync::atomic::Ordering::Acquire)); let (base, ours, theirs) = self.conflict_versions(path, encoding)?; let sides = self.merge_sides()?; let (ours, theirs) = if sides.reversed { (theirs, ours) } else { (ours, theirs) }; let merged = match (&ours, &theirs) { (Some(o), Some(t)) => Some(ops::merge_file(self, o, base.as_deref().unwrap_or(""), t)?), _ => None, }; Ok(MergeVersions { base, ours, theirs, sides, merged }) } /// For conflict resolution: (base, ours, theirs) versions of a conflicted file. pub fn conflict_versions( &self, path: &Path, encoding: impl Into, ) -> IdeResult<(Option, Option, Option)> { let encoding = encoding.into(); let _scope = crate::ExecutionScope::enter(self.execution_allowed.load(std::sync::atomic::Ordering::Acquire)); let rel = self.rel_path(path)?; let repo = self.gix()?; let stage = |n: u8| -> IdeResult> { Ok(repo::blob_at(&repo, &rel, &format!(":{n}"))?.map(|b| text_encoding::decode(&b, encoding).into_owned())) }; Ok((stage(1)?, stage(2)?, stage(3)?)) } } thread_local! { static EXECUTION_ALLOWED: std::cell::Cell = const { std::cell::Cell::new(true) }; } struct ExecutionScope(bool); impl ExecutionScope { fn enter(allowed: bool) -> Self { Self(EXECUTION_ALLOWED.with(|current| current.replace(current.get() && allowed))) } } impl Drop for ExecutionScope { fn drop(&mut self) { EXECUTION_ALLOWED.with(|current| current.set(self.0)); } } pub(crate) fn execution_allowed() -> bool { EXECUTION_ALLOWED.with(|current| current.get()) } impl Repo { pub fn set_execution_allowed(&self, allowed: bool) { let _scope = crate::ExecutionScope::enter(self.execution_allowed.load(std::sync::atomic::Ordering::Acquire)); self.execution_allowed.store(allowed, std::sync::atomic::Ordering::Release); } }