use std::path::PathBuf;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct WorktreeRemove {
pub path: PathBuf,
pub force: bool,
}
impl WorktreeRemove {
pub fn new(path: impl Into<PathBuf>) -> Self {
Self {
path: path.into(),
force: false,
}
}
pub fn force(mut self) -> Self {
self.force = true;
self
}
}
#[derive(Debug, Clone)]
pub struct WorktreeCreatePartial {
path: PathBuf,
branch: String,
}
impl WorktreeCreatePartial {
pub fn base(self, base: impl Into<String>) -> WorktreeCreate {
WorktreeCreate {
path: self.path,
branch: self.branch,
base: base.into(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct WorktreeCreate {
pub path: PathBuf,
pub branch: String,
pub base: String,
}
impl WorktreeCreate {
#[allow(clippy::new_ret_no_self)]
pub fn new(path: impl Into<PathBuf>, branch: impl Into<String>) -> WorktreeCreatePartial {
WorktreeCreatePartial {
path: path.into(),
branch: branch.into(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct BranchDelete {
pub name: String,
pub force: bool,
}
impl BranchDelete {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
force: false,
}
}
pub fn force(mut self) -> Self {
self.force = true;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub enum BackendKind {
Git,
Jj,
}
impl BackendKind {
pub fn as_str(self) -> &'static str {
match self {
BackendKind::Git => "git",
BackendKind::Jj => "jj",
}
}
}
pub use vcs_diff::ChangeKind;
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub struct FileChange {
pub path: String,
pub old_path: Option<String>,
pub kind: ChangeKind,
}
impl FileChange {
pub fn new(path: impl Into<String>, kind: ChangeKind) -> Self {
Self {
path: path.into(),
old_path: None,
kind,
}
}
pub fn old_path(mut self, old: impl Into<String>) -> Self {
self.old_path = Some(old.into());
self
}
}
pub use vcs_diff::DiffStat;
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub struct WorktreeInfo {
pub path: PathBuf,
pub branch: Option<String>,
pub commit: Option<String>,
pub is_bare: bool,
}
impl WorktreeInfo {
pub fn new(path: impl Into<PathBuf>) -> Self {
Self {
path: path.into(),
branch: None,
commit: None,
is_bare: false,
}
}
pub fn branch(mut self, branch: impl Into<String>) -> Self {
self.branch = Some(branch.into());
self
}
pub fn commit(mut self, commit: impl Into<String>) -> Self {
self.commit = Some(commit.into());
self
}
pub fn bare(mut self) -> Self {
self.is_bare = true;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub enum OperationState {
Clear,
Merge,
Rebase,
ApplyMailbox,
Conflict,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub struct UpstreamTracking {
pub branch: String,
pub ahead: Option<usize>,
pub behind: Option<usize>,
}
impl UpstreamTracking {
pub fn new(branch: impl Into<String>) -> Self {
Self {
branch: branch.into(),
ahead: None,
behind: None,
}
}
pub fn ahead(mut self, n: usize) -> Self {
self.ahead = Some(n);
self
}
pub fn behind(mut self, n: usize) -> Self {
self.behind = Some(n);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub struct RepoSnapshot {
pub head: Option<String>,
pub branch: Option<String>,
pub tracking: Option<UpstreamTracking>,
pub dirty: bool,
pub change_count: usize,
pub conflicted: bool,
pub operation: OperationState,
}
impl RepoSnapshot {
pub fn new() -> Self {
Self {
head: None,
branch: None,
tracking: None,
dirty: false,
change_count: 0,
conflicted: false,
operation: OperationState::Clear,
}
}
pub fn head(mut self, head: impl Into<String>) -> Self {
self.head = Some(head.into());
self
}
pub fn branch(mut self, branch: impl Into<String>) -> Self {
self.branch = Some(branch.into());
self
}
pub fn tracking(mut self, tracking: UpstreamTracking) -> Self {
self.tracking = Some(tracking);
self
}
pub fn dirty(mut self, change_count: usize) -> Self {
self.dirty = true;
self.change_count = change_count;
self
}
pub fn conflicted(mut self) -> Self {
self.conflicted = true;
self
}
pub fn operation(mut self, operation: OperationState) -> Self {
self.operation = operation;
self
}
}
impl Default for RepoSnapshot {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(feature = "serde", serde(tag = "outcome", content = "files"))]
#[non_exhaustive]
pub enum MergeProbe {
Clean,
Conflicts(Vec<String>),
}
impl MergeProbe {
pub fn is_clean(&self) -> bool {
matches!(self, MergeProbe::Clean)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub enum CreateOutcome {
Plain,
CowCloned,
}
#[cfg(all(test, feature = "serde"))]
mod serde_tests {
use super::*;
#[test]
fn snapshot_and_file_change_serialize_to_clean_json() {
let snap = RepoSnapshot {
head: Some("abc".into()),
branch: Some("main".into()),
tracking: Some(UpstreamTracking {
branch: "origin/main".into(),
ahead: Some(1),
behind: Some(0),
}),
dirty: true,
change_count: 2,
conflicted: false,
operation: OperationState::Merge,
};
let v = serde_json::to_value(&snap).unwrap();
assert_eq!(v["branch"], "main");
assert_eq!(v["operation"], "Merge"); assert_eq!(v["change_count"], 2);
assert_eq!(v["tracking"]["branch"], "origin/main");
assert_eq!(v["tracking"]["ahead"], 1);
let fc = FileChange {
path: "a.rs".into(),
old_path: None,
kind: ChangeKind::Added, };
let v = serde_json::to_value(fc).unwrap();
assert_eq!(v["path"], "a.rs");
assert_eq!(v["kind"], "Added");
}
#[test]
fn merge_probe_serializes_to_a_type_stable_object() {
let clean = serde_json::to_value(MergeProbe::Clean).unwrap();
assert_eq!(clean["outcome"], "Clean");
assert!(clean.get("files").is_none(), "{clean}");
let conflicts =
serde_json::to_value(MergeProbe::Conflicts(vec!["a.rs".into(), "b.rs".into()]))
.unwrap();
assert_eq!(conflicts["outcome"], "Conflicts");
assert_eq!(conflicts["files"][0], "a.rs");
assert_eq!(conflicts["files"][1], "b.rs");
}
}
#[cfg(test)]
mod ctor_tests {
use super::*;
#[test]
fn dto_constructors_populate_fields() {
let fc = FileChange::new("new.rs", ChangeKind::Modified).old_path("old.rs");
assert_eq!(fc.path, "new.rs");
assert_eq!(fc.old_path.as_deref(), Some("old.rs"));
assert_eq!(fc.kind, ChangeKind::Modified);
let wt = WorktreeInfo::new("/wt")
.branch("feature")
.commit("abc123")
.bare();
assert_eq!(wt.path, PathBuf::from("/wt"));
assert_eq!(wt.branch.as_deref(), Some("feature"));
assert_eq!(wt.commit.as_deref(), Some("abc123"));
assert!(wt.is_bare);
let up = UpstreamTracking::new("origin/main").ahead(2).behind(3);
assert_eq!(up.branch, "origin/main");
assert_eq!(up.ahead, Some(2));
assert_eq!(up.behind, Some(3));
assert_eq!(UpstreamTracking::new("origin/x").ahead, None);
let snap = RepoSnapshot::new()
.head("deadbeef")
.branch("main")
.tracking(up)
.dirty(4)
.conflicted()
.operation(OperationState::Merge);
assert_eq!(snap.head.as_deref(), Some("deadbeef"));
assert_eq!(snap.branch.as_deref(), Some("main"));
assert_eq!(snap.tracking.as_ref().unwrap().branch, "origin/main");
assert_eq!(snap.tracking.as_ref().unwrap().ahead, Some(2));
assert!(snap.dirty);
assert_eq!(snap.change_count, 4);
assert!(snap.conflicted);
assert_eq!(snap.operation, OperationState::Merge);
let clean = RepoSnapshot::default();
assert!(!clean.dirty && !clean.conflicted && clean.head.is_none());
assert_eq!(clean.operation, OperationState::Clear);
assert_eq!(clean.change_count, 0);
}
}