revsets: add syntax for a particular workspace's checkout (#13)

Because we record each workspace's checkout in the repo view, we can
-- unlike other VCSs -- let the user refer to any workspace's checkout
in revsets. This patch adds syntax for that, so you can show the
contents of the checkout in workspace "foo" with `jj show foo@`. That
won't automatically commit that workspace's working copy, however.
This commit is contained in:
Martin von Zweigbergk 2022-02-02 08:15:25 -08:00
parent 5b46fa3282
commit 012b4c4d8e
5 changed files with 138 additions and 68 deletions

View File

@ -27,6 +27,7 @@ use thiserror::Error;
use crate::backend::{BackendError, BackendResult, CommitId};
use crate::commit::Commit;
use crate::index::{HexPrefix, IndexEntry, IndexPosition, PrefixResolution, RevWalk};
use crate::op_store::WorkspaceId;
use crate::repo::RepoRef;
use crate::revset_graph_iterator::RevsetGraphIterator;
use crate::store::Store;
@ -102,7 +103,7 @@ fn resolve_change_id(repo: RepoRef, change_id_prefix: &str) -> Result<Vec<Commit
let mut found_change_id = None;
let mut commit_ids = vec![];
// TODO: Create a persistent lookup from change id to (visible?) commit ids.
for index_entry in RevsetExpression::all().evaluate(repo).unwrap().iter() {
for index_entry in RevsetExpression::all().evaluate(repo, None).unwrap().iter() {
let change_id = index_entry.change_id();
if change_id.hex().starts_with(hex_prefix.hex()) {
if let Some(previous_change_id) = found_change_id.replace(change_id.clone()) {
@ -124,9 +125,26 @@ fn resolve_change_id(repo: RepoRef, change_id_prefix: &str) -> Result<Vec<Commit
}
}
pub fn resolve_symbol(repo: RepoRef, symbol: &str) -> Result<Vec<CommitId>, RevsetError> {
if symbol == "@" {
Ok(vec![repo.view().checkout().clone()])
pub fn resolve_symbol(
repo: RepoRef,
symbol: &str,
workspace_id: Option<&WorkspaceId>,
) -> Result<Vec<CommitId>, RevsetError> {
if symbol.ends_with('@') {
let target_workspace = if symbol == "@" {
if let Some(workspace_id) = workspace_id {
workspace_id.clone()
} else {
return Err(RevsetError::NoSuchRevision(symbol.to_owned()));
}
} else {
WorkspaceId::new(symbol.strip_suffix('@').unwrap().to_string())
};
if let Some(commit_id) = repo.view().get_checkout(&target_workspace) {
Ok(vec![commit_id.clone()])
} else {
Err(RevsetError::NoSuchRevision(symbol.to_owned()))
}
} else if symbol == "root" {
Ok(vec![repo.store().root_commit_id().clone()])
} else {
@ -387,8 +405,9 @@ impl RevsetExpression {
pub fn evaluate<'repo>(
&self,
repo: RepoRef<'repo>,
workspace_id: Option<&WorkspaceId>,
) -> Result<Box<dyn Revset<'repo> + 'repo>, RevsetError> {
evaluate_expression(repo, self)
evaluate_expression(repo, self, workspace_id)
}
}
@ -1069,6 +1088,7 @@ impl<'revset, 'repo> Iterator for DifferenceRevsetIterator<'revset, 'repo> {
pub fn evaluate_expression<'repo>(
repo: RepoRef<'repo>,
expression: &RevsetExpression,
workspace_id: Option<&WorkspaceId>,
) -> Result<Box<dyn Revset<'repo> + 'repo>, RevsetError> {
match expression {
RevsetExpression::None => Ok(Box::new(EagerRevset {
@ -1076,12 +1096,12 @@ pub fn evaluate_expression<'repo>(
})),
RevsetExpression::Commits(commit_ids) => Ok(revset_for_commit_ids(repo, commit_ids)),
RevsetExpression::Symbol(symbol) => {
let commit_ids = resolve_symbol(repo, symbol)?;
evaluate_expression(repo, &RevsetExpression::Commits(commit_ids))
let commit_ids = resolve_symbol(repo, symbol, workspace_id)?;
evaluate_expression(repo, &RevsetExpression::Commits(commit_ids), workspace_id)
}
RevsetExpression::Parents(base_expression) => {
// TODO: Make this lazy
let base_set = base_expression.evaluate(repo)?;
let base_set = base_expression.evaluate(repo, workspace_id)?;
let mut parent_entries = base_set
.iter()
.flat_map(|entry| entry.parents())
@ -1093,9 +1113,9 @@ pub fn evaluate_expression<'repo>(
}))
}
RevsetExpression::Children(roots) => {
let root_set = roots.evaluate(repo)?;
let root_set = roots.evaluate(repo, workspace_id)?;
let candidates_expression = roots.descendants();
let candidate_set = candidates_expression.evaluate(repo)?;
let candidate_set = candidates_expression.evaluate(repo, workspace_id)?;
Ok(Box::new(ChildrenRevset {
root_set,
candidate_set,
@ -1103,11 +1123,11 @@ pub fn evaluate_expression<'repo>(
}
RevsetExpression::Ancestors(base_expression) => RevsetExpression::none()
.range(base_expression)
.evaluate(repo),
.evaluate(repo, workspace_id),
RevsetExpression::Range { roots, heads } => {
let root_set = roots.evaluate(repo)?;
let root_set = roots.evaluate(repo, workspace_id)?;
let root_ids = root_set.iter().commit_ids().collect_vec();
let head_set = heads.evaluate(repo)?;
let head_set = heads.evaluate(repo, workspace_id)?;
let head_ids = head_set.iter().commit_ids().collect_vec();
let walk = repo.index().walk_revs(&head_ids, &root_ids);
Ok(Box::new(RevWalkRevset { walk }))
@ -1116,8 +1136,8 @@ pub fn evaluate_expression<'repo>(
// reverse
#[allow(clippy::needless_collect)]
RevsetExpression::DagRange { roots, heads } => {
let root_set = roots.evaluate(repo)?;
let candidate_set = heads.ancestors().evaluate(repo)?;
let root_set = roots.evaluate(repo, workspace_id)?;
let candidate_set = heads.ancestors().evaluate(repo, workspace_id)?;
let mut reachable: HashSet<_> = root_set.iter().map(|entry| entry.position()).collect();
let mut result = vec![];
let candidates = candidate_set.iter().collect_vec();
@ -1142,7 +1162,7 @@ pub fn evaluate_expression<'repo>(
&repo.view().heads().iter().cloned().collect_vec(),
)),
RevsetExpression::HeadsOf(candidates) => {
let candidate_set = candidates.evaluate(repo)?;
let candidate_set = candidates.evaluate(repo, workspace_id)?;
let candidate_ids = candidate_set.iter().commit_ids().collect_vec();
Ok(revset_for_commit_ids(
repo,
@ -1153,7 +1173,7 @@ pub fn evaluate_expression<'repo>(
candidates,
parent_count_range,
} => {
let candidates = candidates.evaluate(repo)?;
let candidates = candidates.evaluate(repo, workspace_id)?;
let parent_count_range = parent_count_range.clone();
Ok(Box::new(FilterRevset {
candidates,
@ -1201,7 +1221,7 @@ pub fn evaluate_expression<'repo>(
Ok(revset_for_commit_ids(repo, &commit_ids))
}
RevsetExpression::Description { needle, candidates } => {
let candidates = candidates.evaluate(repo)?;
let candidates = candidates.evaluate(repo, workspace_id)?;
let repo = repo;
let needle = needle.clone();
Ok(Box::new(FilterRevset {
@ -1216,7 +1236,7 @@ pub fn evaluate_expression<'repo>(
}))
}
RevsetExpression::Author { needle, candidates } => {
let candidates = candidates.evaluate(repo)?;
let candidates = candidates.evaluate(repo, workspace_id)?;
let repo = repo;
let needle = needle.clone();
// TODO: Make these functions that take a needle to search for accept some
@ -1232,7 +1252,7 @@ pub fn evaluate_expression<'repo>(
}))
}
RevsetExpression::Committer { needle, candidates } => {
let candidates = candidates.evaluate(repo)?;
let candidates = candidates.evaluate(repo, workspace_id)?;
let repo = repo;
let needle = needle.clone();
Ok(Box::new(FilterRevset {
@ -1245,18 +1265,18 @@ pub fn evaluate_expression<'repo>(
}))
}
RevsetExpression::Union(expression1, expression2) => {
let set1 = expression1.evaluate(repo)?;
let set2 = expression2.evaluate(repo)?;
let set1 = expression1.evaluate(repo, workspace_id)?;
let set2 = expression2.evaluate(repo, workspace_id)?;
Ok(Box::new(UnionRevset { set1, set2 }))
}
RevsetExpression::Intersection(expression1, expression2) => {
let set1 = expression1.evaluate(repo)?;
let set2 = expression2.evaluate(repo)?;
let set1 = expression1.evaluate(repo, workspace_id)?;
let set2 = expression2.evaluate(repo, workspace_id)?;
Ok(Box::new(IntersectionRevset { set1, set2 }))
}
RevsetExpression::Difference(expression1, expression2) => {
let set1 = expression1.evaluate(repo)?;
let set2 = expression2.evaluate(repo)?;
let set1 = expression1.evaluate(repo, workspace_id)?;
let set2 = expression2.evaluate(repo, workspace_id)?;
Ok(Box::new(DifferenceRevset { set1, set2 }))
}
}

View File

@ -156,7 +156,7 @@ impl<'settings, 'repo> DescendantRebaser<'settings, 'repo> {
.parents()
.minus(&old_commits_expression);
let heads_to_add = heads_to_add_expression
.evaluate(mut_repo.as_repo_ref())
.evaluate(mut_repo.as_repo_ref(), None)
.unwrap()
.iter()
.commit_ids()
@ -164,7 +164,7 @@ impl<'settings, 'repo> DescendantRebaser<'settings, 'repo> {
let to_visit_expression = old_commits_expression.descendants();
let to_visit_revset = to_visit_expression
.evaluate(mut_repo.as_repo_ref())
.evaluate(mut_repo.as_repo_ref(), None)
.unwrap();
let to_visit_entries = to_visit_revset.iter().collect_vec();
drop(to_visit_revset);

View File

@ -29,7 +29,7 @@ fn test_resolve_symbol_root(use_git: bool) {
let repo = &test_workspace.repo;
assert_eq!(
resolve_symbol(repo.as_repo_ref(), "root"),
resolve_symbol(repo.as_repo_ref(), "root", None),
Ok(vec![repo.store().root_commit_id().clone()])
);
}
@ -84,39 +84,39 @@ fn test_resolve_symbol_commit_id() {
// Test lookup by full commit id
let repo_ref = repo.as_repo_ref();
assert_eq!(
resolve_symbol(repo_ref, "0454de3cae04c46cda37ba2e8873b4c17ff51dcb"),
resolve_symbol(repo_ref, "0454de3cae04c46cda37ba2e8873b4c17ff51dcb", None),
Ok(vec![commits[0].id().clone()])
);
assert_eq!(
resolve_symbol(repo_ref, "045f56cd1b17e8abde86771e2705395dcde6a957"),
resolve_symbol(repo_ref, "045f56cd1b17e8abde86771e2705395dcde6a957", None),
Ok(vec![commits[1].id().clone()])
);
assert_eq!(
resolve_symbol(repo_ref, "0468f7da8de2ce442f512aacf83411d26cd2e0cf"),
resolve_symbol(repo_ref, "0468f7da8de2ce442f512aacf83411d26cd2e0cf", None),
Ok(vec![commits[2].id().clone()])
);
// Test commit id prefix
assert_eq!(
resolve_symbol(repo_ref, "046"),
resolve_symbol(repo_ref, "046", None),
Ok(vec![commits[2].id().clone()])
);
assert_eq!(
resolve_symbol(repo_ref, "04"),
resolve_symbol(repo_ref, "04", None),
Err(RevsetError::AmbiguousCommitIdPrefix("04".to_string()))
);
assert_eq!(
resolve_symbol(repo_ref, ""),
resolve_symbol(repo_ref, "", None),
Err(RevsetError::AmbiguousCommitIdPrefix("".to_string()))
);
assert_eq!(
resolve_symbol(repo_ref, "040"),
resolve_symbol(repo_ref, "040", None),
Err(RevsetError::NoSuchRevision("040".to_string()))
);
// Test non-hex string
assert_eq!(
resolve_symbol(repo_ref, "foo"),
resolve_symbol(repo_ref, "foo", None),
Err(RevsetError::NoSuchRevision("foo".to_string()))
);
}
@ -183,19 +183,19 @@ fn test_resolve_symbol_change_id() {
// Test lookup by full change id
let repo_ref = repo.as_repo_ref();
assert_eq!(
resolve_symbol(repo_ref, "04e12a5467bba790efb88a9870894ec2"),
resolve_symbol(repo_ref, "04e12a5467bba790efb88a9870894ec2", None),
Ok(vec![CommitId::from_hex(
"8fd68d104372910e19511df709e5dde62a548720"
)])
);
assert_eq!(
resolve_symbol(repo_ref, "040b3ba3a51d8edbc4c5855cbd09de71"),
resolve_symbol(repo_ref, "040b3ba3a51d8edbc4c5855cbd09de71", None),
Ok(vec![CommitId::from_hex(
"5339432b8e7b90bd3aa1a323db71b8a5c5dcd020"
)])
);
assert_eq!(
resolve_symbol(repo_ref, "04e1c7082e4e34f3f371d8a1a46770b8"),
resolve_symbol(repo_ref, "04e1c7082e4e34f3f371d8a1a46770b8", None),
Ok(vec![CommitId::from_hex(
"e2ad9d861d0ee625851b8ecfcf2c727410e38720"
)])
@ -203,34 +203,34 @@ fn test_resolve_symbol_change_id() {
// Test change id prefix
assert_eq!(
resolve_symbol(repo_ref, "04e12"),
resolve_symbol(repo_ref, "04e12", None),
Ok(vec![CommitId::from_hex(
"8fd68d104372910e19511df709e5dde62a548720"
)])
);
assert_eq!(
resolve_symbol(repo_ref, "04e1c"),
resolve_symbol(repo_ref, "04e1c", None),
Ok(vec![CommitId::from_hex(
"e2ad9d861d0ee625851b8ecfcf2c727410e38720"
)])
);
assert_eq!(
resolve_symbol(repo_ref, "04e1"),
resolve_symbol(repo_ref, "04e1", None),
Err(RevsetError::AmbiguousChangeIdPrefix("04e1".to_string()))
);
assert_eq!(
resolve_symbol(repo_ref, ""),
resolve_symbol(repo_ref, "", None),
// Commit id is checked first, so this is considered an ambiguous commit id
Err(RevsetError::AmbiguousCommitIdPrefix("".to_string()))
);
assert_eq!(
resolve_symbol(repo_ref, "04e13"),
resolve_symbol(repo_ref, "04e13", None),
Err(RevsetError::NoSuchRevision("04e13".to_string()))
);
// Test non-hex string
assert_eq!(
resolve_symbol(repo_ref, "foo"),
resolve_symbol(repo_ref, "foo", None),
Err(RevsetError::NoSuchRevision("foo".to_string()))
);
}
@ -248,14 +248,40 @@ fn test_resolve_symbol_checkout(use_git: bool) {
let commit1 = testutils::create_random_commit(&settings, repo).write_to_repo(mut_repo);
let commit2 = testutils::create_random_commit(&settings, repo).write_to_repo(mut_repo);
mut_repo.set_checkout(WorkspaceId::default(), commit1.id().clone());
let ws1 = WorkspaceId::new("ws1".to_string());
let ws2 = WorkspaceId::new("ws2".to_string());
mut_repo.remove_checkout(&WorkspaceId::default());
// With no workspaces, no variation can be resolved
assert_eq!(
resolve_symbol(mut_repo.as_repo_ref(), "@"),
resolve_symbol(mut_repo.as_repo_ref(), "@", None),
Err(RevsetError::NoSuchRevision("@".to_string()))
);
assert_eq!(
resolve_symbol(mut_repo.as_repo_ref(), "@", Some(&ws1)),
Err(RevsetError::NoSuchRevision("@".to_string()))
);
assert_eq!(
resolve_symbol(mut_repo.as_repo_ref(), "ws1@", Some(&ws1)),
Err(RevsetError::NoSuchRevision("ws1@".to_string()))
);
// Add some workspaces
mut_repo.set_checkout(ws1.clone(), commit1.id().clone());
mut_repo.set_checkout(ws2, commit2.id().clone());
// @ cannot be resolved without a default workspace ID
assert_eq!(
resolve_symbol(mut_repo.as_repo_ref(), "@", None),
Err(RevsetError::NoSuchRevision("@".to_string()))
);
// Can resolve "@" shorthand with a default workspace ID
assert_eq!(
resolve_symbol(mut_repo.as_repo_ref(), "@", Some(&ws1)),
Ok(vec![commit1.id().clone()])
);
mut_repo.set_checkout(WorkspaceId::default(), commit2.id().clone());
// Can resolve an explicit checkout
assert_eq!(
resolve_symbol(mut_repo.as_repo_ref(), "@"),
resolve_symbol(mut_repo.as_repo_ref(), "ws2@", Some(&ws1)),
Ok(vec![commit2.id().clone()])
);
}
@ -301,7 +327,7 @@ fn test_resolve_symbol_git_refs() {
// Non-existent ref
assert_eq!(
resolve_symbol(mut_repo.as_repo_ref(), "non-existent"),
resolve_symbol(mut_repo.as_repo_ref(), "non-existent", None),
Err(RevsetError::NoSuchRevision("non-existent".to_string()))
);
@ -311,7 +337,7 @@ fn test_resolve_symbol_git_refs() {
RefTarget::Normal(commit4.id().clone()),
);
assert_eq!(
resolve_symbol(mut_repo.as_repo_ref(), "refs/heads/branch"),
resolve_symbol(mut_repo.as_repo_ref(), "refs/heads/branch", None),
Ok(vec![commit4.id().clone()])
);
@ -325,7 +351,7 @@ fn test_resolve_symbol_git_refs() {
RefTarget::Normal(commit4.id().clone()),
);
assert_eq!(
resolve_symbol(mut_repo.as_repo_ref(), "heads/branch"),
resolve_symbol(mut_repo.as_repo_ref(), "heads/branch", None),
Ok(vec![commit5.id().clone()])
);
@ -339,7 +365,7 @@ fn test_resolve_symbol_git_refs() {
RefTarget::Normal(commit4.id().clone()),
);
assert_eq!(
resolve_symbol(mut_repo.as_repo_ref(), "branch"),
resolve_symbol(mut_repo.as_repo_ref(), "branch", None),
Ok(vec![commit3.id().clone()])
);
@ -349,7 +375,7 @@ fn test_resolve_symbol_git_refs() {
RefTarget::Normal(commit4.id().clone()),
);
assert_eq!(
resolve_symbol(mut_repo.as_repo_ref(), "tag"),
resolve_symbol(mut_repo.as_repo_ref(), "tag", None),
Ok(vec![commit4.id().clone()])
);
@ -359,7 +385,7 @@ fn test_resolve_symbol_git_refs() {
RefTarget::Normal(commit2.id().clone()),
);
assert_eq!(
resolve_symbol(mut_repo.as_repo_ref(), "origin/remote-branch"),
resolve_symbol(mut_repo.as_repo_ref(), "origin/remote-branch", None),
Ok(vec![commit2.id().clone()])
);
@ -367,17 +393,21 @@ fn test_resolve_symbol_git_refs() {
mut_repo.set_git_ref("@".to_string(), RefTarget::Normal(commit2.id().clone()));
mut_repo.set_git_ref("root".to_string(), RefTarget::Normal(commit3.id().clone()));
assert_eq!(
resolve_symbol(mut_repo.as_repo_ref(), "@"),
Ok(vec![mut_repo.view().checkout().clone()])
resolve_symbol(mut_repo.as_repo_ref(), "@", Some(&WorkspaceId::default())),
Ok(vec![mut_repo
.view()
.get_checkout(&WorkspaceId::default())
.unwrap()
.clone()])
);
assert_eq!(
resolve_symbol(mut_repo.as_repo_ref(), "root"),
resolve_symbol(mut_repo.as_repo_ref(), "root", None),
Ok(vec![mut_repo.store().root_commit().id().clone()])
);
// Conflicted ref resolves to its "adds"
assert_eq!(
resolve_symbol(mut_repo.as_repo_ref(), "refs/heads/conflicted"),
resolve_symbol(mut_repo.as_repo_ref(), "refs/heads/conflicted", None),
Ok(vec![commit1.id().clone(), commit3.id().clone()])
);
}
@ -385,7 +415,21 @@ fn test_resolve_symbol_git_refs() {
fn resolve_commit_ids(repo: RepoRef, revset_str: &str) -> Vec<CommitId> {
let expression = parse(revset_str).unwrap();
expression
.evaluate(repo)
.evaluate(repo, None)
.unwrap()
.iter()
.commit_ids()
.collect()
}
fn resolve_commit_ids_in_workspace(
repo: RepoRef,
revset_str: &str,
workspace_id: &WorkspaceId,
) -> Vec<CommitId> {
let expression = parse(revset_str).unwrap();
expression
.evaluate(repo, Some(workspace_id))
.unwrap()
.iter()
.commit_ids()
@ -414,7 +458,7 @@ fn test_evaluate_expression_root_and_checkout(use_git: bool) {
// Can find the current checkout
mut_repo.set_checkout(WorkspaceId::default(), commit1.id().clone());
assert_eq!(
resolve_commit_ids(mut_repo.as_repo_ref(), "@"),
resolve_commit_ids_in_workspace(mut_repo.as_repo_ref(), "@", &WorkspaceId::default()),
vec![commit1.id().clone()]
);
}
@ -504,7 +548,7 @@ fn test_evaluate_expression_parents(use_git: bool) {
// Can find parents of the current checkout
mut_repo.set_checkout(WorkspaceId::default(), commit2.id().clone());
assert_eq!(
resolve_commit_ids(mut_repo.as_repo_ref(), "@-"),
resolve_commit_ids_in_workspace(mut_repo.as_repo_ref(), "@-", &WorkspaceId::default()),
vec![commit1.id().clone()]
);

View File

@ -386,7 +386,8 @@ impl WorkspaceCommandHelper {
revision_str: &str,
) -> Result<Commit, CommandError> {
let revset_expression = self.parse_revset(ui, revision_str)?;
let revset = revset_expression.evaluate(self.repo.as_repo_ref())?;
let revset =
revset_expression.evaluate(self.repo.as_repo_ref(), Some(&self.workspace_id()))?;
let mut iter = revset.iter().commits(self.repo.store());
match iter.next() {
None => Err(CommandError::UserError(format!(
@ -412,7 +413,8 @@ impl WorkspaceCommandHelper {
revision_str: &str,
) -> Result<Vec<Commit>, CommandError> {
let revset_expression = self.parse_revset(ui, revision_str)?;
let revset = revset_expression.evaluate(self.repo.as_repo_ref())?;
let revset =
revset_expression.evaluate(self.repo.as_repo_ref(), Some(&self.workspace_id()))?;
Ok(revset
.iter()
.commits(self.repo.store())
@ -2541,7 +2543,8 @@ fn cmd_log(ui: &mut Ui, command: &CommandHelper, args: &ArgMatches) -> Result<()
workspace_command.parse_revset(ui, args.value_of("revisions").unwrap())?;
let repo = workspace_command.repo();
let checkout_id = repo.view().checkout().clone();
let revset = revset_expression.evaluate(repo.as_repo_ref())?;
let revset =
revset_expression.evaluate(repo.as_repo_ref(), Some(&workspace_command.workspace_id()))?;
let store = repo.store();
let template_string = match args.value_of("template") {
@ -3344,7 +3347,10 @@ fn cmd_rebase(ui: &mut Ui, command: &CommandHelper, args: &ArgMatches) -> Result
let mut num_rebased_descendants = 0;
let store = workspace_command.repo.store();
for child_commit in children_expression
.evaluate(workspace_command.repo().as_repo_ref())
.evaluate(
workspace_command.repo().as_repo_ref(),
Some(&workspace_command.workspace_id()),
)
.unwrap()
.iter()
.commits(store)

View File

@ -312,7 +312,7 @@ impl DivergentProperty {
pub fn new(repo: RepoRef) -> Self {
// TODO: Create a persistent index from change id to commit ids.
let mut commit_count_by_change: HashMap<ChangeId, i32> = HashMap::new();
for index_entry in RevsetExpression::all().evaluate(repo).unwrap().iter() {
for index_entry in RevsetExpression::all().evaluate(repo, None).unwrap().iter() {
let change_id = index_entry.change_id();
commit_count_by_change
.entry(change_id)