just playing with tangled
1use itertools::Itertools;
2use jj_lib::commit::Commit;
3use jj_lib::matchers::EverythingMatcher;
4use jj_lib::merged_tree::MergedTree;
5use jj_lib::repo::ReadonlyRepo;
6use jj_lib::settings::UserSettings;
7
8use crate::cli_util::{edit_temp_file, WorkspaceCommandHelper};
9use crate::command_error::CommandError;
10use crate::diff_util::{self, DiffFormat};
11use crate::formatter::PlainTextFormatter;
12use crate::text_util;
13use crate::ui::Ui;
14
15pub fn edit_description(
16 repo: &ReadonlyRepo,
17 description: &str,
18 settings: &UserSettings,
19) -> Result<String, CommandError> {
20 let description = format!(
21 r#"{}
22JJ: Lines starting with "JJ: " (like this one) will be removed.
23"#,
24 description
25 );
26
27 let description = edit_temp_file(
28 "description",
29 ".jjdescription",
30 repo.repo_path(),
31 &description,
32 settings,
33 )?;
34
35 // Normalize line ending, remove leading and trailing blank lines.
36 let description = description
37 .lines()
38 .filter(|line| !line.starts_with("JJ: "))
39 .join("\n");
40 Ok(text_util::complete_newline(description.trim_matches('\n')))
41}
42
43/// Combines the descriptions from the input commits. If only one is non-empty,
44/// then that one is used. Otherwise we concatenate the messages and ask the
45/// user to edit the result in their editor.
46pub fn combine_messages(
47 repo: &ReadonlyRepo,
48 sources: &[&Commit],
49 destination: &Commit,
50 settings: &UserSettings,
51) -> Result<String, CommandError> {
52 let non_empty = sources
53 .iter()
54 .chain(std::iter::once(&destination))
55 .filter(|c| !c.description().is_empty())
56 .take(2)
57 .collect_vec();
58 match *non_empty.as_slice() {
59 [] => {
60 return Ok(String::new());
61 }
62 [commit] => {
63 return Ok(commit.description().to_owned());
64 }
65 _ => {}
66 }
67 // Produce a combined description with instructions for the user to edit.
68 // Include empty descriptins too, so the user doesn't have to wonder why they
69 // only see 2 descriptions when they combined 3 commits.
70 let mut combined = "JJ: Enter a description for the combined commit.".to_string();
71 combined.push_str("\nJJ: Description from the destination commit:\n");
72 combined.push_str(destination.description());
73 for commit in sources {
74 combined.push_str("\nJJ: Description from source commit:\n");
75 combined.push_str(commit.description());
76 }
77 edit_description(repo, &combined, settings)
78}
79
80/// Create a description from a list of paragraphs.
81///
82/// Based on the Git CLI behavior. See `opt_parse_m()` and `cleanup_mode` in
83/// `git/builtin/commit.c`.
84pub fn join_message_paragraphs(paragraphs: &[String]) -> String {
85 // Ensure each paragraph ends with a newline, then add another newline between
86 // paragraphs.
87 paragraphs
88 .iter()
89 .map(|p| text_util::complete_newline(p.as_str()))
90 .join("\n")
91}
92
93pub fn description_template_for_describe(
94 ui: &Ui,
95 settings: &UserSettings,
96 workspace_command: &WorkspaceCommandHelper,
97 commit: &Commit,
98) -> Result<String, CommandError> {
99 let mut diff_summary_bytes = Vec::new();
100 diff_util::show_patch(
101 ui,
102 &mut PlainTextFormatter::new(&mut diff_summary_bytes),
103 workspace_command,
104 commit,
105 &EverythingMatcher,
106 &[DiffFormat::Summary],
107 )?;
108 let description = if commit.description().is_empty() {
109 settings.default_description()
110 } else {
111 commit.description().to_owned()
112 };
113 if diff_summary_bytes.is_empty() {
114 Ok(description)
115 } else {
116 Ok(description + "\n" + &diff_summary_to_description(&diff_summary_bytes))
117 }
118}
119
120pub fn description_template_for_commit(
121 ui: &Ui,
122 settings: &UserSettings,
123 workspace_command: &WorkspaceCommandHelper,
124 intro: &str,
125 overall_commit_description: &str,
126 from_tree: &MergedTree,
127 to_tree: &MergedTree,
128) -> Result<String, CommandError> {
129 let mut diff_summary_bytes = Vec::new();
130 diff_util::show_diff(
131 ui,
132 &mut PlainTextFormatter::new(&mut diff_summary_bytes),
133 workspace_command,
134 from_tree,
135 to_tree,
136 &EverythingMatcher,
137 &[DiffFormat::Summary],
138 )?;
139 let mut template_chunks = Vec::new();
140 if !intro.is_empty() {
141 template_chunks.push(format!("JJ: {intro}\n"));
142 }
143 template_chunks.push(if overall_commit_description.is_empty() {
144 settings.default_description()
145 } else {
146 overall_commit_description.to_owned()
147 });
148 if !diff_summary_bytes.is_empty() {
149 template_chunks.push("\n".to_owned());
150 template_chunks.push(diff_summary_to_description(&diff_summary_bytes));
151 }
152 Ok(template_chunks.concat())
153}
154
155pub fn diff_summary_to_description(bytes: &[u8]) -> String {
156 let text = std::str::from_utf8(bytes).expect(
157 "Summary diffs and repo paths must always be valid UTF8.",
158 // Double-check this assumption for diffs that include file content.
159 );
160 "JJ: This commit contains the following changes:\n".to_owned()
161 + &textwrap::indent(text, "JJ: ")
162}