Skip to main content

rskit_git/
types.rs

1//! Shared types for git operations.
2
3use std::fmt;
4use std::time::SystemTime;
5
6/// Git object ID (SHA-1 hash, 20 bytes).
7#[derive(Clone, Copy, PartialEq, Eq, Hash)]
8pub struct Oid([u8; 20]);
9
10impl Oid {
11    /// Creates an OID from raw bytes.
12    pub fn from_bytes(bytes: [u8; 20]) -> Self {
13        Self(bytes)
14    }
15
16    /// Returns the raw bytes.
17    pub fn as_bytes(&self) -> &[u8; 20] {
18        &self.0
19    }
20
21    /// Reports whether this is the zero OID.
22    pub fn is_zero(&self) -> bool {
23        self.0 == [0u8; 20]
24    }
25}
26
27impl fmt::Display for Oid {
28    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29        for byte in &self.0 {
30            write!(f, "{byte:02x}")?;
31        }
32        Ok(())
33    }
34}
35
36impl fmt::Debug for Oid {
37    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38        write!(f, "Oid({self})")
39    }
40}
41
42/// OID of a tree object for content-addressed comparison.
43pub type TreeHash = Oid;
44
45/// A git reference (branch, tag, or HEAD).
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct Reference {
48    /// Fully qualified or symbolic reference name.
49    pub name: String,
50    /// Target object ID.
51    pub target: Oid,
52    /// Whether this reference is a branch.
53    pub is_branch: bool,
54    /// Whether this reference is a tag.
55    pub is_tag: bool,
56}
57
58/// Author or committer identity.
59#[derive(Debug, Clone, PartialEq, Eq)]
60pub struct Signature {
61    /// Display name.
62    pub name: String,
63    /// Email address.
64    pub email: String,
65    /// Timestamp of the signature.
66    pub when: SystemTime,
67}
68
69/// A git commit object.
70#[derive(Debug, Clone, PartialEq, Eq)]
71pub struct Commit {
72    /// Commit object ID.
73    pub oid: Oid,
74    /// Author identity.
75    pub author: Signature,
76    /// Committer identity.
77    pub committer: Signature,
78    /// Commit message.
79    pub message: String,
80    /// Parent commit IDs.
81    pub parents: Vec<Oid>,
82}
83
84/// How a file changed in a diff.
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86#[non_exhaustive]
87pub enum FileStatus {
88    /// File was added.
89    Added,
90    /// File content changed.
91    Modified,
92    /// File was removed.
93    Deleted,
94    /// File was renamed.
95    Renamed,
96    /// File was copied.
97    Copied,
98    /// File is untracked.
99    Untracked,
100    /// File is ignored.
101    Ignored,
102    /// File type changed.
103    TypeChanged,
104    /// File has merge conflicts.
105    Conflicted,
106}
107
108impl fmt::Display for FileStatus {
109    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
110        match self {
111            Self::Added => write!(f, "added"),
112            Self::Modified => write!(f, "modified"),
113            Self::Deleted => write!(f, "deleted"),
114            Self::Renamed => write!(f, "renamed"),
115            Self::Copied => write!(f, "copied"),
116            Self::Untracked => write!(f, "untracked"),
117            Self::Ignored => write!(f, "ignored"),
118            Self::TypeChanged => write!(f, "type_changed"),
119            Self::Conflicted => write!(f, "conflicted"),
120        }
121    }
122}
123
124/// A single file change between two refs.
125#[derive(Debug, Clone, PartialEq, Eq)]
126pub struct DiffEntry {
127    /// Path of the changed file in the new tree.
128    pub path: String,
129    /// Previous path when renamed or copied.
130    pub old_path: Option<String>,
131    /// Previous object ID.
132    pub old_oid: Oid,
133    /// New object ID.
134    pub new_oid: Oid,
135    /// Kind of change.
136    pub status: FileStatus,
137}
138
139/// Aggregated diff statistics.
140#[derive(Debug, Clone, Default, PartialEq, Eq)]
141pub struct DiffStats {
142    /// Lines added.
143    pub additions: usize,
144    /// Lines deleted.
145    pub deletions: usize,
146    /// Number of changed files.
147    pub files_changed: usize,
148}
149
150/// A file's state in the working tree or index.
151#[derive(Debug, Clone, Copy, PartialEq, Eq)]
152#[non_exhaustive]
153pub enum EntryState {
154    /// Changes staged in the index.
155    Staged,
156    /// Changes present only in the working tree.
157    Unstaged,
158    /// Path not tracked by git.
159    Untracked,
160    /// Path has merge conflicts.
161    Conflicted,
162}
163
164impl fmt::Display for EntryState {
165    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
166        match self {
167            Self::Staged => write!(f, "staged"),
168            Self::Unstaged => write!(f, "unstaged"),
169            Self::Untracked => write!(f, "untracked"),
170            Self::Conflicted => write!(f, "conflicted"),
171        }
172    }
173}
174
175/// A file's status in the working tree.
176#[derive(Debug, Clone, PartialEq, Eq)]
177pub struct StatusEntry {
178    /// Repository-relative file path.
179    pub path: String,
180    /// Current working tree or index state.
181    pub state: EntryState,
182}
183
184/// A file entry in the git index.
185#[derive(Debug, Clone, PartialEq, Eq)]
186pub struct IndexEntry {
187    /// Repository-relative file path.
188    pub path: String,
189    /// Object ID stored in the index.
190    pub oid: Oid,
191    /// Entry kind.
192    pub kind: EntryKind,
193    /// Raw git file mode.
194    pub filemode: u32,
195}
196
197/// Type of a tree entry.
198#[derive(Debug, Clone, Copy, PartialEq, Eq)]
199#[non_exhaustive]
200pub enum EntryKind {
201    /// Regular file/blob entry.
202    Blob,
203    /// Nested tree/directory entry.
204    Tree,
205    /// Git submodule entry.
206    Submodule,
207}
208
209impl fmt::Display for EntryKind {
210    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
211        match self {
212            Self::Blob => write!(f, "blob"),
213            Self::Tree => write!(f, "tree"),
214            Self::Submodule => write!(f, "submodule"),
215        }
216    }
217}
218
219/// An entry within a git tree object.
220#[derive(Debug, Clone, PartialEq, Eq)]
221pub struct TreeEntry {
222    /// Entry name relative to its parent tree.
223    pub name: String,
224    /// Object ID of the entry.
225    pub oid: Oid,
226    /// Entry kind.
227    pub kind: EntryKind,
228    /// Raw git file mode.
229    pub filemode: u32,
230}
231
232/// Branch metadata.
233#[derive(Debug, Clone, PartialEq, Eq)]
234pub struct Branch {
235    /// Branch name.
236    pub name: String,
237    /// Tip commit ID.
238    pub target: Oid,
239    /// Upstream tracking branch (for example `origin/main`).
240    pub upstream: Option<String>,
241}
242
243/// Tag metadata.
244#[derive(Debug, Clone, PartialEq, Eq)]
245pub struct Tag {
246    /// Tag name.
247    pub name: String,
248    /// Target object ID.
249    pub target: Oid,
250    /// Tagger signature (`None` for lightweight tags).
251    pub tagger: Option<Signature>,
252    /// Annotation message (`""` for lightweight tags).
253    pub message: String,
254}
255
256/// Remote repository metadata.
257#[derive(Debug, Clone, PartialEq, Eq)]
258pub struct Remote {
259    /// Remote name.
260    pub name: String,
261    /// Remote URL.
262    pub url: String,
263    /// Fetch refspecs.
264    pub fetch_specs: Vec<String>,
265    /// Push refspecs.
266    pub push_specs: Vec<String>,
267}
268
269/// Line-level attribution from `git blame`.
270#[derive(Debug, Clone, PartialEq, Eq)]
271pub struct BlameLine {
272    /// One-based line number.
273    pub line: usize,
274    /// Commit that last changed the line.
275    pub commit_oid: Oid,
276    /// Author of the blamed line.
277    pub author: Signature,
278    /// Full line content.
279    pub content: String,
280}
281
282/// A match returned from `git grep`-style repository inspection.
283#[derive(Debug, Clone, PartialEq, Eq)]
284pub struct GrepMatch {
285    /// Repository-relative file path.
286    pub path: String,
287    /// One-based line number, or `None` when line numbers were not requested.
288    pub line_number: Option<usize>,
289    /// Raw matching line content.
290    pub line: String,
291}
292
293/// Information about a stash entry.
294#[derive(Debug, Clone, PartialEq, Eq)]
295pub struct StashEntry {
296    /// Zero-based stash index.
297    pub index: usize,
298    /// Stash commit OID when known.
299    pub oid: Oid,
300    /// Human-readable stash message.
301    pub message: String,
302}
303
304/// Result returned from merge operations.
305#[derive(Debug, Clone, Default, PartialEq, Eq)]
306pub struct MergeResult {
307    /// The resulting HEAD OID when available.
308    pub head: Option<Oid>,
309    /// Whether the merge completed as a fast-forward.
310    pub fast_forward: bool,
311    /// Conflicting paths produced by the merge.
312    pub conflicts: Vec<String>,
313}
314
315/// Result returned from rebase operations.
316#[derive(Debug, Clone, Default, PartialEq, Eq)]
317pub struct RebaseResult {
318    /// The resulting HEAD OID when available.
319    pub head: Option<Oid>,
320    /// Number of commits applied during the rebase.
321    pub applied: usize,
322    /// Conflicting paths encountered during the rebase.
323    pub conflicts: Vec<String>,
324}
325
326/// Controls which branches to list.
327#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
328#[non_exhaustive]
329pub enum BranchFilter {
330    /// Only local branches.
331    #[default]
332    Local,
333    /// Only remote branches.
334    Remote,
335    /// Both local and remote branches.
336    All,
337}
338
339/// Controls repository reset behavior.
340#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
341#[non_exhaustive]
342pub enum ResetMode {
343    /// Reset HEAD and index, preserving worktree changes.
344    #[default]
345    Mixed,
346    /// Reset HEAD only.
347    Soft,
348    /// Reset HEAD, index, and worktree.
349    Hard,
350}
351
352#[cfg(test)]
353mod tests {
354    use super::*;
355
356    #[test]
357    fn oid_display_debug_and_zero_detection_are_stable() {
358        let zero = Oid::from_bytes([0; 20]);
359        let mut bytes = [0; 20];
360        bytes[0] = 0xab;
361        bytes[19] = 0x05;
362        let oid = Oid::from_bytes(bytes);
363
364        assert!(zero.is_zero());
365        assert!(!oid.is_zero());
366        assert_eq!(oid.as_bytes(), &bytes);
367        assert_eq!(oid.to_string(), "ab00000000000000000000000000000000000005");
368        assert_eq!(
369            format!("{oid:?}"),
370            "Oid(ab00000000000000000000000000000000000005)"
371        );
372    }
373
374    #[test]
375    fn enum_display_values_match_public_contract() {
376        assert_eq!(FileStatus::Added.to_string(), "added");
377        assert_eq!(FileStatus::Modified.to_string(), "modified");
378        assert_eq!(FileStatus::Deleted.to_string(), "deleted");
379        assert_eq!(FileStatus::Renamed.to_string(), "renamed");
380        assert_eq!(FileStatus::Copied.to_string(), "copied");
381        assert_eq!(FileStatus::Untracked.to_string(), "untracked");
382        assert_eq!(FileStatus::Ignored.to_string(), "ignored");
383        assert_eq!(FileStatus::TypeChanged.to_string(), "type_changed");
384        assert_eq!(FileStatus::Conflicted.to_string(), "conflicted");
385
386        assert_eq!(EntryState::Staged.to_string(), "staged");
387        assert_eq!(EntryState::Unstaged.to_string(), "unstaged");
388        assert_eq!(EntryState::Untracked.to_string(), "untracked");
389        assert_eq!(EntryState::Conflicted.to_string(), "conflicted");
390
391        assert_eq!(EntryKind::Blob.to_string(), "blob");
392        assert_eq!(EntryKind::Tree.to_string(), "tree");
393        assert_eq!(EntryKind::Submodule.to_string(), "submodule");
394    }
395
396    #[test]
397    fn default_result_types_are_empty_and_non_destructive() {
398        assert_eq!(DiffStats::default().files_changed, 0);
399        assert_eq!(BranchFilter::default(), BranchFilter::Local);
400        assert_eq!(ResetMode::default(), ResetMode::Mixed);
401        assert_eq!(MergeResult::default().conflicts, Vec::<String>::new());
402        assert_eq!(RebaseResult::default().applied, 0);
403    }
404}