cli: add jj concepts subcommand for concept help, starting with "branches"

With this commit, you can run `jj concepts branches` to get help about
the "branches" concept. We don't have much help for other commands and
their arguments yet, but I'm starting with concept guides so we can
point to them as we add help for commands and their arguments.

I initially tried to make the command to get help be `jj help
--concept branches`. That would require replacing clap's
implementation of the help command with our own. clap-rs/clap#1350
prevented me from doing that. But I'm pretty happy with having it
under `jj concepts` anyway. It's probably more discoverable that way.

I tried to mimic clap's styling with yellow headings.
This commit is contained in:
Martin von Zweigbergk 2021-09-02 09:06:34 -07:00
parent fddcaaada1
commit fa73b17c66
4 changed files with 134 additions and 1 deletions

29
Cargo.lock generated
View file

@ -174,7 +174,7 @@ dependencies = [
"atty",
"bitflags",
"strsim",
"textwrap",
"textwrap 0.11.0",
"unicode-width",
"vec_map",
]
@ -540,6 +540,7 @@ dependencies = [
"serde_json",
"tempfile",
"test-case",
"textwrap 0.14.2",
"thiserror",
"uuid",
"zstd",
@ -1203,6 +1204,12 @@ dependencies = [
"opaque-debug 0.2.3",
]
[[package]]
name = "smawk"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043"
[[package]]
name = "static_assertions"
version = "1.1.0"
@ -1268,6 +1275,17 @@ dependencies = [
"unicode-width",
]
[[package]]
name = "textwrap"
version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80"
dependencies = [
"smawk",
"unicode-linebreak",
"unicode-width",
]
[[package]]
name = "thiserror"
version = "1.0.25"
@ -1354,6 +1372,15 @@ dependencies = [
"matches",
]
[[package]]
name = "unicode-linebreak"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a52dcaab0c48d931f7cc8ef826fa51690a08e1ea55117ef26f89864f532383f"
dependencies = [
"regex",
]
[[package]]
name = "unicode-normalization"
version = "0.1.19"

View file

@ -39,6 +39,7 @@ protobuf-codegen-pure = "2.22.1"
rand = "0.8.0"
serde_json = "1.0.60"
tempfile = "3.1.0"
textwrap = "0.14.2"
thiserror = "1.0.22"
uuid = { version = "0.8.1", features = ["v4"] }
zstd = "0.6.0"

View file

@ -1031,6 +1031,11 @@ fn get_app<'a, 'b>() -> App<'a, 'b> {
)
.subcommand(SubCommand::with_name("index").about("Show commit index stats"))
.subcommand(SubCommand::with_name("reindex").about("Rebuild commit index"));
let concepts_command = SubCommand::with_name("concepts")
.alias("concept")
.about("Show help about concepts")
.setting(clap::AppSettings::SubcommandRequiredElseHelp)
.subcommand(SubCommand::with_name("branches").about("Show help about branches"));
let help_message = "Print help information";
let mut app = App::new("Jujutsu")
.global_setting(clap::AppSettings::ColoredHelp)
@ -1087,6 +1092,7 @@ fn get_app<'a, 'b>() -> App<'a, 'b> {
git_command,
bench_command,
debug_command,
concepts_command,
] {
app = app.subcommand(subcommand.help_message(help_message));
}
@ -2247,6 +2253,9 @@ fn cmd_merge(
let mut commits = vec![];
let mut parent_ids = vec![];
for revision_arg in revision_args {
// TODO: Should we allow each argument to resolve to multiple revisions?
// It would be neat to be able to do `jj merge main` when `main` is conflicted,
// but I'm not sure it would actually be useful.
let commit = repo_command.resolve_single_rev(ui, revision_arg)?;
parent_ids.push(commit.id().clone());
commits.push(commit);
@ -3021,6 +3030,98 @@ fn cmd_git(
Ok(())
}
fn cmd_concepts(
ui: &mut Ui,
command: &CommandHelper,
sub_matches: &ArgMatches,
) -> Result<(), CommandError> {
let mut sections = vec![];
if sub_matches.is_present("branches") {
sections.push((
"INTRODUCTION:",
"\
Branches are named pointers to revisions (just like they are in Git). You can move them without \
affecting the target revision's identity. Branches automatically move when revisions \
are rewritten (e.g. by `jj rebase`). You can pass a branch's name to commands that \
want a revision as argument. For example, `jj co main` will check out the revision \
pointed to by the \"main\" branch. Use `jj branches` to list branches and `jj \
branch` to create, move, or delete branches. There is currently no concept of an \
active/current/checked-out branch.",
));
sections.push((
"REMOTES:",
"\
Jujutsu identifies a branch by its name across remotes (this is unlike Git and more like \
Mercurial's \"bookmarks\"). For example, a branch called \"main\" in your local repo \
is considered the same branch as a branch by the same name on a remote. When you \
pull from a remote (currently only via `jj git fetch`), any branches from the remote \
will be imported as branches in your local repo.
Jujutsu also records the last seen position on each remote (just like Git's remote-tracking \
branches). You can refer to these with `<branch name>@<remote name>`, such as `jj co \
main@origin`. Most commands don't show the remote branch if it has the same target \
as the local branch. The local branch (without `@<remote name>`) is considered the \
branch's desired target. Consequently, if you want to update a branch on a remote, \
you first update the branch locally and then push the update to the remote.
When you pull from a remote, any changes compared to the current record of the remote's state will \
be propagated to the local branch. Let's say you run `jj git fetch --remote origin` \
and the remote's \"main\" branch has moved so its target is now ahead of the local \
record in `main@origin`. That will update `main@origin` to the new target. It will \
also apply the change to the local branch `main`. If the local target had also moved \
compared to `main@origin` (probably because you had run `jj branch main`), then the \
two updates will be merged. If one is ahead of the other, then that target will be \
the new target. Otherwise the local branch will be conflicted (see next section for \
details).",
));
sections.push((
"CONFLICTS:",
"\
Branches can end up in a conflicted state. When that happens, `jj status` will include information \
about the conflicted branches (and instructions for how to mitigate it). `jj \
branches` will have details. `jj log` will show the branch name with a question mark \
suffix (e.g. `main?`) on each of the conflicted branch's potential target revisions. \
Using the branch name to look up a revision will resolve to all potential targets. \
That means that `jj co main` will error out, complaining that the revset resolved to \
multiple revisions.
Both local branches (e.g. `main`) and the remote branch (e.g. `main@origin`) can have conflicts. \
Both can end up in that state if concurrent operations were run in the repo. The \
local branch more typically becomes conflicted because it was updated both locally \
and on a remote.
To resolve a conflicted state in a local branch (e.g. `main`), you can move the branch to the \
desired target with `jj branch`. You may want to first either merge the conflicted \
targets with `jj merge`, or you may want to rebase one side on top of the other with \
`jj rebase`.
To resolve a conflicted state in a remote branch (e.g. `main@origin`), simply pull from the remote \
(e.g. `jj git fetch`). The conflict resolution will also propagate to the local \
branch (which was presumably also conflicted).",
));
} else {
panic!("unhandled help concept: {:#?}", command.root_matches());
}
let mut formatter = ui.stdout_formatter();
formatter.add_label("concepts".to_string())?;
for (i, (heading, text)) in sections.iter().enumerate() {
if i != 0 {
formatter.write_str("\n")?;
}
formatter.add_label("heading".to_string())?;
formatter.write_str(heading)?;
formatter.remove_label()?;
formatter.write_str("\n")?;
let text = textwrap::fill(text, 116);
let text = textwrap::indent(&text, " ");
formatter.write_str(&text)?;
formatter.write_str("\n")?;
}
formatter.remove_label()?;
Ok(())
}
fn resolve_alias(ui: &mut Ui, args: Vec<String>) -> Vec<String> {
if args.len() >= 2 {
let command_name = args[1].clone();
@ -3126,6 +3227,8 @@ where
cmd_bench(&mut ui, &command_helper, sub_matches)
} else if let Some(sub_matches) = matches.subcommand_matches("debug") {
cmd_debug(&mut ui, &command_helper, sub_matches)
} else if let Some(sub_matches) = matches.subcommand_matches("concepts") {
cmd_concepts(&mut ui, &command_helper, sub_matches)
} else {
panic!("unhandled command: {:#?}", matches);
};

View file

@ -106,6 +106,8 @@ fn config_colors(user_settings: &UserSettings) -> HashMap<String, String> {
result.insert(String::from("op-log user"), String::from("yellow"));
result.insert(String::from("op-log time"), String::from("magenta"));
result.insert(String::from("concepts heading"), String::from("yellow"));
if let Ok(table) = user_settings.config().get_table("colors") {
for (key, value) in table {
result.insert(key, value.to_string());