use std::collections::HashMap;
use std::path::{Path, PathBuf};
use thiserror::Error;
use crate::diffalgo::DiffAlgorithm;
use crate::model::{DiffModel, HunkId};
#[derive(Debug, Error)]
pub enum VcsError {
#[error(transparent)]
Git(#[from] git2::Error),
#[error("repository has no working directory")]
NoWorkdir,
#[error("{0}")]
Rejected(String),
#[error(transparent)]
Io(#[from] std::io::Error),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HeadInfo {
pub branch: Option<String>,
pub oid7: String,
pub subject: String,
pub upstream: Option<String>,
pub ahead: usize,
pub behind: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LogEntry {
pub oid: String,
pub oid7: String,
pub refs: Vec<String>,
pub parents: Vec<String>,
pub subject: String,
pub author: String,
pub time_unix: i64,
pub change_id: Option<String>,
}
impl LogEntry {
pub fn short_id(&self) -> &str {
self.change_id.as_deref().unwrap_or(&self.oid7)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ChangeInfo {
pub change_id: String,
pub commit_id: String,
pub bookmark: Option<String>,
pub description: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ChangeOp {
New(String),
Edit(String),
Describe {
rev: String,
message: String,
},
Squash(String),
Abandon(String),
Undo,
Redo,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PullTarget {
pub bookmark: String,
pub remote: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PullOutcome {
Gone { onto: String },
UpToDate { onto: String },
Rebased {
onto: String,
conflicts: usize,
bookmark_conflicted: bool,
},
}
pub const JJ_ONLY: &str = "edit changes in a jj repo; this one is plain git";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Bookmark {
pub name: String,
pub oid: String,
pub remote: bool,
pub tip_unix: i64,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct JjView {
pub change: Option<ChangeInfo>,
pub base: Option<String>,
pub bookmarks: Vec<Bookmark>,
pub change_ids: HashMap<String, String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BranchInfo {
pub name: String,
pub is_head: bool,
pub tip_unix: i64,
pub divergence: Option<(usize, usize)>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum NetworkOp {
Fetch,
FetchAll,
Push,
PushSetUpstream { remote: String },
Pull,
PullFrom { remote: String, branch: String },
PullRebase,
PullMerge,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BlameSpan {
pub start_line: u32,
pub line_count: u32,
pub oid: String,
pub oid7: String,
pub author: String,
pub time_unix: i64,
pub summary: String,
pub committed: bool,
}
#[derive(Debug, Clone, Default)]
pub struct StatusModel {
pub untracked: DiffModel,
pub unstaged: DiffModel,
pub staged: DiffModel,
}
pub trait Vcs: Send {
fn git_dir(&self) -> Result<PathBuf, VcsError>;
fn head(&self) -> Result<HeadInfo, VcsError>;
fn read_jj_view(&self, _log_limit: usize) -> Result<Option<JjView>, VcsError> {
Ok(None)
}
fn adopt_jj_view(&self, _view: JjView) {}
fn has_index(&self) -> bool;
fn status(&self) -> Result<StatusModel, VcsError>;
fn working_tree_diff(&self) -> Result<DiffModel, VcsError>;
fn tree_to_workdir_diff(&self, base_oid: &str) -> Result<DiffModel, VcsError>;
fn commit_diff(&self, oid: &str) -> Result<DiffModel, VcsError>;
fn range_diff(&self, oldest_oid: &str, newest_oid: &str) -> Result<DiffModel, VcsError>;
fn tree_diff(&self, base_oid: &str, newest_oid: &str) -> Result<DiffModel, VcsError>;
fn merge_base(&self, a: &str, b: &str) -> Result<String, VcsError>;
fn resolve(&self, revision: &str) -> Result<String, VcsError>;
fn log(&self, limit: usize, all_branches: bool) -> Result<Vec<LogEntry>, VcsError>;
fn default_branch(&self, remote: &str) -> Result<Option<String>, VcsError>;
fn commits_between(&self, base: &str, head: &str) -> Result<Vec<LogEntry>, VcsError>;
fn unpushed(&self, limit: usize) -> Result<Option<Vec<LogEntry>>, VcsError>;
fn blame(&self, rel: &Path) -> Result<Vec<BlameSpan>, VcsError>;
fn read_at(&self, rev: &str, path: &str) -> Result<Option<String>, VcsError>;
fn read_blob(&self, oid: &str) -> Result<Option<Vec<u8>>, VcsError>;
fn tracked_files(&self) -> Result<Vec<PathBuf>, VcsError>;
fn attr(&self, rel: &Path, name: &str) -> bool;
fn branches(&self) -> Result<Vec<BranchInfo>, VcsError>;
fn divergence(&self, branch: &str) -> Result<Option<(usize, usize)>, VcsError>;
fn all_branches(&self) -> Result<Vec<String>, VcsError>;
fn stage(&self, rel: &Path) -> Result<(), VcsError>;
fn stage_everything(&self) -> Result<(), VcsError>;
fn unstage_everything(&self) -> Result<(), VcsError>;
fn stage_hunk(&self, rel: &Path, hunk: &HunkId) -> Result<(), VcsError>;
fn unstage(&self, rel: &Path) -> Result<(), VcsError>;
fn unstage_hunk(&self, rel: &Path, hunk: &HunkId) -> Result<(), VcsError>;
fn discard(&self, rel: &Path) -> Result<(), VcsError>;
fn commit(&self, message: &str) -> Result<String, VcsError>;
fn commit_paths(&self, _message: &str, _paths: &[String]) -> Result<String, VcsError> {
Err(VcsError::Rejected(
"stage the files you want, then commit".into(),
))
}
fn change(&self, _op: &ChangeOp) -> Result<(), VcsError> {
Err(VcsError::Rejected(JJ_ONLY.into()))
}
fn pull_target(&self) -> Result<Option<PullTarget>, VcsError> {
Ok(None)
}
fn rebase_onto_remote(&self, _bookmark: &str, _remote: &str) -> Result<PullOutcome, VcsError> {
Err(VcsError::Rejected(JJ_ONLY.into()))
}
fn commit_message(&self, oid: &str) -> Result<String, VcsError>;
fn head_message(&self) -> Result<String, VcsError>;
fn amend(&self, message: Option<&str>, use_index: bool) -> Result<String, VcsError>;
fn create_branch(&self, name: &str, checkout: bool) -> Result<(), VcsError>;
fn delete_branch(&self, name: &str) -> Result<(), VcsError>;
fn checkout(&self, name: &str) -> Result<(), VcsError>;
fn native_git_checkout(&self) -> bool;
fn stash_push(&self, message: Option<&str>) -> Result<(), VcsError>;
fn stash_pop(&self) -> Result<(), VcsError>;
fn network_argv(&self, op: NetworkOp) -> Result<Vec<String>, VcsError>;
fn workdir(&self) -> Result<PathBuf, VcsError>;
fn remote_url(&self, name: &str) -> Result<Option<String>, VcsError>;
fn remotes(&self) -> Result<Vec<String>, VcsError>;
fn set_diff_algorithm(&self, algorithm: DiffAlgorithm, indent_heuristic: bool);
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct CommitLists {
pub unpushed: Option<Vec<LogEntry>>,
pub recent: Vec<LogEntry>,
}
pub fn commit_lists(
vcs: &dyn Vcs,
recent_limit: usize,
unpushed_limit: usize,
) -> Result<CommitLists, VcsError> {
let unpushed = vcs.unpushed(unpushed_limit)?;
let ahead = unpushed.as_deref().unwrap_or_default();
let mut recent = vcs.log(recent_limit + ahead.len(), false)?;
recent.retain(|entry| !ahead.iter().any(|local| local.oid == entry.oid));
recent.truncate(recent_limit);
Ok(CommitLists { unpushed, recent })
}
pub fn against_diff(vcs: &dyn Vcs, rev: &str) -> Result<DiffModel, VcsError> {
let head = vcs.resolve("HEAD")?;
let base = vcs.merge_base(&vcs.resolve(rev)?, &head)?;
vcs.tree_to_workdir_diff(&base)
}