use std::fmt;
use std::path::{Path, PathBuf};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct GitVersion {
pub major: u32,
pub minor: u32,
pub patch: u32,
}
impl fmt::Display for GitVersion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
}
}
impl GitVersion {
pub fn parse(s: &str) -> Result<Self, crate::error::GitError> {
let version_part = s.trim().strip_prefix("git version ").unwrap_or(s.trim());
let mut parts = version_part.split('.');
let major = parts.next().and_then(|p| p.parse().ok()).ok_or_else(|| {
crate::error::GitError::Parse(format!("invalid major version in '{s}'"))
})?;
let minor = parts.next().and_then(|p| p.parse().ok()).ok_or_else(|| {
crate::error::GitError::Parse(format!("invalid minor version in '{s}'"))
})?;
let patch = parts.next().and_then(|p| p.parse().ok()).unwrap_or(0);
Ok(Self {
major,
minor,
patch,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Oid(String);
impl Oid {
pub fn new(s: impl AsRef<str>) -> Result<Self, crate::error::GitError> {
let s = s.as_ref();
if s.len() < 4 {
return Err(crate::error::GitError::Parse(format!(
"oid too short (minimum 4 chars): {s}"
)));
}
if !s.chars().all(|c| c.is_ascii_hexdigit()) {
return Err(crate::error::GitError::Parse(format!(
"oid contains non-hex characters: {s}"
)));
}
Ok(Self(s.to_lowercase()))
}
pub fn len(&self) -> usize {
self.0.len()
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
impl fmt::Display for Oid {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl AsRef<str> for Oid {
fn as_ref(&self) -> &str {
&self.0
}
}
impl std::str::FromStr for Oid {
type Err = crate::error::GitError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::new(s)
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct GitStatus {
pub staged: Vec<String>,
pub unstaged: Vec<String>,
pub untracked: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct GitLogEntry {
pub sha: String,
pub short_sha: String,
pub message: String,
pub author: String,
pub timestamp: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct GitRemote {
pub name: String,
pub url: String,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct GitMergeResult {
pub has_conflicts: bool,
pub conflict_files: Vec<String>,
pub tree_oid: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct GitWorktree {
pub path: PathBuf,
pub branch: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct GitTag {
pub name: String,
pub sha: String,
pub message: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct GitStash {
pub ref_name: String,
pub sha: String,
pub message: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum ResetMode {
Mixed,
Soft,
Hard,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct GitVerification {
pub valid: bool,
pub signer: Option<String>,
pub fingerprint: Option<String>,
pub status: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct GitSubmodule {
pub sha: String,
pub path: String,
pub describe: Option<String>,
pub dirty: bool,
pub uninitialized: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct GitGrepResult {
pub path: String,
pub line: u32,
pub text: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum DiffLineKind {
Context,
Deletion,
Insertion,
NoNewline,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct DiffLine {
pub kind: DiffLineKind,
pub content: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct DiffHunk {
pub old_start: usize,
pub old_lines: usize,
pub new_start: usize,
pub new_lines: usize,
pub section: String,
pub lines: Vec<DiffLine>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct FileDiff {
pub old_path: Option<String>,
pub new_path: Option<String>,
pub is_binary: bool,
pub mode_changed: bool,
pub old_mode: Option<String>,
pub new_mode: Option<String>,
pub hunks: Vec<DiffHunk>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct BlameLine {
pub commit: String,
pub author: String,
pub author_mail: String,
pub author_time: String,
pub line_no: usize,
pub content: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Patch {
pub commit: Option<String>,
pub subject: Option<String>,
pub from: Option<String>,
pub date: Option<String>,
pub diff: Vec<FileDiff>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct ApplyReport {
pub files_changed: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct ReflogEntry {
pub commit: String,
pub author: String,
pub author_mail: String,
pub timestamp: String,
pub subject: String,
pub designator: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Hook {
pub name: String,
pub path: PathBuf,
pub active: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct HookOutput {
pub stdout: String,
pub stderr: String,
pub exit_code: i32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct BisectState {
pub current: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct BisectResult {
pub found: Option<String>,
pub remaining: usize,
pub log: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct GitNote {
pub object: String,
pub commit: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct GitAttr {
pub path: String,
pub attr: String,
pub value: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct TreeEntry {
pub mode: String,
pub path: String,
pub oid: Oid,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct IndexEntry {
pub path: String,
pub oid: Oid,
pub mode: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum ObjectKind {
Blob,
Tree,
Commit,
Tag,
}
impl std::str::FromStr for ObjectKind {
type Err = crate::error::GitError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"blob" => Ok(Self::Blob),
"tree" => Ok(Self::Tree),
"commit" => Ok(Self::Commit),
"tag" => Ok(Self::Tag),
_ => Err(crate::error::GitError::Parse(format!(
"unknown object kind: {s}"
))),
}
}
}
impl fmt::Display for ObjectKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Blob => write!(f, "blob"),
Self::Tree => write!(f, "tree"),
Self::Commit => write!(f, "commit"),
Self::Tag => write!(f, "tag"),
}
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct ObjectContent {
pub oid: Oid,
pub kind: ObjectKind,
pub size: usize,
pub data: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct GitCommit {
pub tree: Oid,
pub parents: Vec<Oid>,
pub author: String,
pub committer: String,
pub message: String,
}
#[derive(Debug, Clone, Default)]
pub struct PushOptions<'a> {
pub remote: &'a str,
pub branch: &'a str,
pub force: bool,
pub force_with_lease: bool,
pub set_upstream: bool,
}
#[derive(Debug, Clone)]
pub struct CommitOptions<'a> {
pub message: &'a str,
pub paths: &'a [&'a std::path::Path],
pub no_verify: bool,
pub amend: bool,
pub signoff: bool,
}
impl<'a> CommitOptions<'a> {
pub fn new(message: &'a str) -> Self {
Self {
message,
paths: &[],
no_verify: false,
amend: false,
signoff: false,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct RebaseOptions<'a> {
pub branch: &'a str,
pub interactive: bool,
pub autosquash: bool,
pub onto: Option<&'a str>,
}
#[derive(Debug, Clone, Default)]
pub struct MergeOptions<'a> {
pub branch: &'a str,
pub no_edit: bool,
pub no_ff: bool,
pub squash: bool,
}
#[derive(Debug, Clone, Default)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct FetchOptions<'a> {
pub remote: &'a str,
pub prune: bool,
pub tags: bool,
pub depth: Option<usize>,
pub filter: Option<&'a str>,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct CloneOptions<'a> {
pub url: &'a str,
pub path: &'a Path,
pub depth: Option<usize>,
pub branch: Option<&'a str>,
pub filter: Option<&'a str>,
pub bare: bool,
}
impl Default for CloneOptions<'_> {
fn default() -> Self {
Self {
url: "",
path: Path::new("."),
depth: None,
branch: None,
filter: None,
bare: false,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct GitLfsFile {
pub oid: String,
pub path: String,
pub size: Option<u64>,
}
#[derive(Debug, Clone, Default)]
pub struct CherryPickOptions<'a> {
pub commits: &'a [&'a str],
pub no_commit: bool,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_git_version_parse_and_display() {
let v = GitVersion::parse("git version 2.45.1").unwrap();
assert_eq!(v.major, 2);
assert_eq!(v.minor, 45);
assert_eq!(v.patch, 1);
assert_eq!(v.to_string(), "2.45.1");
let v = GitVersion::parse("3.0").unwrap();
assert_eq!(v.to_string(), "3.0.0");
}
#[test]
fn test_git_version_parse_errors() {
let err = GitVersion::parse("not a version").unwrap_err();
assert!(matches!(err, crate::error::GitError::Parse(ref s) if s.contains("major")));
let err = GitVersion::parse("2.not_a_minor").unwrap_err();
assert!(matches!(err, crate::error::GitError::Parse(ref s) if s.contains("minor")));
}
#[test]
fn test_oid_new_and_display() {
let oid = Oid::new("abc123").unwrap();
assert_eq!(oid.to_string(), "abc123");
assert_eq!(oid.len(), 6);
assert!(!oid.is_empty());
assert_eq!(oid.as_ref(), "abc123");
assert_eq!("abc123".parse::<Oid>().unwrap(), oid);
}
#[test]
fn test_oid_new_errors() {
let err = Oid::new("ab").unwrap_err();
assert!(matches!(err, crate::error::GitError::Parse(ref s) if s.contains("too short")));
let err = Oid::new("xyz!").unwrap_err();
assert!(matches!(err, crate::error::GitError::Parse(ref s) if s.contains("non-hex")));
}
#[test]
fn test_object_kind_from_str_and_display() {
assert_eq!("blob".parse::<ObjectKind>().unwrap(), ObjectKind::Blob);
assert_eq!("tree".parse::<ObjectKind>().unwrap(), ObjectKind::Tree);
assert_eq!("commit".parse::<ObjectKind>().unwrap(), ObjectKind::Commit);
assert_eq!("tag".parse::<ObjectKind>().unwrap(), ObjectKind::Tag);
assert_eq!(ObjectKind::Blob.to_string(), "blob");
assert_eq!(ObjectKind::Tree.to_string(), "tree");
assert_eq!(ObjectKind::Commit.to_string(), "commit");
assert_eq!(ObjectKind::Tag.to_string(), "tag");
let err = "unknown".parse::<ObjectKind>().unwrap_err();
assert!(
matches!(err, crate::error::GitError::Parse(ref s) if s.contains("unknown object kind"))
);
}
#[test]
fn test_commit_options_new() {
let opts = CommitOptions::new("msg");
assert_eq!(opts.message, "msg");
assert!(opts.paths.is_empty());
assert!(!opts.no_verify);
}
#[test]
fn test_clone_options_default() {
let opts = CloneOptions::default();
assert_eq!(opts.url, "");
assert_eq!(opts.path, Path::new("."));
assert!(!opts.bare);
}
}