forked from mirrors/jj
cli: add ui.log-word-wrap option
Unlike Mercurial, this isn't a template keyword/function, but a config knob. Exposing graph_width to templater wouldn't be easy, and I don't think it's better to handle terminal wrapping in template. I'm not sure if patch content should be wrapped, so this option only applies to the template output for now. Closes #1043
This commit is contained in:
parent
2a32d81542
commit
904e9c5520
10 changed files with 315 additions and 15 deletions
|
@ -37,6 +37,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
* `jj config set` command allows simple config edits like
|
||||
`jj config set --repo user.email "somebody@example.com"`
|
||||
|
||||
* Added `ui.log-word-wrap` option to wrap `jj log`/`obslog`/`op log` content
|
||||
based on terminal width. [#1043](https://github.com/martinvonz/jj/issues/1043)
|
||||
|
||||
### Fixed bugs
|
||||
|
||||
* Modify/delete conflicts now include context lines
|
||||
|
|
|
@ -136,6 +136,15 @@ ui.diff.format = "git"
|
|||
ui.graph.style = "square"
|
||||
```
|
||||
|
||||
### Wrap log content
|
||||
|
||||
If enabled, `log`/`obslog`/`op log` content will be wrapped based on
|
||||
the terminal width.
|
||||
|
||||
```toml
|
||||
ui.log-word-wrap = true
|
||||
```
|
||||
|
||||
### Display of commit and change ids
|
||||
|
||||
Can be customized by the `format_short_id()` template alias.
|
||||
|
|
|
@ -62,7 +62,7 @@ use tracing_subscriber::prelude::*;
|
|||
use crate::config::{
|
||||
config_path, AnnotatedValue, CommandNameAndArgs, ConfigSource, LayeredConfigs,
|
||||
};
|
||||
use crate::formatter::{Formatter, PlainTextFormatter};
|
||||
use crate::formatter::{FormatRecorder, Formatter, PlainTextFormatter};
|
||||
use crate::merge_tools::{ConflictResolveError, DiffEditError};
|
||||
use crate::template_parser::{TemplateAliasesMap, TemplateParseError};
|
||||
use crate::templater::Template;
|
||||
|
@ -1617,6 +1617,53 @@ fn parse_commit_summary_template<'a>(
|
|||
)?)
|
||||
}
|
||||
|
||||
/// Helper to reformat content of log-like commands.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum LogContentFormat {
|
||||
NoWrap,
|
||||
Wrap { term_width: usize },
|
||||
}
|
||||
|
||||
impl LogContentFormat {
|
||||
pub fn new(ui: &Ui, settings: &UserSettings) -> Result<Self, config::ConfigError> {
|
||||
if settings.config().get_bool("ui.log-word-wrap")? {
|
||||
let term_width = usize::from(ui.term_width().unwrap_or(80));
|
||||
Ok(LogContentFormat::Wrap { term_width })
|
||||
} else {
|
||||
Ok(LogContentFormat::NoWrap)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write(
|
||||
&self,
|
||||
formatter: &mut dyn Formatter,
|
||||
content_fn: impl FnOnce(&mut dyn Formatter) -> std::io::Result<()>,
|
||||
) -> std::io::Result<()> {
|
||||
self.write_graph_text(formatter, content_fn, || 0)
|
||||
}
|
||||
|
||||
pub fn write_graph_text(
|
||||
&self,
|
||||
formatter: &mut dyn Formatter,
|
||||
content_fn: impl FnOnce(&mut dyn Formatter) -> std::io::Result<()>,
|
||||
graph_width_fn: impl FnOnce() -> usize,
|
||||
) -> std::io::Result<()> {
|
||||
match self {
|
||||
LogContentFormat::NoWrap => content_fn(formatter),
|
||||
LogContentFormat::Wrap { term_width } => {
|
||||
let mut recorder = FormatRecorder::new();
|
||||
content_fn(&mut recorder)?;
|
||||
text_util::write_wrapped(
|
||||
formatter,
|
||||
&recorder,
|
||||
term_width.saturating_sub(graph_width_fn()),
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Use a proper TOML library to serialize instead.
|
||||
pub fn serialize_config_value(value: &config::Value) -> String {
|
||||
match &value.kind {
|
||||
|
|
|
@ -52,8 +52,8 @@ use crate::cli_util::{
|
|||
check_stale_working_copy, get_config_file_path, print_checkout_stats,
|
||||
resolve_multiple_nonempty_revsets, resolve_mutliple_nonempty_revsets_flag_guarded,
|
||||
run_ui_editor, serialize_config_value, short_commit_hash, user_error, user_error_with_hint,
|
||||
write_config_value_to_file, Args, CommandError, CommandHelper, DescriptionArg, RevisionArg,
|
||||
WorkspaceCommandHelper,
|
||||
write_config_value_to_file, Args, CommandError, CommandHelper, DescriptionArg,
|
||||
LogContentFormat, RevisionArg, WorkspaceCommandHelper,
|
||||
};
|
||||
use crate::config::{AnnotatedValue, ConfigSource};
|
||||
use crate::diff_util::{self, DiffFormat, DiffFormatArgs};
|
||||
|
@ -1475,6 +1475,7 @@ fn cmd_log(ui: &mut Ui, command: &CommandHelper, args: &LogArgs) -> Result<(), C
|
|||
None => command.settings().config().get_string("templates.log")?,
|
||||
};
|
||||
let template = workspace_command.parse_commit_template(&template_string)?;
|
||||
let with_content_format = LogContentFormat::new(ui, command.settings())?;
|
||||
|
||||
{
|
||||
ui.request_pager();
|
||||
|
@ -1516,7 +1517,11 @@ fn cmd_log(ui: &mut Ui, command: &CommandHelper, args: &LogArgs) -> Result<(), C
|
|||
let mut buffer = vec![];
|
||||
let commit_id = index_entry.commit_id();
|
||||
let commit = store.get_commit(&commit_id)?;
|
||||
template.format(&commit, ui.new_formatter(&mut buffer).as_mut())?;
|
||||
with_content_format.write_graph_text(
|
||||
ui.new_formatter(&mut buffer).as_mut(),
|
||||
|formatter| template.format(&commit, formatter),
|
||||
|| graph.width(&index_entry.position(), &graphlog_edges),
|
||||
)?;
|
||||
if !buffer.ends_with(b"\n") {
|
||||
buffer.push(b'\n');
|
||||
}
|
||||
|
@ -1551,7 +1556,8 @@ fn cmd_log(ui: &mut Ui, command: &CommandHelper, args: &LogArgs) -> Result<(), C
|
|||
};
|
||||
for index_entry in iter {
|
||||
let commit = store.get_commit(&index_entry.commit_id())?;
|
||||
template.format(&commit, formatter)?;
|
||||
with_content_format
|
||||
.write(formatter, |formatter| template.format(&commit, formatter))?;
|
||||
if !diff_formats.is_empty() {
|
||||
diff_util::show_patch(
|
||||
formatter,
|
||||
|
@ -1604,6 +1610,7 @@ fn cmd_obslog(ui: &mut Ui, command: &CommandHelper, args: &ObslogArgs) -> Result
|
|||
None => command.settings().config().get_string("templates.log")?,
|
||||
};
|
||||
let template = workspace_command.parse_commit_template(&template_string)?;
|
||||
let with_content_format = LogContentFormat::new(ui, command.settings())?;
|
||||
|
||||
ui.request_pager();
|
||||
let mut formatter = ui.stdout_formatter();
|
||||
|
@ -1623,10 +1630,11 @@ fn cmd_obslog(ui: &mut Ui, command: &CommandHelper, args: &ObslogArgs) -> Result
|
|||
edges.push(Edge::direct(predecessor.id().clone()));
|
||||
}
|
||||
let mut buffer = vec![];
|
||||
{
|
||||
let mut formatter = ui.new_formatter(&mut buffer);
|
||||
template.format(&commit, formatter.as_mut())?;
|
||||
}
|
||||
with_content_format.write_graph_text(
|
||||
ui.new_formatter(&mut buffer).as_mut(),
|
||||
|formatter| template.format(&commit, formatter),
|
||||
|| graph.width(commit.id(), &edges),
|
||||
)?;
|
||||
if !buffer.ends_with(b"\n") {
|
||||
buffer.push(b'\n');
|
||||
}
|
||||
|
@ -1653,7 +1661,8 @@ fn cmd_obslog(ui: &mut Ui, command: &CommandHelper, args: &ObslogArgs) -> Result
|
|||
}
|
||||
} else {
|
||||
for commit in commits {
|
||||
template.format(&commit, formatter)?;
|
||||
with_content_format
|
||||
.write(formatter, |formatter| template.format(&commit, formatter))?;
|
||||
if !diff_formats.is_empty() {
|
||||
show_predecessor_patch(formatter, &workspace_command, &commit, &diff_formats)?;
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ use clap::Subcommand;
|
|||
use jujutsu_lib::dag_walk::topo_order_reverse;
|
||||
use jujutsu_lib::operation::Operation;
|
||||
|
||||
use crate::cli_util::{user_error, CommandError, CommandHelper};
|
||||
use crate::cli_util::{user_error, CommandError, CommandHelper, LogContentFormat};
|
||||
use crate::graphlog::{get_graphlog, Edge};
|
||||
use crate::operation_templater;
|
||||
use crate::templater::Template as _;
|
||||
|
@ -63,6 +63,7 @@ fn cmd_op_log(
|
|||
&template_string,
|
||||
workspace_command.template_aliases_map(),
|
||||
)?;
|
||||
let with_content_format = LogContentFormat::new(ui, command.settings())?;
|
||||
|
||||
ui.request_pager();
|
||||
let mut formatter = ui.stdout_formatter();
|
||||
|
@ -79,8 +80,11 @@ fn cmd_op_log(
|
|||
}
|
||||
let is_head_op = op.id() == &head_op_id;
|
||||
let mut buffer = vec![];
|
||||
ui.new_formatter(&mut buffer)
|
||||
.with_label("op_log", |formatter| template.format(&op, formatter))?;
|
||||
with_content_format.write_graph_text(
|
||||
ui.new_formatter(&mut buffer).as_mut(),
|
||||
|formatter| formatter.with_label("op_log", |formatter| template.format(&op, formatter)),
|
||||
|| graph.width(op.id(), &edges),
|
||||
)?;
|
||||
if !buffer.ends_with(b"\n") {
|
||||
buffer.push(b'\n');
|
||||
}
|
||||
|
|
|
@ -102,6 +102,11 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"log-word-wrap": {
|
||||
"type": "boolean",
|
||||
"description": "Whether to wrap log template output",
|
||||
"default": false
|
||||
},
|
||||
"editor": {
|
||||
"type": "string",
|
||||
"description": "Editor to use for commands that involve editing text"
|
||||
|
|
|
@ -7,3 +7,6 @@ fetch = "origin"
|
|||
|
||||
[revset-aliases]
|
||||
# Placeholder: added by user
|
||||
|
||||
[ui]
|
||||
log-word-wrap = false
|
||||
|
|
|
@ -989,3 +989,126 @@ fn test_graph_styles() {
|
|||
o
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_log_word_wrap() {
|
||||
let test_env = TestEnvironment::default();
|
||||
test_env.jj_cmd_success(test_env.env_root(), &["init", "repo", "--git"]);
|
||||
let repo_path = test_env.env_root().join("repo");
|
||||
let render = |args: &[&str], columns: u32, word_wrap: bool| {
|
||||
let mut args = args.to_vec();
|
||||
if word_wrap {
|
||||
args.push("--config-toml=ui.log-word-wrap=true");
|
||||
}
|
||||
let assert = test_env
|
||||
.jj_cmd(&repo_path, &args)
|
||||
.env("COLUMNS", columns.to_string())
|
||||
.assert()
|
||||
.success()
|
||||
.stderr("");
|
||||
get_stdout_string(&assert)
|
||||
};
|
||||
|
||||
test_env.jj_cmd_success(&repo_path, &["commit", "-m", "main branch 1"]);
|
||||
test_env.jj_cmd_success(&repo_path, &["describe", "-m", "main branch 2"]);
|
||||
test_env.jj_cmd_success(&repo_path, &["new", "-m", "side"]);
|
||||
test_env.jj_cmd_success(&repo_path, &["new", "-m", "merge", "@--", "@"]);
|
||||
|
||||
// ui.log-word-wrap option applies to both graph/no-graph outputs
|
||||
insta::assert_snapshot!(render(&["log", "-r@"], 40, false), @r###"
|
||||
@ mzvwutvlkqwt test.user@example.com 2001-02-03 04:05:11.000 +07:00 68518a7e6c9e
|
||||
│ (empty) merge
|
||||
~
|
||||
"###);
|
||||
insta::assert_snapshot!(render(&["log", "-r@"], 40, true), @r###"
|
||||
@ mzvwutvlkqwt test.user@example.com
|
||||
│ 2001-02-03 04:05:11.000 +07:00
|
||||
~ 68518a7e6c9e
|
||||
(empty) merge
|
||||
"###);
|
||||
insta::assert_snapshot!(render(&["log", "--no-graph", "-r@"], 40, false), @r###"
|
||||
mzvwutvlkqwt test.user@example.com 2001-02-03 04:05:11.000 +07:00 68518a7e6c9e
|
||||
(empty) merge
|
||||
"###);
|
||||
insta::assert_snapshot!(render(&["log", "--no-graph", "-r@"], 40, true), @r###"
|
||||
mzvwutvlkqwt test.user@example.com
|
||||
2001-02-03 04:05:11.000 +07:00
|
||||
68518a7e6c9e
|
||||
(empty) merge
|
||||
"###);
|
||||
|
||||
// Color labels should be preserved
|
||||
insta::assert_snapshot!(render(&["log", "-r@", "--color=always"], 40, true), @r###"
|
||||
@ [1m[38;5;13mm[38;5;8mzvwutvlkqwt[39m [38;5;3mtest.user@example.com[39m[0m
|
||||
│ [1m[38;5;14m2001-02-03 04:05:11.000 +07:00[39m[0m
|
||||
~ [1m[38;5;12m6[38;5;8m8518a7e6c9e[39m[0m
|
||||
[1m[38;5;10m(empty)[39m merge[0m
|
||||
"###);
|
||||
|
||||
// Graph width should be subtracted from the term width
|
||||
let template = r#""0 1 2 3 4 5 6 7 8 9""#;
|
||||
insta::assert_snapshot!(render(&["log", "-T", template], 10, true), @r###"
|
||||
@ 0 1 2
|
||||
├─╮ 3 4 5
|
||||
│ │ 6 7 8
|
||||
│ │ 9
|
||||
o │ 0 1 2
|
||||
│ │ 3 4 5
|
||||
│ │ 6 7 8
|
||||
│ │ 9
|
||||
o │ 0 1 2
|
||||
├─╯ 3 4 5
|
||||
│ 6 7 8
|
||||
│ 9
|
||||
o 0 1 2 3
|
||||
│ 4 5 6 7
|
||||
│ 8 9
|
||||
o 0 1 2 3
|
||||
4 5 6 7
|
||||
8 9
|
||||
"###);
|
||||
insta::assert_snapshot!(
|
||||
render(&["log", "-T", template, "--config-toml=ui.graph.style='legacy'"], 9, true),
|
||||
@r###"
|
||||
@ 0 1 2
|
||||
|\ 3 4 5
|
||||
| | 6 7 8
|
||||
| | 9
|
||||
o | 0 1 2
|
||||
| | 3 4 5
|
||||
| | 6 7 8
|
||||
| | 9
|
||||
o | 0 1 2
|
||||
|/ 3 4 5
|
||||
| 6 7 8
|
||||
| 9
|
||||
o 0 1 2 3
|
||||
| 4 5 6 7
|
||||
| 8 9
|
||||
o 0 1 2 3
|
||||
4 5 6 7
|
||||
8 9
|
||||
"###);
|
||||
|
||||
// Shouldn't panic with $COLUMNS < graph_width
|
||||
insta::assert_snapshot!(render(&["log", "-r@"], 0, true), @r###"
|
||||
@ mzvwutvlkqwt
|
||||
│ test.user@example.com
|
||||
~ 2001-02-03
|
||||
04:05:11.000
|
||||
+07:00
|
||||
68518a7e6c9e
|
||||
(empty)
|
||||
merge
|
||||
"###);
|
||||
insta::assert_snapshot!(render(&["log", "-r@"], 1, true), @r###"
|
||||
@ mzvwutvlkqwt
|
||||
│ test.user@example.com
|
||||
~ 2001-02-03
|
||||
04:05:11.000
|
||||
+07:00
|
||||
68518a7e6c9e
|
||||
(empty)
|
||||
merge
|
||||
"###);
|
||||
}
|
||||
|
|
|
@ -11,7 +11,8 @@
|
|||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
use common::TestEnvironment;
|
||||
|
||||
use common::{get_stdout_string, TestEnvironment};
|
||||
|
||||
pub mod common;
|
||||
|
||||
|
@ -119,6 +120,62 @@ fn test_obslog_with_or_without_diff() {
|
|||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_obslog_word_wrap() {
|
||||
let test_env = TestEnvironment::default();
|
||||
test_env.jj_cmd_success(test_env.env_root(), &["init", "repo", "--git"]);
|
||||
let repo_path = test_env.env_root().join("repo");
|
||||
let render = |args: &[&str], columns: u32, word_wrap: bool| {
|
||||
let mut args = args.to_vec();
|
||||
if word_wrap {
|
||||
args.push("--config-toml=ui.log-word-wrap=true");
|
||||
}
|
||||
let assert = test_env
|
||||
.jj_cmd(&repo_path, &args)
|
||||
.env("COLUMNS", columns.to_string())
|
||||
.assert()
|
||||
.success()
|
||||
.stderr("");
|
||||
get_stdout_string(&assert)
|
||||
};
|
||||
|
||||
test_env.jj_cmd_success(&repo_path, &["describe", "-m", "first"]);
|
||||
|
||||
// ui.log-word-wrap option applies to both graph/no-graph outputs
|
||||
insta::assert_snapshot!(render(&["obslog"], 40, false), @r###"
|
||||
@ qpvuntsmwlqt test.user@example.com 2001-02-03 04:05:08.000 +07:00 69542c1984c1
|
||||
│ (empty) first
|
||||
o qpvuntsmwlqt test.user@example.com 2001-02-03 04:05:07.000 +07:00 230dd059e1b0
|
||||
(empty) (no description set)
|
||||
"###);
|
||||
insta::assert_snapshot!(render(&["obslog"], 40, true), @r###"
|
||||
@ qpvuntsmwlqt test.user@example.com
|
||||
│ 2001-02-03 04:05:08.000 +07:00
|
||||
│ 69542c1984c1
|
||||
│ (empty) first
|
||||
o qpvuntsmwlqt test.user@example.com
|
||||
2001-02-03 04:05:07.000 +07:00
|
||||
230dd059e1b0
|
||||
(empty) (no description set)
|
||||
"###);
|
||||
insta::assert_snapshot!(render(&["obslog", "--no-graph"], 40, false), @r###"
|
||||
qpvuntsmwlqt test.user@example.com 2001-02-03 04:05:08.000 +07:00 69542c1984c1
|
||||
(empty) first
|
||||
qpvuntsmwlqt test.user@example.com 2001-02-03 04:05:07.000 +07:00 230dd059e1b0
|
||||
(empty) (no description set)
|
||||
"###);
|
||||
insta::assert_snapshot!(render(&["obslog", "--no-graph"], 40, true), @r###"
|
||||
qpvuntsmwlqt test.user@example.com
|
||||
2001-02-03 04:05:08.000 +07:00
|
||||
69542c1984c1
|
||||
(empty) first
|
||||
qpvuntsmwlqt test.user@example.com
|
||||
2001-02-03 04:05:07.000 +07:00
|
||||
230dd059e1b0
|
||||
(empty) (no description set)
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_obslog_squash() {
|
||||
let mut test_env = TestEnvironment::default();
|
||||
|
|
|
@ -16,7 +16,7 @@ use std::path::Path;
|
|||
|
||||
use regex::Regex;
|
||||
|
||||
use crate::common::TestEnvironment;
|
||||
use crate::common::{get_stdout_string, TestEnvironment};
|
||||
|
||||
pub mod common;
|
||||
|
||||
|
@ -147,6 +147,46 @@ fn test_op_log_template() {
|
|||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_op_log_word_wrap() {
|
||||
let test_env = TestEnvironment::default();
|
||||
test_env.jj_cmd_success(test_env.env_root(), &["init", "repo", "--git"]);
|
||||
let repo_path = test_env.env_root().join("repo");
|
||||
let render = |args: &[&str], columns: u32, word_wrap: bool| {
|
||||
let mut args = args.to_vec();
|
||||
if word_wrap {
|
||||
args.push("--config-toml=ui.log-word-wrap=true");
|
||||
}
|
||||
let assert = test_env
|
||||
.jj_cmd(&repo_path, &args)
|
||||
.env("COLUMNS", columns.to_string())
|
||||
.assert()
|
||||
.success()
|
||||
.stderr("");
|
||||
get_stdout_string(&assert)
|
||||
};
|
||||
|
||||
// ui.log-word-wrap option works
|
||||
insta::assert_snapshot!(render(&["op", "log"], 40, false), @r###"
|
||||
@ a99a3fd5c51e test-username@host.example.com 22 years ago, lasted less than a microsecond
|
||||
│ add workspace 'default'
|
||||
o 56b94dfc38e7 test-username@host.example.com 22 years ago, lasted less than a microsecond
|
||||
initialize repo
|
||||
"###);
|
||||
insta::assert_snapshot!(render(&["op", "log"], 40, true), @r###"
|
||||
@ a99a3fd5c51e
|
||||
│ test-username@host.example.com 22
|
||||
│ years ago, lasted less than a
|
||||
│ microsecond
|
||||
│ add workspace 'default'
|
||||
o 56b94dfc38e7
|
||||
test-username@host.example.com 22
|
||||
years ago, lasted less than a
|
||||
microsecond
|
||||
initialize repo
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_op_log_configurable() {
|
||||
let test_env = TestEnvironment::default();
|
||||
|
|
Loading…
Reference in a new issue