[wip] jj client for mega-merge workflow
at main 12 kB view raw
1use std::{io, path::Path, sync::Arc}; 2 3use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind}; 4use jj_lib::{ 5 commit::Commit, 6 config::StackedConfig, 7 graph::{GraphEdge, GraphEdgeType, TopoGroupedGraphIterator}, 8 repo::{Repo as _, StoreFactories}, 9 revset::{RevsetExpression, RevsetIteratorExt, UserRevsetExpression}, 10 settings::UserSettings, 11 workspace::{WorkingCopyFactories, Workspace}, 12}; 13use ratatui::{ 14 DefaultTerminal, Frame, 15 buffer::Buffer, 16 layout::{Constraint, Direction, Layout, Rect}, 17 style::{Color, Style, Stylize}, 18 symbols::border, 19 text::{Line, Text}, 20 widgets::{Block, Borders, Paragraph, Widget}, 21}; 22 23#[derive(Debug, Default)] 24pub struct App { 25 counter: u8, 26 exit: bool, 27} 28 29// Plan 30// 31// 1. run jj-log, get commits between mega-merge and each history's base. 32// 2. show UI where they are rendered separately 33// 3. make more consice UI (where other histories will be collapsed) 34 35impl App { 36 pub fn run(mut self, terminal: &mut DefaultTerminal) -> io::Result<()> { 37 while !self.exit { 38 // terminal.draw(|frame| self.draw(frame))?; 39 terminal.draw(|frame| frame.render_widget(&self, frame.area()))?; 40 self.handle_events()?; 41 } 42 Ok(()) 43 } 44 45 fn draw(&self, frame: &mut Frame) { 46 // TODO: draw all UI 47 let chunks = Layout::default() 48 .direction(Direction::Vertical) 49 .constraints([ 50 Constraint::Length(3), 51 Constraint::Min(1), 52 Constraint::Length(3), 53 ]) 54 .split(frame.area()); 55 let title_block = Block::default() 56 .borders(Borders::ALL) 57 .style(Style::default()); 58 let title_text = Text::from("hey"); 59 // let title_text = Text::from(vec![ 60 // Line::from(vec![ 61 // "Value: ".into(), 62 // self.counter.to_string().yellow(), 63 // ]), 64 // ]); 65 let title = Paragraph::new(title_text) 66 .block(title_block); 67 frame.render_widget(title, chunks[0]); 68 // frame.render_widget(self, frame.area()); 69 } 70 71 fn handle_events(&mut self) -> io::Result<()> { 72 match event::read()? { 73 Event::Key(key_event) if key_event.kind == KeyEventKind::Press => { 74 self.handle_key_event(key_event) 75 } 76 _ => {} 77 }; 78 Ok(()) 79 } 80 81 fn handle_key_event(&mut self, key_event: KeyEvent) { 82 match key_event.code { 83 KeyCode::Char('q') => self.exit(), 84 KeyCode::Char('h') | KeyCode::Left => self.decrement_counter(), 85 KeyCode::Char('l') | KeyCode::Right => self.increment_counter(), 86 _ => {} 87 } 88 } 89 90 fn exit(&mut self) { 91 self.exit = true 92 } 93 94 fn increment_counter(&mut self) { 95 self.counter += 1; 96 } 97 98 fn decrement_counter(&mut self) { 99 self.counter = self.counter.saturating_sub(1) 100 } 101} 102 103impl Widget for &App { 104 fn render(self, area: Rect, buf: &mut Buffer) { 105 let title = Line::from(" Counter App Tutorial ".bold()); 106 let block = Block::bordered() 107 .title(title.centered()) 108 .border_set(border::THICK); 109 110 let counter_text = Text::from(vec![Line::from(vec![ 111 "Value: ".into(), 112 self.counter.to_string().yellow(), 113 ])]); 114 115 Paragraph::new(counter_text) 116 .centered() 117 .block(block) 118 .render(area, buf); 119 } 120} 121 122mod jj_helper; 123 124use jj_cli::{cli_util::CommandHelper, formatter::FormatterExt as _, graphlog::SaplingGraphLog}; 125use jj_cli::cli_util::RevisionArg; 126use jj_cli::command_error::CommandError; 127use jj_cli::ui::Ui; 128use jj_cli::{ 129 cli_util::{CliRunner, LogContentFormat, format_template}, 130 graphlog::{GraphStyle, get_graphlog}, 131 templater::TemplateRenderer, 132}; 133use renderdag::GraphRowRenderer; 134 135// fn main() -> anyhow::Result<()> { 136// let path = Path::new("/Users/boltless/repo/tangled"); 137// let user_settings = UserSettings::from_config(StackedConfig::with_defaults())?; 138// let store_factories = StoreFactories::default(); 139// let working_copy_factories = WorkingCopyFactories::new(); 140// let ws = Workspace::load( 141// &user_settings, 142// path, 143// &store_factories, 144// &working_copy_factories, 145// )?; 146// name = ws.workspace_name(); 147// // let mut terminal = ratatui::init(); 148// // let result = App::default().run(&mut terminal); 149// // ratatui::restore(); 150// // result 151// Ok(()) 152// } 153 154#[derive(clap::Parser, Clone, Debug)] 155enum CustomCommand { 156 MegaLog(MegaLogArgs), 157} 158 159/// Frobnicate a revisions 160#[derive(clap::Args, Clone, Debug)] 161struct MegaLogArgs { 162 /// The revision to frobnicate 163 #[arg(default_value = "@")] 164 revision: RevisionArg, 165} 166 167fn run_custom_command( 168 ui: &mut Ui, 169 command_helper: &CommandHelper, 170 command: CustomCommand, 171) -> Result<(), CommandError> { 172 match command { 173 CustomCommand::MegaLog(args) => mega_merge_log(ui, command_helper, &args), 174 } 175} 176 177fn mega_merge_log( 178 ui: &mut Ui, 179 command_helper: &CommandHelper, 180 _args: &MegaLogArgs, 181) -> Result<(), CommandError> { 182 // 1. from "MM", get all edges, 183 // 2. render graphs based on those edges, similar logic to jj-log 184 // choose template based on focus state 185 let workspace_command = command_helper.workspace_helper(ui)?; 186 let settings = workspace_command.settings(); 187 188 let repo = workspace_command.repo(); 189 let store = repo.store(); 190 let graph_style = GraphStyle::from_settings(settings)?; 191 192 let use_elided_nodes = settings.get_bool("ui.log-synthetic-elided-nodes")?; 193 let with_content_format = LogContentFormat::new(ui, settings)?; 194 195 let template: TemplateRenderer<Commit>; 196 let node_template: TemplateRenderer<Option<Commit>>; 197 { 198 let language = workspace_command.commit_template_language(); 199 let template_string = settings.get_string("templates.log")?; 200 template = workspace_command 201 .parse_template(ui, &language, &template_string)? 202 .labeled(["log", "commit"]); 203 node_template = workspace_command 204 .parse_template(ui, &language, &settings.get_string("templates.log_node")?)? 205 .labeled(["log", "commit", "node"]); 206 } 207 208 // this part will be configurable 209 let mm_rev = RevisionArg::from(String::from("MM")); 210 let mm_commit = workspace_command.resolve_single_rev(ui, &mm_rev)?; 211 212 let immutable_exp = { 213 let rev = String::from("immutable()"); 214 workspace_command.parse_revset(ui, &RevisionArg::from(rev))? 215 }; 216 let trunk_exp = { 217 let rev = String::from("trunk()"); 218 workspace_command.parse_revset(ui, &RevisionArg::from(rev))? 219 }; 220 221 // let parent = mm_commit.parents().next().unwrap()?; 222 for parent in mm_commit.parents().take(3) { 223 let parent = parent?; 224 println!("{}", parent.change_id()); 225 226 // create revsets based on each branches 227 // maybe make this configurable as "log_revs(head)"? 228 // ancestors(::x ~ immutable(), 2) 229 let exp = 230 RevsetExpression::union( 231 &RevsetExpression::ancestors_range( 232 &RevsetExpression::minus( 233 &RevsetExpression::ancestors(&RevsetExpression::commits(vec![parent.id().clone()])), 234 immutable_exp.expression(), 235 ), 236 0..2, 237 ), 238 // &RevsetExpression::commits(vec![mm_commit.id().clone()]), 239 trunk_exp.expression(), 240 ); 241 let exp = workspace_command.attach_revset_evaluator(exp); 242 let revset = exp.evaluate()?; 243 244 let mut formatter = ui.stdout_formatter(); 245 let formatter = formatter.as_mut(); 246 let mut raw_output = formatter.raw()?; 247 248 249 // let mut graph = get_graphlog(graph_style, raw_output.as_mut()); 250 // let mut graph = GraphRowRenderer::new().output().with_min_row_height(0).build_box_drawing(); 251 let mut graph = { 252 let builder = GraphRowRenderer::new().output().with_min_row_height(0); 253 SaplingGraphLog::create(builder.build_box_drawing().with_square_glyphs(), raw_output.as_mut()) 254 }; 255 256 let iter = { 257 let mut forward_iter = TopoGroupedGraphIterator::new(revset.iter_graph(), |id| id); 258 forward_iter.prioritize_branch(parent.id().clone()); 259 forward_iter 260 }; 261 262 for node in iter { 263 let (commit_id, edges) = node?; 264 265 // The graph is keyed by (CommitId, is_synthetic) 266 let mut graphlog_edges = vec![]; 267 let mut missing_edge_id = None; 268 let mut elided_targets = vec![]; 269 for edge in edges { 270 match edge.edge_type { 271 GraphEdgeType::Missing => { 272 missing_edge_id = Some(edge.target); 273 } 274 GraphEdgeType::Direct => { 275 graphlog_edges.push(GraphEdge::direct((edge.target, false))); 276 } 277 GraphEdgeType::Indirect => { 278 if use_elided_nodes { 279 elided_targets.push(edge.target.clone()); 280 graphlog_edges.push(GraphEdge::direct((edge.target, true))); 281 } else { 282 graphlog_edges.push(GraphEdge::indirect((edge.target, false))); 283 } 284 } 285 } 286 } 287 if let Some(missing_edge_id) = missing_edge_id { 288 graphlog_edges.push(GraphEdge::missing((missing_edge_id, false))); 289 } 290 let mut buffer = vec![]; 291 let key = (commit_id, false); 292 let commit = store.get_commit(&key.0)?; 293 294 with_content_format 295 .sub_width(graph.width(&key, &graphlog_edges)) 296 .write(ui.new_formatter(&mut buffer).as_mut(), |formatter| { 297 template.format(&commit, formatter) 298 })?; 299 if !buffer.ends_with(b"\n") { 300 buffer.push(b'\n'); 301 } 302 303 let node_symbol = format_template(ui, &Some(commit), &node_template); 304 graph.add_node( 305 &key, 306 &graphlog_edges, 307 &node_symbol, 308 &String::from_utf8_lossy(&buffer), 309 )?; 310 for elided_target in elided_targets { 311 let elided_key = (elided_target, true); 312 let real_key = (elided_key.0.clone(), false); 313 let edges = [GraphEdge::direct(real_key)]; 314 let mut buffer = vec![]; 315 let within_graph = 316 with_content_format.sub_width(graph.width(&elided_key, &edges)); 317 within_graph.write(ui.new_formatter(&mut buffer).as_mut(), |formatter| { 318 writeln!(formatter.labeled("elided"), "(elided revisions)") 319 })?; 320 let node_symbol = format_template(ui, &None, &node_template); 321 graph.add_node( 322 &elided_key, 323 &edges, 324 &node_symbol, 325 &String::from_utf8_lossy(&buffer), 326 )?; 327 } 328 } 329 } 330 331 // let revset_expression = { 332 // // let rev = String::from("ancestors(@, 4)"); 333 // let rev = String::from("ancestors(MM, 2)"); 334 // workspace_command.parse_revset(ui, &RevisionArg::from(rev))? 335 // }; 336 // let revset = revset_expression.evaluate()?; 337 338 // // so this is possible 339 // let mut terminal = ratatui::init(); 340 // let result = App::default().run(&mut terminal); 341 // ratatui::restore(); 342 // result?; 343 344 Ok(()) 345} 346 347fn main() -> std::process::ExitCode { 348 CliRunner::init() 349 .add_subcommand(run_custom_command) 350 .run() 351 .into() 352} 353 354// fn main() -> anyhow::Result<()> { 355// let mut terminal = ratatui::init(); 356// let result = App::default().run(&mut terminal); 357// ratatui::restore(); 358// result?; 359// Ok(()) 360// }