forked from mirrors/jj
revset: implement file(pattern[, candidates]) predicate
The name "file()" is just copied from hg. I'm not sure if it's good in jj's context, but I couldn't find a better name.
This commit is contained in:
parent
d5cf2582dc
commit
cb2fcde560
6 changed files with 110 additions and 6 deletions
|
@ -14,6 +14,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
* `jj git push` will search `@-` for branches to push if `@` has none.
|
||||
|
||||
* The new revset function `file(pattern[, x])` finds commits modifying the
|
||||
paths specified by the `pattern`.
|
||||
|
||||
### Fixed bugs
|
||||
|
||||
* `jj edit root` now fails gracefully.
|
||||
|
|
|
@ -112,6 +112,9 @@ revsets (expressions) as arguments.
|
|||
* `committer(needle[, x])`: Commits with the given string in the committer's
|
||||
name or email. If a second argument was provided, then only commits in that
|
||||
set are considered, otherwise all visible commits are considered.
|
||||
* `file(pattern[, x])`: Commits modifying the paths specified by the `pattern`.
|
||||
If a second argument was provided, then only commits in that set are
|
||||
considered, otherwise all visible commits are considered.
|
||||
|
||||
|
||||
## Examples
|
||||
|
|
|
@ -17,6 +17,7 @@ use std::cmp::{Ordering, Reverse};
|
|||
use std::collections::HashSet;
|
||||
use std::iter::Peekable;
|
||||
use std::ops::Range;
|
||||
use std::path::Path;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
|
||||
|
@ -29,9 +30,10 @@ use thiserror::Error;
|
|||
use crate::backend::{BackendError, BackendResult, CommitId};
|
||||
use crate::commit::Commit;
|
||||
use crate::index::{HexPrefix, IndexEntry, IndexPosition, PrefixResolution, RevWalk};
|
||||
use crate::matchers::Matcher;
|
||||
use crate::matchers::{Matcher, PrefixMatcher};
|
||||
use crate::op_store::WorkspaceId;
|
||||
use crate::repo::RepoRef;
|
||||
use crate::repo_path::{FsPathParseError, RepoPath};
|
||||
use crate::revset_graph_iterator::RevsetGraphIterator;
|
||||
use crate::rewrite;
|
||||
use crate::store::Store;
|
||||
|
@ -44,6 +46,10 @@ pub enum RevsetError {
|
|||
AmbiguousCommitIdPrefix(String),
|
||||
#[error("Change id prefix \"{0}\" is ambiguous")]
|
||||
AmbiguousChangeIdPrefix(String),
|
||||
#[error("Invalid file pattern: {0}")]
|
||||
FsPathParseError(#[from] FsPathParseError),
|
||||
#[error("Cannot resolve file pattern without workspace")]
|
||||
FsPathWithoutWorkspace,
|
||||
#[error("Unexpected error from store: {0}")]
|
||||
StoreError(#[from] BackendError),
|
||||
}
|
||||
|
@ -244,6 +250,10 @@ pub enum RevsetExpression {
|
|||
needle: String,
|
||||
candidates: Rc<RevsetExpression>,
|
||||
},
|
||||
File {
|
||||
pattern: String,
|
||||
candidates: Rc<RevsetExpression>,
|
||||
},
|
||||
Union(Rc<RevsetExpression>, Rc<RevsetExpression>),
|
||||
Intersection(Rc<RevsetExpression>, Rc<RevsetExpression>),
|
||||
Difference(Rc<RevsetExpression>, Rc<RevsetExpression>),
|
||||
|
@ -392,6 +402,14 @@ impl RevsetExpression {
|
|||
})
|
||||
}
|
||||
|
||||
/// Commits in `self` modifying the paths specified by the `pattern`.
|
||||
pub fn with_file(self: &Rc<RevsetExpression>, pattern: String) -> Rc<RevsetExpression> {
|
||||
Rc::new(RevsetExpression::File {
|
||||
candidates: self.clone(),
|
||||
pattern,
|
||||
})
|
||||
}
|
||||
|
||||
/// Commits that are in `self` or in `other` (or both).
|
||||
pub fn union(
|
||||
self: &Rc<RevsetExpression>,
|
||||
|
@ -748,7 +766,7 @@ fn parse_function_expression(
|
|||
};
|
||||
Ok(candidates.with_parent_count(2..u32::MAX))
|
||||
}
|
||||
"description" | "author" | "committer" => {
|
||||
"description" | "author" | "committer" | "file" => {
|
||||
if !(1..=2).contains(&arg_count) {
|
||||
return Err(RevsetParseError::InvalidFunctionArguments {
|
||||
name,
|
||||
|
@ -768,6 +786,7 @@ fn parse_function_expression(
|
|||
"description" => Ok(candidates.with_description(needle)),
|
||||
"author" => Ok(candidates.with_author(needle)),
|
||||
"committer" => Ok(candidates.with_committer(needle)),
|
||||
"file" => Ok(candidates.with_file(needle)),
|
||||
_ => {
|
||||
panic!("unexpected function name: {}", name)
|
||||
}
|
||||
|
@ -1140,7 +1159,9 @@ impl<'revset, 'repo> Iterator for DifferenceRevsetIterator<'revset, 'repo> {
|
|||
/// Workspace information needed to evaluate revset expression.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct RevsetWorkspaceContext<'a> {
|
||||
pub cwd: &'a Path,
|
||||
pub workspace_id: &'a WorkspaceId,
|
||||
pub workspace_root: &'a Path,
|
||||
}
|
||||
|
||||
pub fn evaluate_expression<'repo>(
|
||||
|
@ -1338,6 +1359,20 @@ pub fn evaluate_expression<'repo>(
|
|||
}),
|
||||
}))
|
||||
}
|
||||
RevsetExpression::File {
|
||||
pattern,
|
||||
candidates,
|
||||
} => {
|
||||
if let Some(ctx) = workspace_ctx {
|
||||
// TODO: Add support for globs and other formats
|
||||
let path = RepoPath::parse_fs_path(ctx.cwd, ctx.workspace_root, pattern)?;
|
||||
let matcher: Box<dyn Matcher> = Box::new(PrefixMatcher::new(&[path]));
|
||||
let candidates = candidates.evaluate(repo, workspace_ctx)?;
|
||||
Ok(filter_by_diff(repo, matcher, candidates))
|
||||
} else {
|
||||
Err(RevsetError::FsPathWithoutWorkspace)
|
||||
}
|
||||
}
|
||||
RevsetExpression::Union(expression1, expression2) => {
|
||||
let set1 = expression1.evaluate(repo, workspace_ctx)?;
|
||||
let set2 = expression2.evaluate(repo, workspace_ctx)?;
|
||||
|
@ -1490,6 +1525,13 @@ mod tests {
|
|||
needle: "needle".to_string()
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
foo_symbol.with_file("pattern".to_string()),
|
||||
Rc::new(RevsetExpression::File {
|
||||
candidates: foo_symbol.clone(),
|
||||
pattern: "pattern".to_string(),
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
foo_symbol.union(&wc_symbol),
|
||||
Rc::new(RevsetExpression::Union(
|
||||
|
|
|
@ -12,6 +12,8 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use jujutsu_lib::backend::{CommitId, MillisSinceEpoch, Signature, Timestamp};
|
||||
use jujutsu_lib::commit_builder::CommitBuilder;
|
||||
use jujutsu_lib::matchers::{FilesMatcher, Matcher};
|
||||
|
@ -434,10 +436,13 @@ fn resolve_commit_ids_in_workspace(
|
|||
repo: RepoRef,
|
||||
revset_str: &str,
|
||||
workspace: &Workspace,
|
||||
cwd: Option<&Path>,
|
||||
) -> Vec<CommitId> {
|
||||
let expression = parse(revset_str).unwrap();
|
||||
let workspace_ctx = RevsetWorkspaceContext {
|
||||
cwd: cwd.unwrap_or_else(|| workspace.workspace_root()),
|
||||
workspace_id: workspace.workspace_id(),
|
||||
workspace_root: workspace.workspace_root(),
|
||||
};
|
||||
expression
|
||||
.evaluate(repo, Some(&workspace_ctx))
|
||||
|
@ -471,7 +476,12 @@ fn test_evaluate_expression_root_and_checkout(use_git: bool) {
|
|||
.set_wc_commit(WorkspaceId::default(), commit1.id().clone())
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
resolve_commit_ids_in_workspace(mut_repo.as_repo_ref(), "@", &test_workspace.workspace),
|
||||
resolve_commit_ids_in_workspace(
|
||||
mut_repo.as_repo_ref(),
|
||||
"@",
|
||||
&test_workspace.workspace,
|
||||
None,
|
||||
),
|
||||
vec![commit1.id().clone()]
|
||||
);
|
||||
}
|
||||
|
@ -625,7 +635,12 @@ fn test_evaluate_expression_parents(use_git: bool) {
|
|||
.set_wc_commit(WorkspaceId::default(), commit2.id().clone())
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
resolve_commit_ids_in_workspace(mut_repo.as_repo_ref(), "@-", &test_workspace.workspace),
|
||||
resolve_commit_ids_in_workspace(
|
||||
mut_repo.as_repo_ref(),
|
||||
"@-",
|
||||
&test_workspace.workspace,
|
||||
None,
|
||||
),
|
||||
vec![commit1.id().clone()]
|
||||
);
|
||||
|
||||
|
@ -1739,8 +1754,8 @@ fn test_evaluate_expression_difference(use_git: bool) {
|
|||
#[test_case(true ; "git backend")]
|
||||
fn test_filter_by_diff(use_git: bool) {
|
||||
let settings = testutils::user_settings();
|
||||
let test_repo = TestRepo::init(use_git);
|
||||
let repo = &test_repo.repo;
|
||||
let test_workspace = TestWorkspace::init(&settings, use_git);
|
||||
let repo = &test_workspace.repo;
|
||||
|
||||
let mut tx = repo.start_transaction("test");
|
||||
let mut_repo = tx.mut_repo();
|
||||
|
@ -1785,6 +1800,7 @@ fn test_filter_by_diff(use_git: bool) {
|
|||
CommitBuilder::for_new_commit(&settings, vec![commit2.id().clone()], tree3.id().clone())
|
||||
.write_to_repo(mut_repo);
|
||||
|
||||
// matcher API:
|
||||
let resolve = |file_path: &RepoPath| -> Vec<CommitId> {
|
||||
let repo_ref = mut_repo.as_repo_ref();
|
||||
let matcher = FilesMatcher::new([file_path.clone()].into());
|
||||
|
@ -1809,4 +1825,24 @@ fn test_filter_by_diff(use_git: bool) {
|
|||
commit1.id().clone()
|
||||
]
|
||||
);
|
||||
|
||||
// file() revset:
|
||||
assert_eq!(
|
||||
resolve_commit_ids_in_workspace(
|
||||
mut_repo.as_repo_ref(),
|
||||
r#"file("repo/added_clean_clean")"#,
|
||||
&test_workspace.workspace,
|
||||
Some(test_workspace.workspace.workspace_root().parent().unwrap()),
|
||||
),
|
||||
vec![commit1.id().clone()]
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_commit_ids_in_workspace(
|
||||
mut_repo.as_repo_ref(),
|
||||
&format!(r#"file("added_modified_clean", {}:)"#, commit2.id().hex()),
|
||||
&test_workspace.workspace,
|
||||
Some(test_workspace.workspace.workspace_root()),
|
||||
),
|
||||
vec![commit2.id().clone()]
|
||||
);
|
||||
}
|
||||
|
|
|
@ -590,7 +590,9 @@ impl WorkspaceCommandHelper {
|
|||
revset_expression: &RevsetExpression,
|
||||
) -> Result<Box<dyn Revset<'repo> + 'repo>, RevsetError> {
|
||||
let workspace_ctx = RevsetWorkspaceContext {
|
||||
cwd: &self.cwd,
|
||||
workspace_id: self.workspace.workspace_id(),
|
||||
workspace_root: self.workspace.workspace_root(),
|
||||
};
|
||||
revset_expression.evaluate(self.repo.as_repo_ref(), Some(&workspace_ctx))
|
||||
}
|
||||
|
|
|
@ -197,6 +197,24 @@ fn test_log_filtered_by_path() {
|
|||
second
|
||||
A file2
|
||||
"###);
|
||||
|
||||
// file() revset doesn't filter the diff.
|
||||
let stdout = test_env.jj_cmd_success(
|
||||
&repo_path,
|
||||
&[
|
||||
"log",
|
||||
"-T",
|
||||
"description",
|
||||
"-s",
|
||||
"-rfile(file2)",
|
||||
"--no-graph",
|
||||
],
|
||||
);
|
||||
insta::assert_snapshot!(stdout, @r###"
|
||||
second
|
||||
M file1
|
||||
A file2
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
Loading…
Reference in a new issue