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