Skip to main content

vcs_runner/
types.rs

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