Skip to main content

vcs_runner/
types.rs

1/// Whether a jj commit is the current working copy.
2#[cfg(feature = "jj-parse")]
3#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
4pub enum WorkingCopy {
5    #[default]
6    Background,
7    Current,
8}
9
10/// Whether a jj commit has unresolved conflicts.
11#[cfg(feature = "jj-parse")]
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
13pub enum ConflictState {
14    #[default]
15    Clean,
16    Conflicted,
17}
18
19#[cfg(feature = "jj-parse")]
20impl ConflictState {
21    pub fn is_conflicted(self) -> bool {
22        matches!(self, Self::Conflicted)
23    }
24}
25
26/// Whether a jj commit is empty (no file changes).
27#[cfg(feature = "jj-parse")]
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
29pub enum ContentState {
30    #[default]
31    HasContent,
32    Empty,
33}
34
35#[cfg(feature = "jj-parse")]
36impl ContentState {
37    pub fn is_empty(self) -> bool {
38        matches!(self, Self::Empty)
39    }
40}
41
42/// A jj log entry parsed from jj template JSON output.
43///
44/// Not directly deserializable from jj's JSON — use [`crate::parse_log_output`]
45/// which handles the raw string booleans and populates the enum fields.
46#[cfg(feature = "jj-parse")]
47#[derive(Debug, Clone)]
48pub struct LogEntry {
49    pub commit_id: String,
50    pub change_id: String,
51    pub author_name: String,
52    pub author_email: String,
53    pub description: String,
54    pub parents: Vec<String>,
55    pub local_bookmarks: Vec<String>,
56    pub remote_bookmarks: Vec<String>,
57    pub working_copy: WorkingCopy,
58    pub conflict: ConflictState,
59    pub content: ContentState,
60}
61
62#[cfg(feature = "jj-parse")]
63impl LogEntry {
64    /// The first line of the commit description.
65    pub fn summary(&self) -> &str {
66        self.description.lines().next().unwrap_or("")
67    }
68}
69
70/// Sync status of a bookmark relative to its remote.
71#[cfg(feature = "jj-parse")]
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
73pub enum RemoteStatus {
74    /// Bookmark exists only locally (no non-git remote tracking branch).
75    Local,
76    /// Remote exists but local and remote have diverged.
77    Unsynced,
78    /// Local and remote point to the same commit.
79    Synced,
80}
81
82#[cfg(feature = "jj-parse")]
83impl RemoteStatus {
84    pub fn has_remote(self) -> bool {
85        !matches!(self, Self::Local)
86    }
87
88    pub fn is_synced(self) -> bool {
89        matches!(self, Self::Synced)
90    }
91}
92
93/// A jj bookmark with sync status.
94#[cfg(feature = "jj-parse")]
95#[derive(Debug, Clone, PartialEq, Eq)]
96pub struct Bookmark {
97    pub name: String,
98    pub commit_id: String,
99    pub change_id: String,
100    pub remote: RemoteStatus,
101}
102
103/// A git remote.
104#[cfg(feature = "jj-parse")]
105#[derive(Debug, Clone, PartialEq, Eq)]
106pub struct GitRemote {
107    pub name: String,
108    pub url: String,
109}
110
111/// The kind of change a file underwent in a diff.
112#[cfg(any(feature = "jj-parse", feature = "git-parse"))]
113#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
114pub enum FileChangeKind {
115    Added,
116    Modified,
117    Deleted,
118    Renamed,
119    Copied,
120}
121
122/// A single file change in a diff summary.
123///
124/// `from_path` is populated only for `Renamed` and `Copied` — it records
125/// the previous/source path. `path` is always the resulting path.
126#[cfg(any(feature = "jj-parse", feature = "git-parse"))]
127#[derive(Debug, Clone, PartialEq, Eq)]
128pub struct FileChange {
129    pub kind: FileChangeKind,
130    pub path: std::path::PathBuf,
131    pub from_path: Option<std::path::PathBuf>,
132}
133
134#[cfg(all(test, feature = "jj-parse"))]
135mod tests {
136    use super::*;
137
138    #[test]
139    fn conflict_state() {
140        assert!(!ConflictState::Clean.is_conflicted());
141        assert!(ConflictState::Conflicted.is_conflicted());
142    }
143
144    #[test]
145    fn content_state() {
146        assert!(!ContentState::HasContent.is_empty());
147        assert!(ContentState::Empty.is_empty());
148    }
149
150    #[test]
151    fn working_copy_default() {
152        assert_eq!(WorkingCopy::default(), WorkingCopy::Background);
153    }
154
155    #[test]
156    fn remote_status_local() {
157        assert!(!RemoteStatus::Local.has_remote());
158        assert!(!RemoteStatus::Local.is_synced());
159    }
160
161    #[test]
162    fn remote_status_unsynced() {
163        assert!(RemoteStatus::Unsynced.has_remote());
164        assert!(!RemoteStatus::Unsynced.is_synced());
165    }
166
167    #[test]
168    fn remote_status_synced() {
169        assert!(RemoteStatus::Synced.has_remote());
170        assert!(RemoteStatus::Synced.is_synced());
171    }
172
173    #[test]
174    fn log_entry_summary() {
175        let entry = LogEntry {
176            commit_id: "abc".into(),
177            change_id: "xyz".into(),
178            author_name: "A".into(),
179            author_email: "a@b".into(),
180            description: "first line\nsecond line".into(),
181            parents: vec![],
182            local_bookmarks: vec![],
183            remote_bookmarks: vec![],
184            working_copy: WorkingCopy::Background,
185            conflict: ConflictState::Clean,
186            content: ContentState::HasContent,
187        };
188        assert_eq!(entry.summary(), "first line");
189    }
190
191    #[test]
192    fn log_entry_summary_empty_description() {
193        let entry = LogEntry {
194            commit_id: "abc".into(),
195            change_id: "xyz".into(),
196            author_name: "A".into(),
197            author_email: "a@b".into(),
198            description: String::new(),
199            parents: vec![],
200            local_bookmarks: vec![],
201            remote_bookmarks: vec![],
202            working_copy: WorkingCopy::Background,
203            conflict: ConflictState::Clean,
204            content: ContentState::HasContent,
205        };
206        assert_eq!(entry.summary(), "");
207    }
208}