[wip] jj client for mega-merge workflow
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// }