a very good jj gui
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

make sure there's file status

+102 -6
+66 -2
src/repo/log.rs
··· 11 11 pub is_working_copy: bool, 12 12 pub is_immutable: bool, 13 13 pub bookmarks: Vec<String>, 14 + pub files: Vec<ChangedFile>, 15 + } 16 + 17 + #[derive(Clone, Debug)] 18 + pub struct ChangedFile { 19 + pub path: String, 20 + pub status: FileStatus, 21 + } 22 + 23 + #[derive(Clone, Debug, PartialEq)] 24 + pub enum FileStatus { 25 + Added, 26 + Modified, 27 + Deleted, 14 28 } 15 29 16 30 pub fn fetch_log(repo_path: &Path, limit: usize) -> Result<Vec<Revision>, String> { ··· 53 67 Vec::new() 54 68 }; 55 69 70 + let change_id = parts[1].to_string(); 71 + let timestamp = format_timestamp(parts[4], &change_id); 72 + let files = fetch_files(repo_path, &change_id); 73 + 56 74 revisions.push(Revision { 57 75 commit_id: parts[0].to_string(), 58 - change_id: parts[1].to_string(), 76 + change_id, 59 77 description: parts[2].to_string(), 60 78 author: parts[3].to_string(), 61 - timestamp: parts[4].to_string(), 79 + timestamp, 62 80 is_working_copy: idx == 0, 63 81 is_immutable: parts[5] == "immutable", 64 82 bookmarks, 83 + files, 65 84 }); 66 85 } 67 86 } 68 87 69 88 Ok(revisions) 70 89 } 90 + 91 + fn format_timestamp(raw: &str, change_id: &str) -> String { 92 + if change_id == "zzzzzzzz" { 93 + "root".to_string() 94 + } else { 95 + raw.to_string() 96 + } 97 + } 98 + 99 + pub fn fetch_files(repo_path: &Path, change_id: &str) -> Vec<ChangedFile> { 100 + let output = Command::new("jj") 101 + .args(["diff", "--summary", "-r", change_id, "--no-pager"]) 102 + .current_dir(repo_path) 103 + .output(); 104 + 105 + let Ok(output) = output else { 106 + return Vec::new(); 107 + }; 108 + 109 + if !output.status.success() { 110 + return Vec::new(); 111 + } 112 + 113 + let stdout = String::from_utf8_lossy(&output.stdout); 114 + stdout 115 + .lines() 116 + .filter_map(|line| { 117 + let line = line.trim(); 118 + if line.len() < 2 { 119 + return None; 120 + } 121 + let status_char = line.chars().next()?; 122 + let path = line[1..].trim().to_string(); 123 + 124 + let status = match status_char { 125 + 'A' => FileStatus::Added, 126 + 'M' => FileStatus::Modified, 127 + 'D' => FileStatus::Deleted, 128 + _ => return None, 129 + }; 130 + 131 + Some(ChangedFile { path, status }) 132 + }) 133 + .collect() 134 + }
+36 -4
src/ui/log_view.rs
··· 5 5 6 6 use super::theme::{Colors, TextSize}; 7 7 use crate::app::Tatami; 8 - use crate::repo::log::Revision; 8 + use crate::repo::log::{FileStatus, Revision}; 9 9 10 10 pub fn render_log_view( 11 11 revisions: &[Revision], ··· 162 162 } 163 163 164 164 fn render_expanded_detail(rev: &Revision, is_last: bool) -> impl IntoElement + use<> { 165 + let files_content = if rev.files.is_empty() { 166 + div() 167 + .text_size(TextSize::XS) 168 + .text_color(rgb(Colors::TEXT_MUTED)) 169 + .child("(no file changes)") 170 + } else { 171 + div() 172 + .flex() 173 + .flex_col() 174 + .gap_1() 175 + .text_size(TextSize::XS) 176 + .children(rev.files.iter().map(|f| { 177 + let (prefix, color) = match f.status { 178 + FileStatus::Added => ("A", Colors::ADDED), 179 + FileStatus::Modified => ("M", Colors::MODIFIED), 180 + FileStatus::Deleted => ("D", Colors::DELETED), 181 + }; 182 + div() 183 + .flex() 184 + .gap_2() 185 + .child( 186 + div() 187 + .w(px(12.0)) 188 + .text_color(rgb(color)) 189 + .child(prefix), 190 + ) 191 + .child( 192 + div() 193 + .text_color(rgb(Colors::TEXT)) 194 + .child(f.path.clone()), 195 + ) 196 + })) 197 + }; 198 + 165 199 div() 166 200 .flex() 167 201 .child( ··· 228 262 .pt_2() 229 263 .border_t_1() 230 264 .border_color(rgb(Colors::BORDER_MUTED)) 231 - .text_size(TextSize::XS) 232 - .text_color(rgb(Colors::TEXT_MUTED)) 233 - .child("(file changes not yet loaded)"), 265 + .child(files_content), 234 266 ), 235 267 ) 236 268 }