mirror of
https://github.com/martinvonz/jj.git
synced 2025-01-15 16:53:25 +00:00
completion: teach commands about files
Some checks are pending
binaries / Build binary artifacts (push) Waiting to run
nix / flake check (push) Waiting to run
build / build (, macos-13) (push) Waiting to run
build / build (, macos-14) (push) Waiting to run
build / build (, ubuntu-latest) (push) Waiting to run
build / build (, windows-latest) (push) Waiting to run
build / build (--all-features, ubuntu-latest) (push) Waiting to run
build / Build jj-lib without Git support (push) Waiting to run
build / Check protos (push) Waiting to run
build / Check formatting (push) Waiting to run
build / Check that MkDocs can build the docs (push) Waiting to run
build / Check that MkDocs can build the docs with latest Python and uv (push) Waiting to run
build / cargo-deny (advisories) (push) Waiting to run
build / cargo-deny (bans licenses sources) (push) Waiting to run
build / Clippy check (push) Waiting to run
Codespell / Codespell (push) Waiting to run
website / prerelease-docs-build-deploy (ubuntu-latest) (push) Waiting to run
Scorecards supply-chain security / Scorecards analysis (push) Waiting to run
Some checks are pending
binaries / Build binary artifacts (push) Waiting to run
nix / flake check (push) Waiting to run
build / build (, macos-13) (push) Waiting to run
build / build (, macos-14) (push) Waiting to run
build / build (, ubuntu-latest) (push) Waiting to run
build / build (, windows-latest) (push) Waiting to run
build / build (--all-features, ubuntu-latest) (push) Waiting to run
build / Build jj-lib without Git support (push) Waiting to run
build / Check protos (push) Waiting to run
build / Check formatting (push) Waiting to run
build / Check that MkDocs can build the docs (push) Waiting to run
build / Check that MkDocs can build the docs with latest Python and uv (push) Waiting to run
build / cargo-deny (advisories) (push) Waiting to run
build / cargo-deny (bans licenses sources) (push) Waiting to run
build / Clippy check (push) Waiting to run
Codespell / Codespell (push) Waiting to run
website / prerelease-docs-build-deploy (ubuntu-latest) (push) Waiting to run
Scorecards supply-chain security / Scorecards analysis (push) Waiting to run
This is heavily based on Benjamin Tan's fish completions: https://gist.github.com/bnjmnt4n/9f47082b8b6e6ed2b2a805a1516090c8 Some differences include: - The end of a `--from`, `--to` ranges is also considered. - `jj log` is not completed (yet). It has a different `--revisions` argument that requires some special handling.
This commit is contained in:
parent
a5690beab5
commit
5fcc549eab
13 changed files with 542 additions and 11 deletions
|
@ -12,6 +12,7 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use clap_complete::ArgValueCandidates;
|
||||
use jj_lib::backend::Signature;
|
||||
use jj_lib::object_id::ObjectId;
|
||||
use jj_lib::repo::Repo;
|
||||
|
@ -20,6 +21,7 @@ use tracing::instrument;
|
|||
use crate::cli_util::CommandHelper;
|
||||
use crate::command_error::user_error;
|
||||
use crate::command_error::CommandError;
|
||||
use crate::complete;
|
||||
use crate::description_util::description_template;
|
||||
use crate::description_util::edit_description;
|
||||
use crate::description_util::join_message_paragraphs;
|
||||
|
@ -40,7 +42,10 @@ pub(crate) struct CommitArgs {
|
|||
#[arg(long = "message", short, value_name = "MESSAGE")]
|
||||
message_paragraphs: Vec<String>,
|
||||
/// Put these paths in the first commit
|
||||
#[arg(value_hint = clap::ValueHint::AnyPath)]
|
||||
#[arg(
|
||||
value_hint = clap::ValueHint::AnyPath,
|
||||
add = ArgValueCandidates::new(complete::modified_files),
|
||||
)]
|
||||
paths: Vec<String>,
|
||||
/// Reset the author to the configured user
|
||||
///
|
||||
|
|
|
@ -57,7 +57,10 @@ pub(crate) struct DiffArgs {
|
|||
#[arg(long, short, conflicts_with = "revision", add = ArgValueCandidates::new(complete::all_revisions))]
|
||||
to: Option<RevisionArg>,
|
||||
/// Restrict the diff to these paths
|
||||
#[arg(value_hint = clap::ValueHint::AnyPath)]
|
||||
#[arg(
|
||||
value_hint = clap::ValueHint::AnyPath,
|
||||
add = ArgValueCandidates::new(complete::modified_revision_or_range_files),
|
||||
)]
|
||||
paths: Vec<String>,
|
||||
#[command(flatten)]
|
||||
format: DiffFormatArgs,
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
// limitations under the License.
|
||||
|
||||
use clap_complete::ArgValueCandidates;
|
||||
use clap_complete::ArgValueCompleter;
|
||||
use jj_lib::annotate::get_annotation_for_file;
|
||||
use jj_lib::annotate::FileAnnotation;
|
||||
use jj_lib::commit::Commit;
|
||||
|
@ -37,7 +38,10 @@ use crate::ui::Ui;
|
|||
#[derive(clap::Args, Clone, Debug)]
|
||||
pub(crate) struct FileAnnotateArgs {
|
||||
/// the file to annotate
|
||||
#[arg(value_hint = clap::ValueHint::AnyPath)]
|
||||
#[arg(
|
||||
value_hint = clap::ValueHint::AnyPath,
|
||||
add = ArgValueCompleter::new(complete::all_revision_files),
|
||||
)]
|
||||
path: String,
|
||||
/// an optional revision to start at
|
||||
#[arg(long, short, add = ArgValueCandidates::new(complete::all_revisions))]
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
// limitations under the License.
|
||||
|
||||
use clap_complete::ArgValueCandidates;
|
||||
use clap_complete::ArgValueCompleter;
|
||||
use jj_lib::backend::TreeValue;
|
||||
use jj_lib::merged_tree::MergedTreeBuilder;
|
||||
use jj_lib::object_id::ObjectId;
|
||||
|
@ -52,7 +53,11 @@ pub(crate) struct FileChmodArgs {
|
|||
)]
|
||||
revision: RevisionArg,
|
||||
/// Paths to change the executable bit for
|
||||
#[arg(required = true, value_hint = clap::ValueHint::AnyPath)]
|
||||
#[arg(
|
||||
required = true,
|
||||
value_hint = clap::ValueHint::AnyPath,
|
||||
add = ArgValueCompleter::new(complete::all_revision_files),
|
||||
)]
|
||||
paths: Vec<String>,
|
||||
}
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ use std::io;
|
|||
use std::io::Write;
|
||||
|
||||
use clap_complete::ArgValueCandidates;
|
||||
use clap_complete::ArgValueCompleter;
|
||||
use jj_lib::backend::BackendResult;
|
||||
use jj_lib::conflicts::materialize_merge_result;
|
||||
use jj_lib::conflicts::materialize_tree_value;
|
||||
|
@ -51,7 +52,11 @@ pub(crate) struct FileShowArgs {
|
|||
)]
|
||||
revision: RevisionArg,
|
||||
/// Paths to print
|
||||
#[arg(required = true, value_hint = clap::ValueHint::FilePath)]
|
||||
#[arg(
|
||||
required = true,
|
||||
value_hint = clap::ValueHint::FilePath,
|
||||
add = ArgValueCompleter::new(complete::all_revision_files),
|
||||
)]
|
||||
paths: Vec<String>,
|
||||
}
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
|
||||
use std::io::Write;
|
||||
|
||||
use clap_complete::ArgValueCompleter;
|
||||
use itertools::Itertools;
|
||||
use jj_lib::merge::Merge;
|
||||
use jj_lib::merged_tree::MergedTreeBuilder;
|
||||
|
@ -24,6 +25,7 @@ use tracing::instrument;
|
|||
use crate::cli_util::CommandHelper;
|
||||
use crate::command_error::user_error_with_hint;
|
||||
use crate::command_error::CommandError;
|
||||
use crate::complete;
|
||||
use crate::ui::Ui;
|
||||
|
||||
/// Stop tracking specified paths in the working copy
|
||||
|
@ -33,7 +35,11 @@ pub(crate) struct FileUntrackArgs {
|
|||
///
|
||||
/// The paths could be ignored via a .gitignore or .git/info/exclude (in
|
||||
/// colocated repos).
|
||||
#[arg(required = true, value_hint = clap::ValueHint::AnyPath)]
|
||||
#[arg(
|
||||
required = true,
|
||||
value_hint = clap::ValueHint::AnyPath,
|
||||
add = ArgValueCompleter::new(complete::all_revision_files),
|
||||
)]
|
||||
paths: Vec<String>,
|
||||
}
|
||||
|
||||
|
|
|
@ -42,7 +42,10 @@ pub(crate) struct InterdiffArgs {
|
|||
#[arg(long, short, add = ArgValueCandidates::new(complete::all_revisions))]
|
||||
to: Option<RevisionArg>,
|
||||
/// Restrict the diff to these paths
|
||||
#[arg(value_hint = clap::ValueHint::AnyPath)]
|
||||
#[arg(
|
||||
value_hint = clap::ValueHint::AnyPath,
|
||||
add = ArgValueCandidates::new(complete::interdiff_files),
|
||||
)]
|
||||
paths: Vec<String>,
|
||||
#[command(flatten)]
|
||||
format: DiffFormatArgs,
|
||||
|
|
|
@ -62,7 +62,10 @@ pub(crate) struct ResolveArgs {
|
|||
/// will attempt to resolve the first conflict we can find. You can use
|
||||
/// the `--list` argument to find paths to use here.
|
||||
// TODO: Find the conflict we can resolve even if it's not the first one.
|
||||
#[arg(value_hint = clap::ValueHint::AnyPath)]
|
||||
#[arg(
|
||||
value_hint = clap::ValueHint::AnyPath,
|
||||
add = ArgValueCandidates::new(complete::revision_conflicted_files),
|
||||
)]
|
||||
paths: Vec<String>,
|
||||
}
|
||||
|
||||
|
|
|
@ -45,7 +45,10 @@ use crate::ui::Ui;
|
|||
#[derive(clap::Args, Clone, Debug)]
|
||||
pub(crate) struct RestoreArgs {
|
||||
/// Restore only these paths (instead of all paths)
|
||||
#[arg(value_hint = clap::ValueHint::AnyPath)]
|
||||
#[arg(
|
||||
value_hint = clap::ValueHint::AnyPath,
|
||||
add = ArgValueCandidates::new(complete::modified_range_files),
|
||||
)]
|
||||
paths: Vec<String>,
|
||||
/// Revision to restore from (source)
|
||||
#[arg(long, short, add = ArgValueCandidates::new(complete::all_revisions))]
|
||||
|
|
|
@ -66,7 +66,10 @@ pub(crate) struct SplitArgs {
|
|||
#[arg(long, short, alias = "siblings")]
|
||||
parallel: bool,
|
||||
/// Put these paths in the first commit
|
||||
#[arg(value_hint = clap::ValueHint::AnyPath)]
|
||||
#[arg(
|
||||
value_hint = clap::ValueHint::AnyPath,
|
||||
add = ArgValueCandidates::new(complete::modified_revision_files),
|
||||
)]
|
||||
paths: Vec<String>,
|
||||
}
|
||||
|
||||
|
|
|
@ -90,7 +90,11 @@ pub(crate) struct SquashArgs {
|
|||
#[arg(long, value_name = "NAME")]
|
||||
tool: Option<String>,
|
||||
/// Move only changes to these paths (instead of all paths)
|
||||
#[arg(conflicts_with_all = ["interactive", "tool"], value_hint = clap::ValueHint::AnyPath)]
|
||||
#[arg(
|
||||
conflicts_with_all = ["interactive", "tool"],
|
||||
value_hint = clap::ValueHint::AnyPath,
|
||||
add = ArgValueCandidates::new(complete::squash_revision_files),
|
||||
)]
|
||||
paths: Vec<String>,
|
||||
/// The source revision will not be abandoned
|
||||
#[arg(long, short)]
|
||||
|
|
|
@ -12,6 +12,8 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::io::BufRead;
|
||||
|
||||
use clap::builder::StyledStr;
|
||||
use clap::FromArgMatches as _;
|
||||
use clap_complete::CompletionCandidate;
|
||||
|
@ -446,6 +448,137 @@ pub fn leaf_config_keys() -> Vec<CompletionCandidate> {
|
|||
config_keys_impl(true)
|
||||
}
|
||||
|
||||
fn all_files_from_rev(rev: String) -> Vec<CompletionCandidate> {
|
||||
with_jj(|jj, _| {
|
||||
let mut child = jj
|
||||
.build()
|
||||
.arg("file")
|
||||
.arg("list")
|
||||
.arg("--revision")
|
||||
.arg(rev)
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.spawn()
|
||||
.map_err(user_error)?;
|
||||
let stdout = child.stdout.take().unwrap();
|
||||
|
||||
Ok(std::io::BufReader::new(stdout)
|
||||
.lines()
|
||||
.take(1_000)
|
||||
.map_while(Result::ok)
|
||||
.map(CompletionCandidate::new)
|
||||
.collect())
|
||||
})
|
||||
}
|
||||
|
||||
fn modified_files_from_rev_with_jj_cmd(
|
||||
rev: (String, Option<String>),
|
||||
mut cmd: std::process::Command,
|
||||
) -> Result<Vec<CompletionCandidate>, CommandError> {
|
||||
cmd.arg("diff").arg("--summary");
|
||||
match rev {
|
||||
(rev, None) => cmd.arg("--revision").arg(rev),
|
||||
(from, Some(to)) => cmd.arg("--from").arg(from).arg("--to").arg(to),
|
||||
};
|
||||
let output = cmd.output().map_err(user_error)?;
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
|
||||
Ok(stdout
|
||||
.lines()
|
||||
.map(|line| {
|
||||
let (mode, path) = line
|
||||
.split_once(' ')
|
||||
.expect("diff --summary should contain a space between mode and path");
|
||||
let help = match mode {
|
||||
"M" => "Modified".into(),
|
||||
"D" => "Deleted".into(),
|
||||
"A" => "Added".into(),
|
||||
"R" => "Renamed".into(),
|
||||
"C" => "Copied".into(),
|
||||
_ => format!("unknown mode: '{mode}'"),
|
||||
};
|
||||
CompletionCandidate::new(path).help(Some(help.into()))
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn modified_files_from_rev(rev: (String, Option<String>)) -> Vec<CompletionCandidate> {
|
||||
with_jj(|jj, _| modified_files_from_rev_with_jj_cmd(rev, jj.build()))
|
||||
}
|
||||
|
||||
fn conflicted_files_from_rev(rev: &str) -> Vec<CompletionCandidate> {
|
||||
with_jj(|jj, _| {
|
||||
let output = jj
|
||||
.build()
|
||||
.arg("resolve")
|
||||
.arg("--list")
|
||||
.arg("--revision")
|
||||
.arg(rev)
|
||||
.output()
|
||||
.map_err(user_error)?;
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
|
||||
Ok(stdout
|
||||
.lines()
|
||||
.filter_map(|line| line.split_whitespace().next())
|
||||
.map(CompletionCandidate::new)
|
||||
.collect())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn modified_files() -> Vec<CompletionCandidate> {
|
||||
modified_files_from_rev(("@".into(), None))
|
||||
}
|
||||
|
||||
pub fn all_revision_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
|
||||
// TODO: Use `current` once `jj file list` gains the ability to list only
|
||||
// the content of the "current" directory.
|
||||
let _ = current;
|
||||
all_files_from_rev(parse::revision_or_wc())
|
||||
}
|
||||
|
||||
pub fn modified_revision_files() -> Vec<CompletionCandidate> {
|
||||
modified_files_from_rev((parse::revision_or_wc(), None))
|
||||
}
|
||||
|
||||
pub fn modified_range_files() -> Vec<CompletionCandidate> {
|
||||
match parse::range() {
|
||||
Some((from, to)) => modified_files_from_rev((from, Some(to))),
|
||||
None => modified_files_from_rev(("@".into(), None)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn modified_revision_or_range_files() -> Vec<CompletionCandidate> {
|
||||
if let Some(rev) = parse::revision() {
|
||||
return modified_files_from_rev((rev, None));
|
||||
}
|
||||
modified_range_files()
|
||||
}
|
||||
|
||||
pub fn revision_conflicted_files() -> Vec<CompletionCandidate> {
|
||||
conflicted_files_from_rev(&parse::revision_or_wc())
|
||||
}
|
||||
|
||||
/// Specific function for completing file paths for `jj squash`
|
||||
pub fn squash_revision_files() -> Vec<CompletionCandidate> {
|
||||
let rev = parse::squash_revision().unwrap_or_else(|| "@".into());
|
||||
modified_files_from_rev((rev, None))
|
||||
}
|
||||
|
||||
/// Specific function for completing file paths for `jj interdiff`
|
||||
pub fn interdiff_files() -> Vec<CompletionCandidate> {
|
||||
let Some((from, to)) = parse::range() else {
|
||||
return Vec::new();
|
||||
};
|
||||
// Complete all modified files in "from" and "to". This will also suggest
|
||||
// files that are the same in both, which is a false positive. This approach
|
||||
// is more lightweight than actually doing a temporary rebase here.
|
||||
with_jj(|jj, _| {
|
||||
let mut res = modified_files_from_rev_with_jj_cmd((from, None), jj.build())?;
|
||||
res.extend(modified_files_from_rev_with_jj_cmd((to, None), jj.build())?);
|
||||
Ok(res)
|
||||
})
|
||||
}
|
||||
|
||||
/// Shell out to jj during dynamic completion generation
|
||||
///
|
||||
/// In case of errors, print them and early return an empty vector.
|
||||
|
@ -577,6 +710,81 @@ impl JjBuilder {
|
|||
}
|
||||
}
|
||||
|
||||
/// Functions for parsing revisions and revision ranges from the command line.
|
||||
/// Parsing is done on a best-effort basis and relies on the heuristic that
|
||||
/// most command line flags are consistent across different subcommands.
|
||||
///
|
||||
/// In some cases, this parsing will be incorrect, but it's not worth the effort
|
||||
/// to fix that. For example, if the user specifies any of the relevant flags
|
||||
/// multiple times, the parsing will pick any of the available ones, while the
|
||||
/// actual execution of the command would fail.
|
||||
mod parse {
|
||||
fn parse_flag(candidates: &[&str], mut args: impl Iterator<Item = String>) -> Option<String> {
|
||||
for arg in args.by_ref() {
|
||||
// -r REV syntax
|
||||
if candidates.contains(&arg.as_ref()) {
|
||||
match args.next() {
|
||||
Some(val) if !val.starts_with('-') => return Some(val),
|
||||
_ => return None,
|
||||
}
|
||||
}
|
||||
|
||||
// -r=REV syntax
|
||||
if let Some(value) = candidates.iter().find_map(|candidate| {
|
||||
let rest = arg.strip_prefix(candidate)?;
|
||||
match rest.strip_prefix('=') {
|
||||
Some(value) => Some(value),
|
||||
|
||||
// -rREV syntax
|
||||
None if candidate.len() == 2 => Some(rest),
|
||||
|
||||
None => None,
|
||||
}
|
||||
}) {
|
||||
return Some(value.into());
|
||||
};
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn parse_revision_impl(args: impl Iterator<Item = String>) -> Option<String> {
|
||||
parse_flag(&["-r", "--revision"], args)
|
||||
}
|
||||
|
||||
pub fn revision() -> Option<String> {
|
||||
parse_revision_impl(std::env::args())
|
||||
}
|
||||
|
||||
pub fn revision_or_wc() -> String {
|
||||
revision().unwrap_or_else(|| "@".into())
|
||||
}
|
||||
|
||||
pub fn parse_range_impl<T>(args: impl Fn() -> T) -> Option<(String, String)>
|
||||
where
|
||||
T: Iterator<Item = String>,
|
||||
{
|
||||
let from = parse_flag(&["-f", "--from"], args())?;
|
||||
let to = parse_flag(&["-t", "--to"], args()).unwrap_or_else(|| "@".into());
|
||||
|
||||
Some((from, to))
|
||||
}
|
||||
|
||||
pub fn range() -> Option<(String, String)> {
|
||||
parse_range_impl(std::env::args)
|
||||
}
|
||||
|
||||
// Special parse function only for `jj squash`. While squash has --from and
|
||||
// --to arguments, only files within --from should be completed, because
|
||||
// the files changed only in some other revision in the range between
|
||||
// --from and --to cannot be squashed into --to like that.
|
||||
pub fn squash_revision() -> Option<String> {
|
||||
if let Some(rev) = parse_flag(&["-r", "--revision"], std::env::args()) {
|
||||
return Some(rev);
|
||||
}
|
||||
parse_flag(&["-f", "--from"], std::env::args())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
@ -586,4 +794,71 @@ mod tests {
|
|||
// Just make sure the schema is parsed without failure.
|
||||
let _ = config_keys();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_revision_impl() {
|
||||
let good_cases: &[&[&str]] = &[
|
||||
&["-r", "foo"],
|
||||
&["--revision", "foo"],
|
||||
&["-r=foo"],
|
||||
&["--revision=foo"],
|
||||
&["preceding_arg", "-r", "foo"],
|
||||
&["-r", "foo", "following_arg"],
|
||||
];
|
||||
for case in good_cases {
|
||||
let args = case.iter().map(|s| s.to_string());
|
||||
assert_eq!(
|
||||
parse::parse_revision_impl(args),
|
||||
Some("foo".into()),
|
||||
"case: {case:?}",
|
||||
);
|
||||
}
|
||||
let bad_cases: &[&[&str]] = &[&[], &["-r"], &["foo"], &["-R", "foo"], &["-R=foo"]];
|
||||
for case in bad_cases {
|
||||
let args = case.iter().map(|s| s.to_string());
|
||||
assert_eq!(parse::parse_revision_impl(args), None, "case: {case:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_range_impl() {
|
||||
let wc_cases: &[&[&str]] = &[
|
||||
&["-f", "foo"],
|
||||
&["--from", "foo"],
|
||||
&["-f=foo"],
|
||||
&["preceding_arg", "-f", "foo"],
|
||||
&["-f", "foo", "following_arg"],
|
||||
];
|
||||
for case in wc_cases {
|
||||
let args = case.iter().map(|s| s.to_string());
|
||||
assert_eq!(
|
||||
parse::parse_range_impl(|| args.clone()),
|
||||
Some(("foo".into(), "@".into())),
|
||||
"case: {case:?}",
|
||||
);
|
||||
}
|
||||
let to_cases: &[&[&str]] = &[
|
||||
&["-f", "foo", "-t", "bar"],
|
||||
&["-f", "foo", "--to", "bar"],
|
||||
&["-f=foo", "-t=bar"],
|
||||
&["-t=bar", "-f=foo"],
|
||||
];
|
||||
for case in to_cases {
|
||||
let args = case.iter().map(|s| s.to_string());
|
||||
assert_eq!(
|
||||
parse::parse_range_impl(|| args.clone()),
|
||||
Some(("foo".into(), "bar".into())),
|
||||
"case: {case:?}",
|
||||
);
|
||||
}
|
||||
let bad_cases: &[&[&str]] = &[&[], &["-f"], &["foo"], &["-R", "foo"], &["-R=foo"]];
|
||||
for case in bad_cases {
|
||||
let args = case.iter().map(|s| s.to_string());
|
||||
assert_eq!(
|
||||
parse::parse_range_impl(|| args.clone()),
|
||||
None,
|
||||
"case: {case:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -534,3 +534,215 @@ fn test_config() {
|
|||
core.watchman.register_snapshot_trigger Whether to use triggers to monitor for changes in the background.
|
||||
");
|
||||
}
|
||||
|
||||
fn create_commit(
|
||||
test_env: &TestEnvironment,
|
||||
repo_path: &std::path::Path,
|
||||
name: &str,
|
||||
parents: &[&str],
|
||||
files: &[(&str, Option<&str>)],
|
||||
) {
|
||||
if parents.is_empty() {
|
||||
test_env.jj_cmd_ok(repo_path, &["new", "root()", "-m", name]);
|
||||
} else {
|
||||
let mut args = vec!["new", "-m", name];
|
||||
args.extend(parents);
|
||||
test_env.jj_cmd_ok(repo_path, &args);
|
||||
}
|
||||
for (name, content) in files {
|
||||
match content {
|
||||
Some(content) => std::fs::write(repo_path.join(name), content).unwrap(),
|
||||
None => std::fs::remove_file(repo_path.join(name)).unwrap(),
|
||||
}
|
||||
}
|
||||
test_env.jj_cmd_ok(repo_path, &["bookmark", "create", name]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_files() {
|
||||
let test_env = TestEnvironment::default();
|
||||
test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "repo"]);
|
||||
let repo_path = test_env.env_root().join("repo");
|
||||
|
||||
create_commit(
|
||||
&test_env,
|
||||
&repo_path,
|
||||
"first",
|
||||
&[],
|
||||
&[
|
||||
("f_unchanged", Some("unchanged\n")),
|
||||
("f_modified", Some("not_yet_modified\n")),
|
||||
("f_not_yet_renamed", Some("renamed\n")),
|
||||
("f_deleted", Some("not_yet_deleted\n")),
|
||||
// not yet: "added" file
|
||||
],
|
||||
);
|
||||
create_commit(
|
||||
&test_env,
|
||||
&repo_path,
|
||||
"second",
|
||||
&["first"],
|
||||
&[
|
||||
// "unchanged" file
|
||||
("f_modified", Some("modified\n")),
|
||||
("f_renamed", Some("renamed\n")),
|
||||
("f_deleted", None),
|
||||
("f_added", Some("added\n")),
|
||||
],
|
||||
);
|
||||
|
||||
// create a conflicted commit to check the completions of `jj restore`
|
||||
create_commit(
|
||||
&test_env,
|
||||
&repo_path,
|
||||
"conflicted",
|
||||
&["second"],
|
||||
&[
|
||||
("f_modified", Some("modified_again\n")),
|
||||
("f_added_2", Some("added_2\n")),
|
||||
],
|
||||
);
|
||||
test_env.jj_cmd_ok(&repo_path, &["rebase", "-r=@", "-d=first"]);
|
||||
|
||||
// two commits that are similar but not identical, for `jj interdiff`
|
||||
create_commit(
|
||||
&test_env,
|
||||
&repo_path,
|
||||
"interdiff_from",
|
||||
&[],
|
||||
&[
|
||||
("f_interdiff_same", Some("same in both commits\n")),
|
||||
(("f_interdiff_only_from"), Some("only from\n")),
|
||||
],
|
||||
);
|
||||
create_commit(
|
||||
&test_env,
|
||||
&repo_path,
|
||||
"interdiff_to",
|
||||
&[],
|
||||
&[
|
||||
("f_interdiff_same", Some("same in both commits\n")),
|
||||
(("f_interdiff_only_to"), Some("only to\n")),
|
||||
],
|
||||
);
|
||||
|
||||
// "dirty worktree"
|
||||
create_commit(
|
||||
&test_env,
|
||||
&repo_path,
|
||||
"working_copy",
|
||||
&["second"],
|
||||
&[
|
||||
("f_modified", Some("modified_again\n")),
|
||||
("f_added_2", Some("added_2\n")),
|
||||
],
|
||||
);
|
||||
|
||||
let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-r", "all()", "--summary"]);
|
||||
insta::assert_snapshot!(stdout, @r"
|
||||
@ wqnwkozp test.user@example.com 2001-02-03 08:05:20 working_copy 89d772f3
|
||||
│ working_copy
|
||||
│ A f_added_2
|
||||
│ M f_modified
|
||||
○ zsuskuln test.user@example.com 2001-02-03 08:05:11 second 12ffc2f7
|
||||
│ second
|
||||
│ A f_added
|
||||
│ D f_deleted
|
||||
│ M f_modified
|
||||
│ A f_renamed
|
||||
│ × royxmykx test.user@example.com 2001-02-03 08:05:14 conflicted 14453858 conflict
|
||||
├─╯ conflicted
|
||||
│ A f_added_2
|
||||
│ M f_modified
|
||||
○ rlvkpnrz test.user@example.com 2001-02-03 08:05:09 first 2a2f433c
|
||||
│ first
|
||||
│ A f_deleted
|
||||
│ A f_modified
|
||||
│ A f_not_yet_renamed
|
||||
│ A f_unchanged
|
||||
│ ○ kpqxywon test.user@example.com 2001-02-03 08:05:18 interdiff_to 302c4041
|
||||
├─╯ interdiff_to
|
||||
│ A f_interdiff_only_to
|
||||
│ A f_interdiff_same
|
||||
│ ○ yostqsxw test.user@example.com 2001-02-03 08:05:16 interdiff_from 083d1cc6
|
||||
├─╯ interdiff_from
|
||||
│ A f_interdiff_only_from
|
||||
│ A f_interdiff_same
|
||||
◆ zzzzzzzz root() 00000000
|
||||
");
|
||||
|
||||
let mut test_env = test_env;
|
||||
test_env.add_env_var("COMPLETE", "fish");
|
||||
let test_env = test_env;
|
||||
|
||||
let stdout = test_env.jj_cmd_success(&repo_path, &["--", "jj", "file", "show", "f_"]);
|
||||
insta::assert_snapshot!(stdout, @r"
|
||||
f_added
|
||||
f_added_2
|
||||
f_modified
|
||||
f_not_yet_renamed
|
||||
f_renamed
|
||||
f_unchanged
|
||||
");
|
||||
|
||||
let stdout =
|
||||
test_env.jj_cmd_success(&repo_path, &["--", "jj", "file", "annotate", "-r@-", "f_"]);
|
||||
insta::assert_snapshot!(stdout, @r"
|
||||
f_added
|
||||
f_modified
|
||||
f_not_yet_renamed
|
||||
f_renamed
|
||||
f_unchanged
|
||||
");
|
||||
|
||||
let stdout = test_env.jj_cmd_success(&repo_path, &["--", "jj", "diff", "-r", "@-", "f_"]);
|
||||
insta::assert_snapshot!(stdout, @r"
|
||||
f_added Added
|
||||
f_deleted Deleted
|
||||
f_modified Modified
|
||||
f_renamed Added
|
||||
");
|
||||
let stdout = test_env.jj_cmd_success(
|
||||
&repo_path,
|
||||
&["--", "jj", "diff", "--from", "root()", "--to", "@-", "f_"],
|
||||
);
|
||||
insta::assert_snapshot!(stdout, @r"
|
||||
f_added Added
|
||||
f_modified Added
|
||||
f_not_yet_renamed Added
|
||||
f_renamed Added
|
||||
f_unchanged Added
|
||||
");
|
||||
|
||||
// interdiff has a different behavior with --from and --to flags
|
||||
let stdout = test_env.jj_cmd_success(
|
||||
&repo_path,
|
||||
&[
|
||||
"--",
|
||||
"jj",
|
||||
"interdiff",
|
||||
"--to=interdiff_to",
|
||||
"--from=interdiff_from",
|
||||
"f_",
|
||||
],
|
||||
);
|
||||
insta::assert_snapshot!(stdout, @r"
|
||||
f_interdiff_only_from Added
|
||||
f_interdiff_same Added
|
||||
f_interdiff_only_to Added
|
||||
f_interdiff_same Added
|
||||
");
|
||||
|
||||
// squash has a different behavior with --from and --to flags
|
||||
let stdout = test_env.jj_cmd_success(&repo_path, &["--", "jj", "squash", "-f=first", "f_"]);
|
||||
insta::assert_snapshot!(stdout, @r"
|
||||
f_deleted Added
|
||||
f_modified Added
|
||||
f_not_yet_renamed Added
|
||||
f_unchanged Added
|
||||
");
|
||||
|
||||
let stdout =
|
||||
test_env.jj_cmd_success(&repo_path, &["--", "jj", "resolve", "-r=conflicted", "f_"]);
|
||||
insta::assert_snapshot!(stdout, @"f_modified");
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue