Skip to main content

grit_lib/
push_report.rs

1//! Per-reference push results.
2//!
3//! After a push, each requested reference update resolves to one
4//! [`PushRefStatus`] (up to date, updated, or one of the rejection reasons),
5//! carried with its old and new values in a [`PushRefResult`]. Callers render
6//! these themselves; `grit push` maps them to its human, JSON and Markdown output.
7
8use crate::objects::ObjectId;
9
10/// The resolved outcome of a single reference update during a push.
11#[derive(Clone, Debug, PartialEq, Eq)]
12#[cfg_attr(feature = "serde-models", derive(serde::Serialize))]
13#[cfg_attr(feature = "serde-models", serde(rename_all = "snake_case"))]
14pub enum PushRefStatus {
15    /// The reference was already at the requested value (`=`, `[up to date]`).
16    UpToDate,
17    /// A successful update (`*` new, `-` delete, ` ` fast-forward, `+` forced).
18    Ok,
19    /// Client-side rejection: the update is not a fast-forward and `--force`
20    /// was not given (`!`, `[rejected] (non-fast-forward)`).
21    RejectNonFastForward,
22    /// Rejected because the new ref already exists (`!`, `[rejected] (already exists)`).
23    RejectAlreadyExists,
24    /// Rejected because the remote has the ref but we need to fetch first.
25    RejectFetchFirst,
26    /// Rejected because a forced update is required.
27    RejectNeedsForce,
28    /// Rejected because force-with-lease found stale info.
29    RejectStale,
30    /// The remote `receive-pack` declined the update (`!`, `[remote rejected] (<reason>)`).
31    RemoteRejected,
32    /// Client-side rejection: a delete was requested but the ref is absent on the remote
33    /// (`!`, `[rejected] (remote ref does not exist)`).
34    RejectRemoteRefMissing,
35    /// Part of an atomic push that failed because another ref was rejected
36    /// (`!`, `[rejected] (atomic push failed)`).
37    AtomicPushFailed,
38}
39
40impl PushRefStatus {
41    /// Whether this status represents a hard failure (causes a non-zero exit).
42    #[must_use]
43    pub fn is_error(&self) -> bool {
44        !matches!(self, PushRefStatus::UpToDate | PushRefStatus::Ok)
45    }
46}
47
48/// One reference's resolved push result, ready for display.
49#[derive(Clone, Debug)]
50#[cfg_attr(feature = "serde-models", derive(serde::Serialize))]
51pub struct PushRefResult {
52    /// The local ref name (source side), e.g. `refs/heads/next`. `None` for deletions.
53    pub local_ref: Option<String>,
54    /// The remote ref name (destination side), e.g. `refs/heads/next`.
55    pub remote_ref: String,
56    /// Old value of the remote ref (`None`/zero for a new ref).
57    pub old_oid: Option<ObjectId>,
58    /// New value of the remote ref (`None`/zero for a deletion).
59    pub new_oid: Option<ObjectId>,
60    /// Whether this update was a forced (non-fast-forward but `--force`d) update.
61    pub forced: bool,
62    /// Whether this update deletes the remote ref.
63    pub deletion: bool,
64    /// The resolved status.
65    pub status: PushRefStatus,
66    /// Extra reason text (used for `[remote rejected]`).
67    pub message: Option<String>,
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73
74    #[test]
75    fn only_up_to_date_and_ok_are_successes() {
76        assert!(!PushRefStatus::UpToDate.is_error());
77        assert!(!PushRefStatus::Ok.is_error());
78        for status in [
79            PushRefStatus::RejectNonFastForward,
80            PushRefStatus::RejectAlreadyExists,
81            PushRefStatus::RejectFetchFirst,
82            PushRefStatus::RejectNeedsForce,
83            PushRefStatus::RejectStale,
84            PushRefStatus::RemoteRejected,
85            PushRefStatus::RejectRemoteRefMissing,
86            PushRefStatus::AtomicPushFailed,
87        ] {
88            assert!(status.is_error(), "{status:?}");
89        }
90    }
91}