rskit-git 0.2.0-alpha.1

Composable git repository interfaces backed by libgit2
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
//! Shared types for git operations.

use std::fmt;
use std::time::SystemTime;

/// Git object ID (SHA-1 hash, 20 bytes).
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct Oid([u8; 20]);

impl Oid {
    /// Creates an OID from raw bytes.
    pub fn from_bytes(bytes: [u8; 20]) -> Self {
        Self(bytes)
    }

    /// Returns the raw bytes.
    pub fn as_bytes(&self) -> &[u8; 20] {
        &self.0
    }

    /// Reports whether this is the zero OID.
    pub fn is_zero(&self) -> bool {
        self.0 == [0u8; 20]
    }
}

impl fmt::Display for Oid {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        for byte in &self.0 {
            write!(f, "{byte:02x}")?;
        }
        Ok(())
    }
}

impl fmt::Debug for Oid {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "Oid({self})")
    }
}

/// OID of a tree object for content-addressed comparison.
pub type TreeHash = Oid;

/// A git reference (branch, tag, or HEAD).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Reference {
    /// Fully qualified or symbolic reference name.
    pub name: String,
    /// Target object ID.
    pub target: Oid,
    /// Whether this reference is a branch.
    pub is_branch: bool,
    /// Whether this reference is a tag.
    pub is_tag: bool,
}

/// Author or committer identity.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Signature {
    /// Display name.
    pub name: String,
    /// Email address.
    pub email: String,
    /// Timestamp of the signature.
    pub when: SystemTime,
}

/// A git commit object.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Commit {
    /// Commit object ID.
    pub oid: Oid,
    /// Author identity.
    pub author: Signature,
    /// Committer identity.
    pub committer: Signature,
    /// Commit message.
    pub message: String,
    /// Parent commit IDs.
    pub parents: Vec<Oid>,
}

/// How a file changed in a diff.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum FileStatus {
    /// File was added.
    Added,
    /// File content changed.
    Modified,
    /// File was removed.
    Deleted,
    /// File was renamed.
    Renamed,
    /// File was copied.
    Copied,
    /// File is untracked.
    Untracked,
    /// File is ignored.
    Ignored,
    /// File type changed.
    TypeChanged,
    /// File has merge conflicts.
    Conflicted,
}

impl fmt::Display for FileStatus {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Added => write!(f, "added"),
            Self::Modified => write!(f, "modified"),
            Self::Deleted => write!(f, "deleted"),
            Self::Renamed => write!(f, "renamed"),
            Self::Copied => write!(f, "copied"),
            Self::Untracked => write!(f, "untracked"),
            Self::Ignored => write!(f, "ignored"),
            Self::TypeChanged => write!(f, "type_changed"),
            Self::Conflicted => write!(f, "conflicted"),
        }
    }
}

/// A single file change between two refs.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DiffEntry {
    /// Path of the changed file in the new tree.
    pub path: String,
    /// Previous path when renamed or copied.
    pub old_path: Option<String>,
    /// Previous object ID.
    pub old_oid: Oid,
    /// New object ID.
    pub new_oid: Oid,
    /// Kind of change.
    pub status: FileStatus,
}

/// Aggregated diff statistics.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct DiffStats {
    /// Lines added.
    pub additions: usize,
    /// Lines deleted.
    pub deletions: usize,
    /// Number of changed files.
    pub files_changed: usize,
}

/// A file's state in the working tree or index.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum EntryState {
    /// Changes staged in the index.
    Staged,
    /// Changes present only in the working tree.
    Unstaged,
    /// Path not tracked by git.
    Untracked,
    /// Path has merge conflicts.
    Conflicted,
}

impl fmt::Display for EntryState {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Staged => write!(f, "staged"),
            Self::Unstaged => write!(f, "unstaged"),
            Self::Untracked => write!(f, "untracked"),
            Self::Conflicted => write!(f, "conflicted"),
        }
    }
}

/// A file's status in the working tree.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StatusEntry {
    /// Repository-relative file path.
    pub path: String,
    /// Current working tree or index state.
    pub state: EntryState,
}

/// A file entry in the git index.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IndexEntry {
    /// Repository-relative file path.
    pub path: String,
    /// Object ID stored in the index.
    pub oid: Oid,
    /// Entry kind.
    pub kind: EntryKind,
    /// Raw git file mode.
    pub filemode: u32,
}

/// Type of a tree entry.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum EntryKind {
    /// Regular file/blob entry.
    Blob,
    /// Nested tree/directory entry.
    Tree,
    /// Git submodule entry.
    Submodule,
}

impl fmt::Display for EntryKind {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Blob => write!(f, "blob"),
            Self::Tree => write!(f, "tree"),
            Self::Submodule => write!(f, "submodule"),
        }
    }
}

/// An entry within a git tree object.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TreeEntry {
    /// Entry name relative to its parent tree.
    pub name: String,
    /// Object ID of the entry.
    pub oid: Oid,
    /// Entry kind.
    pub kind: EntryKind,
    /// Raw git file mode.
    pub filemode: u32,
}

/// Branch metadata.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Branch {
    /// Branch name.
    pub name: String,
    /// Tip commit ID.
    pub target: Oid,
    /// Upstream tracking branch (for example `origin/main`).
    pub upstream: Option<String>,
}

/// Tag metadata.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Tag {
    /// Tag name.
    pub name: String,
    /// Target object ID.
    pub target: Oid,
    /// Tagger signature (`None` for lightweight tags).
    pub tagger: Option<Signature>,
    /// Annotation message (`""` for lightweight tags).
    pub message: String,
}

/// Remote repository metadata.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Remote {
    /// Remote name.
    pub name: String,
    /// Remote URL.
    pub url: String,
    /// Fetch refspecs.
    pub fetch_specs: Vec<String>,
    /// Push refspecs.
    pub push_specs: Vec<String>,
}

/// Line-level attribution from `git blame`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BlameLine {
    /// One-based line number.
    pub line: usize,
    /// Commit that last changed the line.
    pub commit_oid: Oid,
    /// Author of the blamed line.
    pub author: Signature,
    /// Full line content.
    pub content: String,
}

/// A match returned from `git grep`-style repository inspection.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GrepMatch {
    /// Repository-relative file path.
    pub path: String,
    /// One-based line number, or `None` when line numbers were not requested.
    pub line_number: Option<usize>,
    /// Raw matching line content.
    pub line: String,
}

/// Information about a stash entry.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StashEntry {
    /// Zero-based stash index.
    pub index: usize,
    /// Stash commit OID when known.
    pub oid: Oid,
    /// Human-readable stash message.
    pub message: String,
}

/// Result returned from merge operations.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct MergeResult {
    /// The resulting HEAD OID when available.
    pub head: Option<Oid>,
    /// Whether the merge completed as a fast-forward.
    pub fast_forward: bool,
    /// Conflicting paths produced by the merge.
    pub conflicts: Vec<String>,
}

/// Result returned from rebase operations.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct RebaseResult {
    /// The resulting HEAD OID when available.
    pub head: Option<Oid>,
    /// Number of commits applied during the rebase.
    pub applied: usize,
    /// Conflicting paths encountered during the rebase.
    pub conflicts: Vec<String>,
}

/// Controls which branches to list.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum BranchFilter {
    /// Only local branches.
    #[default]
    Local,
    /// Only remote branches.
    Remote,
    /// Both local and remote branches.
    All,
}

/// Controls repository reset behavior.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum ResetMode {
    /// Reset HEAD and index, preserving worktree changes.
    #[default]
    Mixed,
    /// Reset HEAD only.
    Soft,
    /// Reset HEAD, index, and worktree.
    Hard,
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn oid_display_debug_and_zero_detection_are_stable() {
        let zero = Oid::from_bytes([0; 20]);
        let mut bytes = [0; 20];
        bytes[0] = 0xab;
        bytes[19] = 0x05;
        let oid = Oid::from_bytes(bytes);

        assert!(zero.is_zero());
        assert!(!oid.is_zero());
        assert_eq!(oid.as_bytes(), &bytes);
        assert_eq!(oid.to_string(), "ab00000000000000000000000000000000000005");
        assert_eq!(
            format!("{oid:?}"),
            "Oid(ab00000000000000000000000000000000000005)"
        );
    }

    #[test]
    fn enum_display_values_match_public_contract() {
        assert_eq!(FileStatus::Added.to_string(), "added");
        assert_eq!(FileStatus::Modified.to_string(), "modified");
        assert_eq!(FileStatus::Deleted.to_string(), "deleted");
        assert_eq!(FileStatus::Renamed.to_string(), "renamed");
        assert_eq!(FileStatus::Copied.to_string(), "copied");
        assert_eq!(FileStatus::Untracked.to_string(), "untracked");
        assert_eq!(FileStatus::Ignored.to_string(), "ignored");
        assert_eq!(FileStatus::TypeChanged.to_string(), "type_changed");
        assert_eq!(FileStatus::Conflicted.to_string(), "conflicted");

        assert_eq!(EntryState::Staged.to_string(), "staged");
        assert_eq!(EntryState::Unstaged.to_string(), "unstaged");
        assert_eq!(EntryState::Untracked.to_string(), "untracked");
        assert_eq!(EntryState::Conflicted.to_string(), "conflicted");

        assert_eq!(EntryKind::Blob.to_string(), "blob");
        assert_eq!(EntryKind::Tree.to_string(), "tree");
        assert_eq!(EntryKind::Submodule.to_string(), "submodule");
    }

    #[test]
    fn default_result_types_are_empty_and_non_destructive() {
        assert_eq!(DiffStats::default().files_changed, 0);
        assert_eq!(BranchFilter::default(), BranchFilter::Local);
        assert_eq!(ResetMode::default(), ResetMode::Mixed);
        assert_eq!(MergeResult::default().conflicts, Vec::<String>::new());
        assert_eq!(RebaseResult::default().applied, 0);
    }
}