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
//! Status output parser (jj status)
use super::super::JjError;
use crate::model::{ChangeId, FileState, FileStatus, Status};
use super::Parser;
impl Parser {
/// Parse `jj status` output
pub fn parse_status(output: &str) -> Result<Status, JjError> {
let mut files = Vec::new();
let mut has_conflicts = false;
let mut working_copy_change_id = ChangeId::default();
let mut parent_change_id = ChangeId::default();
for line in output.lines() {
let line = line.trim();
// Parse file status lines
if let Some(file_status) = Self::parse_status_line(line) {
if matches!(file_status.state, FileState::Conflicted) {
has_conflicts = true;
}
files.push(file_status);
}
// Parse working copy info
// Format: "Working copy (@) : <change_id> <commit_id> <description>"
if line.starts_with("Working copy")
&& let Some(colon_pos) = line.find(": ")
{
let info = &line[colon_pos + 2..];
if let Some(change_id) = info.split_whitespace().next() {
working_copy_change_id = ChangeId::new(change_id.to_string());
}
}
// Parse parent commit info
// Format: "Parent commit (@-): <change_id> <commit_id> <description>"
if line.starts_with("Parent commit")
&& let Some(colon_pos) = line.find(": ")
{
let info = &line[colon_pos + 2..];
if let Some(change_id) = info.split_whitespace().next() {
parent_change_id = ChangeId::new(change_id.to_string());
}
}
}
Ok(Status {
files,
has_conflicts,
working_copy_change_id,
parent_change_id,
})
}
/// Parse a single status line into FileStatus
///
/// Formats:
/// - "A path" (added)
/// - "M path" (modified)
/// - "D path" (deleted)
/// - "R prefix{old => new}" (renamed, jj format)
/// - "C path" (conflicted)
pub(super) fn parse_status_line(line: &str) -> Option<FileStatus> {
if line.len() < 2 {
return None;
}
let status_char = line.chars().next()?;
let rest = line.get(2..)?.trim();
if rest.is_empty() {
return None;
}
let state = match status_char {
'A' => FileState::Added,
'M' => FileState::Modified,
'D' => FileState::Deleted,
'R' => {
// Renamed: "R prefix{old => new}" (jj format)
if let Some(brace_start) = rest.find('{')
&& let Some(brace_end) = rest.find('}')
&& brace_end > brace_start
{
let prefix = &rest[..brace_start];
let inner = &rest[brace_start + 1..brace_end];
if let Some((old_part, new_part)) = inner.split_once(" => ") {
let from = format!("{}{}", prefix, old_part);
let to = format!("{}{}", prefix, new_part);
return Some(FileStatus {
path: to,
state: FileState::Renamed { from },
});
}
}
return None;
}
'C' => FileState::Conflicted,
_ => return None,
};
Some(FileStatus {
path: rest.to_string(),
state,
})
}
}