use crate::commit::parse_git_diff_name_status;use crate::stash::GitStash;use crate::status::{DiffTreeType, GitStatus, StatusCode, TreeDiff};use crate::{Oid, RunHook, SHORT_SHA_LENGTH};use anyhow::{Context as _, Result, anyhow, bail};use collections::HashMap;use futures::channel::oneshot;use futures::future::BoxFuture;use futures::io::BufWriter;use futures::{AsyncWriteExt, FutureExt as _, select_biased};use git2::{BranchType, ErrorCode};use gpui::{AppContext as _, AsyncApp, BackgroundExecutor, SharedString, Task};use parking_lot::Mutex;use rope::Rope;use schemars::JsonSchema;use serde::Deserialize;use smallvec::SmallVec;use smol::channel::Sender;use smol::io::{AsyncBufReadExt, AsyncReadExt, BufReader};use text::LineEnding; use std::collections::HashSet;use std::ffi::{OsStr, OsString}; use std::process::ExitStatus;use std::str::FromStr;use std::{ cmp::Ordering, future, path::{Path, PathBuf}, sync::Arc,};use sum_tree::MapSeekTarget;use thiserror::Error;use util::command::{Stdio, new_command};use util::paths::PathStyle;use util::rel_path::RelPath;use util::{ResultExt, normalize_path, paths};use uuid::Uuid; pub use askpass::{AskPassDelegate, AskPassResult, AskPassSession}; pub const REMOTE_CANCELLED_BY_USER: &str = "Operation cancelled by user"; /// Format string used in graph log to get initial data for the git graph/// %H - Full commit hash/// %P - Parent hashes/// %D - Ref names/// %x00 - Null byte separator, used to split up commit datastatic GRAPH_COMMIT_FORMAT: &str = "--format=%H%x00%P%x00%D"; /// Number of commits to load per chunk for the git graph.pub const GRAPH_CHUNK_SIZE: usize = 1000; /// Default value for the `git.worktree_directory` setting.pub const DEFAULT_WORKTREE_DIRECTORY: &str = "../worktrees"; /// Resolves the configured worktree directory to an absolute path.////// `worktree_directory_setting` is the raw string from the user setting/// (e.g. `"../worktrees"`, `".git/zed-worktrees"`, `"my-worktrees/"`)./// Trailing slashes are stripped. The path is resolved relative to/// `working_directory` (the repository's working directory root).////// When the resolved directory falls outside the working directory/// (e.g. `"../worktrees"`), the repository's directory name is/// automatically appended so that sibling repos don't collide./// For example, with working directory `~/code/zed` and setting/// `"../worktrees"`, this returns `~/code/worktrees/zed`.////// When the resolved directory is inside the working directory/// (e.g. `".git/zed-worktrees"`), no extra component is added/// because the path is already project-scoped.pub fn resolve_worktree_directory( working_directory: &Path, worktree_directory_setting: &str,) -> PathBuf { let trimmed = worktree_directory_setting.trim_end_matches(['/', '\\']); let joined = working_directory.join(trimmed); let resolved = normalize_path(&joined); if resolved.starts_with(working_directory) { resolved } else if let Some(repo_dir_name) = working_directory.file_name() { resolved.join(repo_dir_name) } else { resolved }} /// Validates that the resolved worktree directory is acceptable:/// - The setting must not be an absolute path./// - The resolved path must be either a subdirectory of the working/// directory or a subdirectory of its parent (i.e., a sibling).////// Returns `Ok(resolved_path)` or an error with a user-facing message.pub fn validate_worktree_directory( working_directory: &Path, worktree_directory_setting: &str,) -> Result<PathBuf> { // Check the original setting before trimming, since a path like "///" // is absolute but becomes "" after stripping trailing separators. // Also check for leading `/` or `\` explicitly, because on Windows // `Path::is_absolute()` requires a drive letter — so `/tmp/worktrees` // would slip through even though it's clearly not a relative path. if Path::new(worktree_directory_setting).is_absolute() || worktree_directory_setting.starts_with('/') || worktree_directory_setting.starts_with('\\') { anyhow::bail!( "git.worktree_directory must be a relative path, got: {worktree_directory_setting:?}" ); } if worktree_directory_setting.is_empty() { anyhow::bail!("git.worktree_directory must not be empty"); } let trimmed = worktree_directory_setting.trim_end_matches(['/', '\\']); if trimmed == ".." { anyhow::bail!("git.worktree_directory must not be \"..\" (use \"../some-name\" instead)"); } let resolved = resolve_worktree_directory(working_directory, worktree_directory_setting); let parent = working_directory.parent().unwrap_or(working_directory); if !resolved.starts_with(parent) { anyhow::bail!( "git.worktree_directory resolved to {resolved:?}, which is outside \ the project root and its parent directory. It must resolve to a \ subdirectory of {working_directory:?} or a sibling of it." ); } Ok(resolved)} /// Returns the full absolute path for a specific branch's worktree/// given the resolved worktree directory.pub fn worktree_path_for_branch( working_directory: &Path, worktree_directory_setting: &str, branch: &str,) -> PathBuf { resolve_worktree_directory(working_directory, worktree_directory_setting).join(branch)} /// Commit data needed for the git graph visualization.#[derive(Debug, Clone)]pub struct GraphCommitData { pub sha: Oid, /// Most commits have a single parent, so we use a SmallVec to avoid allocations. pub parents: SmallVec<[Oid; 1]>, pub author_name: SharedString, pub author_email: SharedString, pub commit_timestamp: i64, pub subject: SharedString,} #[derive(Debug)]pub struct InitialGraphCommitData { pub sha: Oid, pub parents: SmallVec<[Oid; 1]>, pub ref_names: Vec<SharedString>,} struct CommitDataRequest { sha: Oid, response_tx: oneshot::Sender<Result<GraphCommitData>>,} pub struct CommitDataReader { request_tx: smol::channel::Sender<CommitDataRequest>, _task: Task<()>,} impl CommitDataReader { pub async fn read(&self, sha: Oid) -> Result<GraphCommitData> { let (response_tx, response_rx) = oneshot::channel(); self.request_tx .send(CommitDataRequest { sha, response_tx }) .await .map_err(|_| anyhow!("commit data reader task closed"))?; response_rx .await .map_err(|_| anyhow!("commit data reader task dropped response"))? }} fn parse_cat_file_commit(sha: Oid, content: &str) -> Option<GraphCommitData> { let mut parents = SmallVec::new(); let mut author_name = SharedString::default(); let mut author_email = SharedString::default(); let mut commit_timestamp = 0i64; let mut in_headers = true; let mut subject = None; for line in content.lines() { if in_headers { if line.is_empty() { in_headers = false; continue; } if let Some(parent_sha) = line.strip_prefix("parent ") { if let Ok(oid) = Oid::from_str(parent_sha.trim()) { parents.push(oid); } } else if let Some(author_line) = line.strip_prefix("author ") { if let Some((name_email, _timestamp_tz)) = author_line.rsplit_once(' ') { if let Some((name_email, timestamp_str)) = name_email.rsplit_once(' ') { if let Ok(ts) = timestamp_str.parse::<i64>() { commit_timestamp = ts; } if let Some((name, email)) = name_email.rsplit_once(" <") { author_name = SharedString::from(name.to_string()); author_email = SharedString::from(email.trim_end_matches('>').to_string()); } } } } } else if subject.is_none() { subject = Some(SharedString::from(line.to_string())); } } Some(GraphCommitData { sha, parents, author_name, author_email, commit_timestamp, subject: subject.unwrap_or_default(), })} #[derive(Clone, Debug, Hash, PartialEq, Eq)]pub struct Branch { pub is_head: bool, pub ref_name: SharedString, pub upstream: Option<Upstream>, pub most_recent_commit: Option<CommitSummary>,} impl Branch { pub fn name(&self) -> &str { self.ref_name .as_ref() .strip_prefix("refs/heads/") .or_else(|| self.ref_name.as_ref().strip_prefix("refs/remotes/")) .unwrap_or(self.ref_name.as_ref()) } pub fn is_remote(&self) -> bool { self.ref_name.starts_with("refs/remotes/") } pub fn remote_name(&self) -> Option<&str> { self.ref_name .strip_prefix("refs/remotes/") .and_then(|stripped| stripped.split("/").next()) } pub fn tracking_status(&self) -> Option<UpstreamTrackingStatus> { self.upstream .as_ref() .and_then(|upstream| upstream.tracking.status()) } pub fn priority_key(&self) -> (bool, Option<i64>) { ( self.is_head, self.most_recent_commit .as_ref() .map(|commit| commit.commit_timestamp), ) }} #[derive(Clone, Debug, Hash, PartialEq, Eq)]pub struct Worktree { pub path: PathBuf, pub ref_name: SharedString, pub sha: SharedString,} impl Worktree { pub fn branch(&self) -> &str { self.ref_name .as_ref() .strip_prefix("refs/heads/") .or_else(|| self.ref_name.as_ref().strip_prefix("refs/remotes/")) .unwrap_or(self.ref_name.as_ref()) }} pub fn parse_worktrees_from_str<T: AsRef<str>>(raw_worktrees: T) -> Vec<Worktree> { let mut worktrees = Vec::new(); let normalized = raw_worktrees.as_ref().replace("\r\n", "\n"); let entries = normalized.split("\n\n"); for entry in entries { let mut path = None; let mut sha = None; let mut ref_name = None; for line in entry.lines() { let line = line.trim(); if line.is_empty() { continue; } if let Some(rest) = line.strip_prefix("worktree ") { path = Some(rest.to_string()); } else if let Some(rest) = line.strip_prefix("HEAD ") { sha = Some(rest.to_string()); } else if let Some(rest) = line.strip_prefix("branch ") { ref_name = Some(rest.to_string()); } // Ignore other lines: detached, bare, locked, prunable, etc. } if let (Some(path), Some(sha), Some(ref_name)) = (path, sha, ref_name) { worktrees.push(Worktree { path: PathBuf::from(path), ref_name: ref_name.into(), sha: sha.into(), }) } } worktrees} #[derive(Clone, Debug, Hash, PartialEq, Eq)]pub struct Upstream { pub ref_name: SharedString, pub tracking: UpstreamTracking,} impl Upstream { pub fn is_remote(&self) -> bool { self.remote_name().is_some() } pub fn remote_name(&self) -> Option<&str> { self.ref_name .strip_prefix("refs/remotes/") .and_then(|stripped| stripped.split("/").next()) } pub fn stripped_ref_name(&self) -> Option<&str> { self.ref_name.strip_prefix("refs/remotes/") } pub fn branch_name(&self) -> Option<&str> { self.ref_name .strip_prefix("refs/remotes/") .and_then(|stripped| stripped.split_once('/').map(|(_, name)| name)) }} #[derive(Clone, Copy, Default)]pub struct CommitOptions { pub amend: bool, pub signoff: bool,} #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]pub enum UpstreamTracking { /// Remote ref not present in local repository. Gone, /// Remote ref present in local repository (fetched from remote). Tracked(UpstreamTrackingStatus),} impl From<UpstreamTrackingStatus> for UpstreamTracking { fn from(status: UpstreamTrackingStatus) -> Self { UpstreamTracking::Tracked(status) }} impl UpstreamTracking { pub fn is_gone(&self) -> bool { matches!(self, UpstreamTracking::Gone) } pub fn status(&self) -> Option<UpstreamTrackingStatus> { match self { UpstreamTracking::Gone => None, UpstreamTracking::Tracked(status) => Some(*status), } }} #[derive(Debug, Clone)]pub struct RemoteCommandOutput { pub stdout: String, pub stderr: String,} impl RemoteCommandOutput { pub fn is_empty(&self) -> bool { self.stdout.is_empty() && self.stderr.is_empty() }} #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]pub struct UpstreamTrackingStatus { pub ahead: u32, pub behind: u32,} #[derive(Clone, Debug, Hash, PartialEq, Eq)]pub struct CommitSummary { pub sha: SharedString, pub subject: SharedString, /// This is a unix timestamp pub commit_timestamp: i64, pub author_name: SharedString, pub has_parent: bool,} #[derive(Clone, Debug, Default, Hash, PartialEq, Eq)]pub struct CommitDetails { pub sha: SharedString, pub message: SharedString, pub commit_timestamp: i64, pub author_email: SharedString, pub author_name: SharedString,} #[derive(Clone, Debug, Hash, PartialEq, Eq)]pub struct FileHistoryEntry { pub sha: SharedString, pub subject: SharedString, pub message: SharedString, pub commit_timestamp: i64, pub author_name: SharedString, pub author_email: SharedString,} #[derive(Debug, Clone)]pub struct FileHistory { pub entries: Vec<FileHistoryEntry>, pub path: RepoPath,} #[derive(Debug)]pub struct CommitDiff { pub files: Vec<CommitFile>,} #[derive(Debug)]pub struct CommitFile { pub path: RepoPath, pub old_text: Option<String>, pub new_text: Option<String>, pub is_binary: bool,} impl CommitDetails { pub fn short_sha(&self) -> SharedString { self.sha[..SHORT_SHA_LENGTH].to_string().into() }} /// Detects if content is binary by checking for NUL bytes in the first 8000 bytes./// This matches git's binary detection heuristic.pub fn is_binary_content(content: &[u8]) -> bool { let check_len = content.len().min(8000); content[..check_len].contains(&0)} #[derive(Debug, Clone, Hash, PartialEq, Eq)]pub struct Remote { pub name: SharedString,} pub enum ResetMode { /// Reset the branch pointer, leave index and worktree unchanged (this will make it look like things that were /// committed are now staged). Soft, /// Reset the branch pointer and index, leave worktree unchanged (this makes it look as though things that were /// committed are now unstaged). Mixed,} #[derive(Debug, Clone, Hash, PartialEq, Eq)]pub enum FetchOptions { All, Remote(Remote),} impl FetchOptions { pub fn to_proto(&self) -> Option<String> { match self { FetchOptions::All => None, FetchOptions::Remote(remote) => Some(remote.clone().name.into()), }Lines 1–500 of 4276. Load more
use crate::commit::parse_git_diff_name_status; use crate::stash::GitStash; use crate::status::{DiffTreeType, GitStatus, StatusCode, TreeDiff}; use crate::{Oid, RunHook, SHORT_SHA_LENGTH}; use anyhow::{Context as _, Result, anyhow, bail}; use collections::HashMap; use futures::channel::oneshot; use futures::future::BoxFuture; use futures::io::BufWriter; use futures::{AsyncWriteExt, FutureExt as _, select_biased}; use git2::{BranchType, ErrorCode}; use gpui::{AppContext as _, AsyncApp, BackgroundExecutor, SharedString, Task}; use parking_lot::Mutex; use rope::Rope; use schemars::JsonSchema; use serde::Deserialize; use smallvec::SmallVec; use smol::channel::Sender; use smol::io::{AsyncBufReadExt, AsyncReadExt, BufReader}; use text::LineEnding; use std::collections::HashSet; use std::ffi::{OsStr, OsString}; use std::process::ExitStatus; use std::str::FromStr; use std::{ cmp::Ordering, future, path::{Path, PathBuf}, sync::Arc, }; use sum_tree::MapSeekTarget; use thiserror::Error; use util::command::{Stdio, new_command}; use util::paths::PathStyle; use util::rel_path::RelPath; use util::{ResultExt, normalize_path, paths}; use uuid::Uuid; pub use askpass::{AskPassDelegate, AskPassResult, AskPassSession}; pub const REMOTE_CANCELLED_BY_USER: &str = "Operation cancelled by user"; /// Format string used in graph log to get initial data for the git graph /// %H - Full commit hash /// %P - Parent hashes /// %D - Ref names /// %x00 - Null byte separator, used to split up commit data static GRAPH_COMMIT_FORMAT: &str = "--format=%H%x00%P%x00%D"; /// Number of commits to load per chunk for the git graph. pub const GRAPH_CHUNK_SIZE: usize = 1000; /// Default value for the `git.worktree_directory` setting. pub const DEFAULT_WORKTREE_DIRECTORY: &str = "../worktrees"; /// Resolves the configured worktree directory to an absolute path. /// /// `worktree_directory_setting` is the raw string from the user setting /// (e.g. `"../worktrees"`, `".git/zed-worktrees"`, `"my-worktrees/"`). /// Trailing slashes are stripped. The path is resolved relative to /// `working_directory` (the repository's working directory root). /// /// When the resolved directory falls outside the working directory /// (e.g. `"../worktrees"`), the repository's directory name is /// automatically appended so that sibling repos don't collide. /// For example, with working directory `~/code/zed` and setting /// `"../worktrees"`, this returns `~/code/worktrees/zed`. /// /// When the resolved directory is inside the working directory /// (e.g. `".git/zed-worktrees"`), no extra component is added /// because the path is already project-scoped. pub fn resolve_worktree_directory( working_directory: &Path, worktree_directory_setting: &str, ) -> PathBuf { let trimmed = worktree_directory_setting.trim_end_matches(['/', '\\']); let joined = working_directory.join(trimmed); let resolved = normalize_path(&joined); if resolved.starts_with(working_directory) { resolved } else if let Some(repo_dir_name) = working_directory.file_name() { resolved.join(repo_dir_name) } else { resolved } } /// Validates that the resolved worktree directory is acceptable: /// - The setting must not be an absolute path. /// - The resolved path must be either a subdirectory of the working /// directory or a subdirectory of its parent (i.e., a sibling). /// /// Returns `Ok(resolved_path)` or an error with a user-facing message. pub fn validate_worktree_directory( working_directory: &Path, worktree_directory_setting: &str, ) -> Result<PathBuf> { // Check the original setting before trimming, since a path like "///" // is absolute but becomes "" after stripping trailing separators. // Also check for leading `/` or `\` explicitly, because on Windows // `Path::is_absolute()` requires a drive letter — so `/tmp/worktrees` // would slip through even though it's clearly not a relative path. if Path::new(worktree_directory_setting).is_absolute() || worktree_directory_setting.starts_with('/') || worktree_directory_setting.starts_with('\\') { anyhow::bail!( "git.worktree_directory must be a relative path, got: {worktree_directory_setting:?}" ); } if worktree_directory_setting.is_empty() { anyhow::bail!("git.worktree_directory must not be empty"); } let trimmed = worktree_directory_setting.trim_end_matches(['/', '\\']); if trimmed == ".." { anyhow::bail!("git.worktree_directory must not be \"..\" (use \"../some-name\" instead)"); } let resolved = resolve_worktree_directory(working_directory, worktree_directory_setting); let parent = working_directory.parent().unwrap_or(working_directory); if !resolved.starts_with(parent) { anyhow::bail!( "git.worktree_directory resolved to {resolved:?}, which is outside \ the project root and its parent directory. It must resolve to a \ subdirectory of {working_directory:?} or a sibling of it." ); } Ok(resolved) } /// Returns the full absolute path for a specific branch's worktree /// given the resolved worktree directory. pub fn worktree_path_for_branch( working_directory: &Path, worktree_directory_setting: &str, branch: &str, ) -> PathBuf { resolve_worktree_directory(working_directory, worktree_directory_setting).join(branch) } /// Commit data needed for the git graph visualization. #[derive(Debug, Clone)] pub struct GraphCommitData { pub sha: Oid, /// Most commits have a single parent, so we use a SmallVec to avoid allocations. pub parents: SmallVec<[Oid; 1]>, pub author_name: SharedString, pub author_email: SharedString, pub commit_timestamp: i64, pub subject: SharedString, } #[derive(Debug)] pub struct InitialGraphCommitData { pub sha: Oid, pub parents: SmallVec<[Oid; 1]>, pub ref_names: Vec<SharedString>, } struct CommitDataRequest { sha: Oid, response_tx: oneshot::Sender<Result<GraphCommitData>>, } pub struct CommitDataReader { request_tx: smol::channel::Sender<CommitDataRequest>, _task: Task<()>, } impl CommitDataReader { pub async fn read(&self, sha: Oid) -> Result<GraphCommitData> { let (response_tx, response_rx) = oneshot::channel(); self.request_tx .send(CommitDataRequest { sha, response_tx }) .await .map_err(|_| anyhow!("commit data reader task closed"))?; response_rx .await .map_err(|_| anyhow!("commit data reader task dropped response"))? } } fn parse_cat_file_commit(sha: Oid, content: &str) -> Option<GraphCommitData> { let mut parents = SmallVec::new(); let mut author_name = SharedString::default(); let mut author_email = SharedString::default(); let mut commit_timestamp = 0i64; let mut in_headers = true; let mut subject = None; for line in content.lines() { if in_headers { if line.is_empty() { in_headers = false; continue; } if let Some(parent_sha) = line.strip_prefix("parent ") { if let Ok(oid) = Oid::from_str(parent_sha.trim()) { parents.push(oid); } } else if let Some(author_line) = line.strip_prefix("author ") { if let Some((name_email, _timestamp_tz)) = author_line.rsplit_once(' ') { if let Some((name_email, timestamp_str)) = name_email.rsplit_once(' ') { if let Ok(ts) = timestamp_str.parse::<i64>() { commit_timestamp = ts; } if let Some((name, email)) = name_email.rsplit_once(" <") { author_name = SharedString::from(name.to_string()); author_email = SharedString::from(email.trim_end_matches('>').to_string()); } } } } } else if subject.is_none() { subject = Some(SharedString::from(line.to_string())); } } Some(GraphCommitData { sha, parents, author_name, author_email, commit_timestamp, subject: subject.unwrap_or_default(), }) } #[derive(Clone, Debug, Hash, PartialEq, Eq)] pub struct Branch { pub is_head: bool, pub ref_name: SharedString, pub upstream: Option<Upstream>, pub most_recent_commit: Option<CommitSummary>, } impl Branch { pub fn name(&self) -> &str { self.ref_name .as_ref() .strip_prefix("refs/heads/") .or_else(|| self.ref_name.as_ref().strip_prefix("refs/remotes/")) .unwrap_or(self.ref_name.as_ref()) } pub fn is_remote(&self) -> bool { self.ref_name.starts_with("refs/remotes/") } pub fn remote_name(&self) -> Option<&str> { self.ref_name .strip_prefix("refs/remotes/") .and_then(|stripped| stripped.split("/").next()) } pub fn tracking_status(&self) -> Option<UpstreamTrackingStatus> { self.upstream .as_ref() .and_then(|upstream| upstream.tracking.status()) } pub fn priority_key(&self) -> (bool, Option<i64>) { ( self.is_head, self.most_recent_commit .as_ref() .map(|commit| commit.commit_timestamp), ) } } #[derive(Clone, Debug, Hash, PartialEq, Eq)] pub struct Worktree { pub path: PathBuf, pub ref_name: SharedString, pub sha: SharedString, } impl Worktree { pub fn branch(&self) -> &str { self.ref_name .as_ref() .strip_prefix("refs/heads/") .or_else(|| self.ref_name.as_ref().strip_prefix("refs/remotes/")) .unwrap_or(self.ref_name.as_ref()) } } pub fn parse_worktrees_from_str<T: AsRef<str>>(raw_worktrees: T) -> Vec<Worktree> { let mut worktrees = Vec::new(); let normalized = raw_worktrees.as_ref().replace("\r\n", "\n"); let entries = normalized.split("\n\n"); for entry in entries { let mut path = None; let mut sha = None; let mut ref_name = None; for line in entry.lines() { let line = line.trim(); if line.is_empty() { continue; } if let Some(rest) = line.strip_prefix("worktree ") { path = Some(rest.to_string()); } else if let Some(rest) = line.strip_prefix("HEAD ") { sha = Some(rest.to_string()); } else if let Some(rest) = line.strip_prefix("branch ") { ref_name = Some(rest.to_string()); } // Ignore other lines: detached, bare, locked, prunable, etc. } if let (Some(path), Some(sha), Some(ref_name)) = (path, sha, ref_name) { worktrees.push(Worktree { path: PathBuf::from(path), ref_name: ref_name.into(), sha: sha.into(), }) } } worktrees } #[derive(Clone, Debug, Hash, PartialEq, Eq)] pub struct Upstream { pub ref_name: SharedString, pub tracking: UpstreamTracking, } impl Upstream { pub fn is_remote(&self) -> bool { self.remote_name().is_some() } pub fn remote_name(&self) -> Option<&str> { self.ref_name .strip_prefix("refs/remotes/") .and_then(|stripped| stripped.split("/").next()) } pub fn stripped_ref_name(&self) -> Option<&str> { self.ref_name.strip_prefix("refs/remotes/") } pub fn branch_name(&self) -> Option<&str> { self.ref_name .strip_prefix("refs/remotes/") .and_then(|stripped| stripped.split_once('/').map(|(_, name)| name)) } } #[derive(Clone, Copy, Default)] pub struct CommitOptions { pub amend: bool, pub signoff: bool, } #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] pub enum UpstreamTracking { /// Remote ref not present in local repository. Gone, /// Remote ref present in local repository (fetched from remote). Tracked(UpstreamTrackingStatus), } impl From<UpstreamTrackingStatus> for UpstreamTracking { fn from(status: UpstreamTrackingStatus) -> Self { UpstreamTracking::Tracked(status) } } impl UpstreamTracking { pub fn is_gone(&self) -> bool { matches!(self, UpstreamTracking::Gone) } pub fn status(&self) -> Option<UpstreamTrackingStatus> { match self { UpstreamTracking::Gone => None, UpstreamTracking::Tracked(status) => Some(*status), } } } #[derive(Debug, Clone)] pub struct RemoteCommandOutput { pub stdout: String, pub stderr: String, } impl RemoteCommandOutput { pub fn is_empty(&self) -> bool { self.stdout.is_empty() && self.stderr.is_empty() } } #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] pub struct UpstreamTrackingStatus { pub ahead: u32, pub behind: u32, } #[derive(Clone, Debug, Hash, PartialEq, Eq)] pub struct CommitSummary { pub sha: SharedString, pub subject: SharedString, /// This is a unix timestamp pub commit_timestamp: i64, pub author_name: SharedString, pub has_parent: bool, } #[derive(Clone, Debug, Default, Hash, PartialEq, Eq)] pub struct CommitDetails { pub sha: SharedString, pub message: SharedString, pub commit_timestamp: i64, pub author_email: SharedString, pub author_name: SharedString, } #[derive(Clone, Debug, Hash, PartialEq, Eq)] pub struct FileHistoryEntry { pub sha: SharedString, pub subject: SharedString, pub message: SharedString, pub commit_timestamp: i64, pub author_name: SharedString, pub author_email: SharedString, } #[derive(Debug, Clone)] pub struct FileHistory { pub entries: Vec<FileHistoryEntry>, pub path: RepoPath, } #[derive(Debug)] pub struct CommitDiff { pub files: Vec<CommitFile>, } #[derive(Debug)] pub struct CommitFile { pub path: RepoPath, pub old_text: Option<String>, pub new_text: Option<String>, pub is_binary: bool, } impl CommitDetails { pub fn short_sha(&self) -> SharedString { self.sha[..SHORT_SHA_LENGTH].to_string().into() } } /// Detects if content is binary by checking for NUL bytes in the first 8000 bytes. /// This matches git's binary detection heuristic. pub fn is_binary_content(content: &[u8]) -> bool { let check_len = content.len().min(8000); content[..check_len].contains(&0) } #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub struct Remote { pub name: SharedString, } pub enum ResetMode { /// Reset the branch pointer, leave index and worktree unchanged (this will make it look like things that were /// committed are now staged). Soft, /// Reset the branch pointer and index, leave worktree unchanged (this makes it look as though things that were /// committed are now unstaged). Mixed, } #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub enum FetchOptions { All, Remote(Remote), } impl FetchOptions { pub fn to_proto(&self) -> Option<String> { match self { FetchOptions::All => None, FetchOptions::Remote(remote) => Some(remote.clone().name.into()), }