diff --git a/Cargo.lock b/Cargo.lock index d2d77fc93..56b93bf87 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index 683adc167..7da8b7504 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/src/commands.rs b/src/commands.rs index 6ec040c9f..b7a0e017e 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -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 `@`, 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 `@`) 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) -> Vec { 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); }; diff --git a/src/formatter.rs b/src/formatter.rs index cda73a196..c85080958 100644 --- a/src/formatter.rs +++ b/src/formatter.rs @@ -106,6 +106,8 @@ fn config_colors(user_settings: &UserSettings) -> HashMap { 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());