mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-08 08:52:12 +00:00
Git Log could lose commits when filtering by several paths, draw wrong ancestry after filtering or collapsing rows, and open the wrong repository in multi-root views. The graph is now projected over the full commit DAG and laid out in Bek order, so filtered and collapsed views keep the real ancestry; each log view keeps its own filters and requests. Commit metadata, messages and path changes are kept in a persistent index (by object id, in the IDE's cache directory) with seekable paging, and the log gains parent-aware merge comparisons, a lazy revision browser, read-only logs of repositories outside the project (opened with command execution disabled), commit bookmarks, configurable built-in columns and batched commit details. The adapted IntelliJ algorithms are attributed in THIRD_PARTY_LICENSES.md. Closes #872 Co-authored-by: shohruh-genius <[email protected]>
1415 lines
62 KiB
Rust
1415 lines
62 KiB
Rust
//! 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<ChangeKind>,
|
||
/// Unstaged (working tree) change, if any.
|
||
pub unstaged: Option<ChangeKind>,
|
||
pub renamed_from: Option<String>,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, Type)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct BranchInfo {
|
||
/// `None` when HEAD is detached.
|
||
pub head: Option<String>,
|
||
pub head_commit: Option<String>,
|
||
pub upstream: Option<String>,
|
||
pub ahead: u32,
|
||
pub behind: u32,
|
||
/// Merge / rebase / cherry-pick in progress.
|
||
pub operation: Option<String>,
|
||
}
|
||
|
||
#[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<BlockRevision>,
|
||
}
|
||
|
||
/// 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<String>,
|
||
/// Connections in the filtered graph; never use these as revision/diff parents.
|
||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||
pub graph_parents: Option<Vec<String>>,
|
||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||
pub graph_dashed: Vec<bool>,
|
||
/// Repository-relative filename at this revision in rename-following history.
|
||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||
pub history_path: Option<String>,
|
||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||
pub log_position: Option<u32>,
|
||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||
pub log_total: Option<u32>,
|
||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||
pub graph_boundary_parents: Vec<String>,
|
||
/// Reachability from HEAD, computed independently of the visible filters.
|
||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||
pub on_current_branch: Option<bool>,
|
||
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<String>,
|
||
/// 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<bool>,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, Type)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct LogDetails {
|
||
pub message: String,
|
||
pub containing_branches: Vec<String>,
|
||
/// 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<String>,
|
||
#[serde(default)]
|
||
pub search_fields: Vec<String>,
|
||
/// Jump directly to an indexed window containing this commit (50 rows of context).
|
||
#[serde(default)]
|
||
pub seek: Option<String>,
|
||
#[serde(default)]
|
||
pub retain_until: Option<String>,
|
||
pub branch: Option<String>,
|
||
pub path: Option<ide_core::IpcPath>,
|
||
#[serde(default)]
|
||
pub paths: Vec<ide_core::IpcPath>,
|
||
/// File history follows renames; structure filters retain fixed paths.
|
||
#[serde(default)]
|
||
pub follow: bool,
|
||
pub author: Option<String>,
|
||
pub text: Option<String>,
|
||
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<String>,
|
||
/// Additional ranges unioned with each other and with selected branches.
|
||
#[serde(default)]
|
||
pub ranges: Vec<String>,
|
||
/// `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<String>,
|
||
/// More author needles next to `author`; a commit matches any of them (Log User filter).
|
||
#[serde(default)]
|
||
pub authors: Vec<String>,
|
||
#[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<f64>,
|
||
#[serde(default)]
|
||
pub until_ms: Option<f64>,
|
||
#[serde(default)]
|
||
pub request: Option<ide_core::RequestId>,
|
||
#[serde(default)]
|
||
pub summary_only: bool,
|
||
/// Inclusive refresh boundary. Clients join only verified linear prefixes.
|
||
#[serde(default)]
|
||
pub stop_at: Option<String>,
|
||
}
|
||
|
||
#[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<String>,
|
||
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 <[email protected]>`; `None` or blank uses the configured user.
|
||
pub author: Option<String>,
|
||
/// 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<PartialCommitFile>,
|
||
}
|
||
|
||
/// 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<HunkRange>,
|
||
}
|
||
|
||
/// 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<String>,
|
||
}
|
||
|
||
/// 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<String>,
|
||
/// Commit holding the old content; `None` when the file was added.
|
||
pub before: Option<String>,
|
||
/// Commit holding the new content; `None` when the file was deleted.
|
||
pub after: Option<String>,
|
||
}
|
||
|
||
#[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<String>,
|
||
}
|
||
|
||
/// `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<String>,
|
||
/// Repository owning the patch; absent in legacy shelves.
|
||
#[serde(default)]
|
||
pub repo: Option<ide_core::IpcPath>,
|
||
/// Repository-relative paths.
|
||
pub files: Vec<ChangedFile>,
|
||
}
|
||
|
||
/// 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<String>,
|
||
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<String>,
|
||
/// Repository-relative paths left with conflicts (index stages for the merge UI).
|
||
pub conflicts: Vec<String>,
|
||
}
|
||
|
||
/// 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<DivergedLocal>,
|
||
}
|
||
|
||
/// 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<String>,
|
||
},
|
||
/// 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<String>,
|
||
},
|
||
}
|
||
|
||
/// 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<String>,
|
||
/// 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<String>,
|
||
/// 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<String>,
|
||
/// Short id of the commit being applied (REBASE_HEAD, CHERRY_PICK_HEAD, REVERT_HEAD)
|
||
/// or merged (MERGE_HEAD).
|
||
pub commit: Option<String>,
|
||
/// The other branch: the one being merged, or the one rebased onto.
|
||
pub other_branch: Option<String>,
|
||
}
|
||
|
||
/// 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<String>,
|
||
/// The user's side ("Accept Yours"): git's stage 2, or stage 3 when `sides.reversed`.
|
||
pub ours: Option<String>,
|
||
pub theirs: Option<String>,
|
||
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<String>,
|
||
}
|
||
|
||
#[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<Changelist>,
|
||
/// 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<String, String>,
|
||
}
|
||
|
||
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<u32>) -> IdeResult<Vec<String>> {
|
||
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<u32>,
|
||
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<LineHunk> {
|
||
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<repo::Cache>,
|
||
ignored: std::sync::RwLock<IgnoredNames>,
|
||
log_index: std::sync::Mutex<log_index::LogIndex>,
|
||
index_progress: std::sync::Mutex<LogIndexProgress>,
|
||
log_signatures: std::sync::Mutex<std::collections::HashMap<String, LogDetails>>,
|
||
}
|
||
|
||
impl Repo {
|
||
/// Discovers the repository containing `path`; `Ok(None)` when not under git.
|
||
pub fn discover(path: &Path) -> IdeResult<Option<Self>> {
|
||
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<Vec<FileStatus>> {
|
||
let _scope = crate::ExecutionScope::enter(self.execution_allowed.load(std::sync::atomic::Ordering::Acquire));
|
||
status::status(self)
|
||
}
|
||
pub fn branch_info(&self) -> IdeResult<BranchInfo> {
|
||
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<String>, Option<String>)> {
|
||
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<Option<String>> {
|
||
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<TextEncoding>,
|
||
) -> IdeResult<Option<String>> {
|
||
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<Option<Vec<u8>>> {
|
||
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<TextEncoding>,
|
||
) -> 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<TextEncoding>,
|
||
) -> IdeResult<Vec<LineHunk>> {
|
||
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<TextEncoding>,
|
||
) -> IdeResult<Vec<BlameLine>> {
|
||
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<TextEncoding>,
|
||
cancel: &CancellationToken,
|
||
) -> IdeResult<BlockHistory> {
|
||
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: <name>`.
|
||
pub fn log(&self, query: &LogQuery) -> IdeResult<Vec<Commit>> {
|
||
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<Vec<Commit>> {
|
||
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<std::collections::BTreeMap<String, String>> {
|
||
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<Vec<String>> {
|
||
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::<std::collections::BTreeSet<_>>()
|
||
.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<LogIndexProgress> {
|
||
{
|
||
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::<IdeResult<_>>()?;
|
||
{
|
||
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::<Vec<_>>();
|
||
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<LogDetails> {
|
||
self.log_details_cancellable(revision, &CancellationToken::new())
|
||
}
|
||
pub fn log_details_cancellable(&self, revision: &str, cancel: &CancellationToken) -> IdeResult<LogDetails> {
|
||
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<Vec<String>> {
|
||
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<Vec<String>> {
|
||
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<Vec<ChangedFile>> {
|
||
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<Vec<RevisionChange>> {
|
||
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<Vec<Branch>> {
|
||
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<Vec<Stash>> {
|
||
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<TextEncoding>) -> IdeResult<String> {
|
||
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 <key>` (all scopes); `None` when unset.
|
||
pub fn config_get(&self, key: &str) -> IdeResult<Option<String>> {
|
||
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<E: Into<TextEncoding>>(
|
||
&self,
|
||
patch: &str,
|
||
encoding: &dyn Fn(&Path) -> IdeResult<E>,
|
||
) -> 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<String> {
|
||
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<E: Into<TextEncoding>>(
|
||
&self,
|
||
message: &str,
|
||
options: &CommitOptions,
|
||
encoding: &dyn Fn(&Path) -> IdeResult<E>,
|
||
) -> IdeResult<String> {
|
||
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<CheckoutOutcome> {
|
||
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<String> {
|
||
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<DeletedBranch> {
|
||
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 <remote> --delete <branch>`); `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<PushTags>,
|
||
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 --<mode>`).
|
||
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<Vec<String>> {
|
||
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<Vec<ChangedFile>> {
|
||
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<Vec<ChangedFile>> {
|
||
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<Vec<(String, u32, u32)>> {
|
||
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<String> {
|
||
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<String> {
|
||
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<Vec<PatchFile>> {
|
||
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<ApplyOutcome> {
|
||
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<ShelvedChangeList> {
|
||
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<ApplyOutcome> {
|
||
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<MergeSides> {
|
||
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<TextEncoding>) -> IdeResult<MergeVersions> {
|
||
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<TextEncoding>,
|
||
) -> IdeResult<(Option<String>, Option<String>, Option<String>)> {
|
||
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<Option<String>> {
|
||
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<bool> = 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);
|
||
}
|
||
}
|