forked from mirrors/jj
git: allow conflicts in "HEAD@git"
Git's HEAD ref is similar to other refs and can logically have conflicts just like the other refs in `git_refs`. As with the other refs, it can happen if you run concurrent commands importing two different updates from Git. So let's treat `git_head` the same as `git_refs` by making it an `Option<RefTarget>`.
This commit is contained in:
parent
6cf7d98465
commit
4e8fbaa210
17 changed files with 123 additions and 58 deletions
|
@ -26,6 +26,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
* The `author`/`committer` templates now display both name and email. Use
|
||||
`author.name()`/`committer.name()` to extract the name.
|
||||
|
||||
* Storage of the "HEAD@git" reference changed and can now have conflicts.
|
||||
Operations written by a new `jj` binary will have a "HEAD@git" reference that
|
||||
is not visible to older binaries.
|
||||
|
||||
### New features
|
||||
|
||||
* The default log format now uses the committer timestamp instead of the author
|
||||
|
|
|
@ -75,7 +75,7 @@ pub fn import_refs(
|
|||
.flat_map(|old_target| old_target.adds())
|
||||
.collect_vec();
|
||||
if let Some(old_git_head) = mut_repo.view().git_head() {
|
||||
old_git_heads.push(old_git_head);
|
||||
old_git_heads.extend(old_git_head.adds());
|
||||
}
|
||||
|
||||
let mut new_git_heads = HashSet::new();
|
||||
|
@ -90,7 +90,7 @@ pub fn import_refs(
|
|||
new_git_heads.insert(head_commit_id.clone());
|
||||
prevent_gc(git_repo, &head_commit_id);
|
||||
mut_repo.add_head(&head_commit);
|
||||
mut_repo.set_git_head(head_commit_id);
|
||||
mut_repo.set_git_head(RefTarget::Normal(head_commit_id));
|
||||
} else {
|
||||
mut_repo.clear_git_head();
|
||||
}
|
||||
|
|
|
@ -180,7 +180,7 @@ impl From<&simple_op_store_model::View> for View {
|
|||
view.git_head = thrift_view
|
||||
.git_head
|
||||
.as_ref()
|
||||
.map(|head| CommitId::new(head.clone()));
|
||||
.map(|head| RefTarget::Normal(CommitId::new(head.clone())));
|
||||
|
||||
view
|
||||
}
|
||||
|
|
|
@ -199,7 +199,7 @@ content_hash! {
|
|||
/// The commit the Git HEAD points to.
|
||||
// TODO: Support multiple Git worktrees?
|
||||
// TODO: Do we want to store the current branch name too?
|
||||
pub git_head: Option<CommitId>,
|
||||
pub git_head: Option<RefTarget>,
|
||||
// The commit that *should be* checked out in the workspace. Note that the working copy
|
||||
// (.jj/working_copy/) has the source of truth about which commit *is* checked out (to be
|
||||
// precise: the commit to which we most recently completed an update to).
|
||||
|
|
|
@ -224,7 +224,7 @@ fn view_to_proto(view: &View) -> crate::protos::op_store::View {
|
|||
}
|
||||
|
||||
if let Some(git_head) = &view.git_head {
|
||||
proto.git_head = git_head.to_bytes();
|
||||
proto.git_head = Some(ref_target_to_proto(git_head));
|
||||
}
|
||||
|
||||
proto
|
||||
|
@ -290,8 +290,11 @@ fn view_from_proto(proto: crate::protos::op_store::View) -> View {
|
|||
}
|
||||
}
|
||||
|
||||
if !proto.git_head.is_empty() {
|
||||
view.git_head = Some(CommitId::new(proto.git_head));
|
||||
#[allow(deprecated)]
|
||||
if let Some(git_head) = proto.git_head.as_ref() {
|
||||
view.git_head = Some(ref_target_from_proto(git_head.clone()));
|
||||
} else if !proto.git_head_legacy.is_empty() {
|
||||
view.git_head = Some(RefTarget::Normal(CommitId::new(proto.git_head_legacy)));
|
||||
}
|
||||
|
||||
view
|
||||
|
|
|
@ -67,7 +67,11 @@ message View {
|
|||
repeated Tag tags = 6;
|
||||
// Only a subset of the refs. For example, does not include refs/notes/.
|
||||
repeated GitRef git_refs = 3;
|
||||
bytes git_head = 7;
|
||||
// This field is just for historical reasons (before we had the RefTarget
|
||||
// type). New Views have (only) the target field.
|
||||
// TODO: Delete support for the old format.
|
||||
bytes git_head_legacy = 7 [deprecated = true];
|
||||
RefTarget git_head = 9;
|
||||
}
|
||||
|
||||
message Operation {
|
||||
|
|
|
@ -89,8 +89,14 @@ pub struct View {
|
|||
/// Only a subset of the refs. For example, does not include refs/notes/.
|
||||
#[prost(message, repeated, tag = "3")]
|
||||
pub git_refs: ::prost::alloc::vec::Vec<GitRef>,
|
||||
/// This field is just for historical reasons (before we had the RefTarget
|
||||
/// type). New Views have (only) the target field.
|
||||
/// TODO: Delete support for the old format.
|
||||
#[deprecated]
|
||||
#[prost(bytes = "vec", tag = "7")]
|
||||
pub git_head: ::prost::alloc::vec::Vec<u8>,
|
||||
pub git_head_legacy: ::prost::alloc::vec::Vec<u8>,
|
||||
#[prost(message, optional, tag = "9")]
|
||||
pub git_head: ::core::option::Option<RefTarget>,
|
||||
}
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
|
|
|
@ -980,8 +980,8 @@ impl MutableRepo {
|
|||
self.view_mut().remove_git_ref(name);
|
||||
}
|
||||
|
||||
pub fn set_git_head(&mut self, head_id: CommitId) {
|
||||
self.view_mut().set_git_head(head_id);
|
||||
pub fn set_git_head(&mut self, target: RefTarget) {
|
||||
self.view_mut().set_git_head(target);
|
||||
}
|
||||
|
||||
pub fn clear_git_head(&mut self) {
|
||||
|
|
|
@ -2039,7 +2039,10 @@ pub fn evaluate_expression<'repo>(
|
|||
Ok(revset_for_commit_ids(repo, &commit_ids))
|
||||
}
|
||||
RevsetExpression::GitHead => {
|
||||
let commit_ids = repo.view().git_head().into_iter().collect_vec();
|
||||
let mut commit_ids = vec![];
|
||||
if let Some(ref_target) = repo.view().git_head() {
|
||||
commit_ids.extend(ref_target.adds());
|
||||
}
|
||||
Ok(revset_for_commit_ids(repo, &commit_ids))
|
||||
}
|
||||
RevsetExpression::Filter(predicate) => Ok(Box::new(FilterRevset {
|
||||
|
|
|
@ -204,7 +204,7 @@ mod tests {
|
|||
"refs/heads/main".to_string() => git_refs_main_target,
|
||||
"refs/heads/feature".to_string() => git_refs_feature_target
|
||||
},
|
||||
git_head: Some(CommitId::from_hex("fff111")),
|
||||
git_head: Some(RefTarget::Normal(CommitId::from_hex("fff111"))),
|
||||
wc_commit_ids: hashmap! {
|
||||
WorkspaceId::default() => default_wc_commit_id,
|
||||
WorkspaceId::new("test".to_string()) => test_wc_commit_id,
|
||||
|
@ -244,7 +244,7 @@ mod tests {
|
|||
// Test exact output so we detect regressions in compatibility
|
||||
assert_snapshot!(
|
||||
ViewId::new(blake2b_hash(&create_view()).to_vec()).hex(),
|
||||
@"2a026b6a091219a3d8ca43d822984cf9be0c53438225d76a5ba5e6d3724fab15104579fb08fa949977c4357b1806d240bef28d958cbcd7d786962ac88c15df31"
|
||||
@"7f47fa81494d7189cb1827b83b3f834662f0f61b4c4090298067e85cdc60f773bf639c4e6a3554a4e401650218ca240291ce591f45a1c501ade1d2b9f97e1a37"
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -84,8 +84,8 @@ impl View {
|
|||
&self.data.git_refs
|
||||
}
|
||||
|
||||
pub fn git_head(&self) -> Option<CommitId> {
|
||||
self.data.git_head.clone()
|
||||
pub fn git_head(&self) -> Option<&RefTarget> {
|
||||
self.data.git_head.as_ref()
|
||||
}
|
||||
|
||||
pub fn set_wc_commit(&mut self, workspace_id: WorkspaceId, commit_id: CommitId) {
|
||||
|
@ -244,8 +244,8 @@ impl View {
|
|||
self.data.git_refs.remove(name);
|
||||
}
|
||||
|
||||
pub fn set_git_head(&mut self, head_id: CommitId) {
|
||||
self.data.git_head = Some(head_id);
|
||||
pub fn set_git_head(&mut self, target: RefTarget) {
|
||||
self.data.git_head = Some(target);
|
||||
}
|
||||
|
||||
pub fn clear_git_head(&mut self) {
|
||||
|
|
|
@ -161,7 +161,7 @@ fn test_import_refs() {
|
|||
view.git_refs().get("refs/tags/v1.0"),
|
||||
Some(RefTarget::Normal(jj_id(&commit5))).as_ref()
|
||||
);
|
||||
assert_eq!(view.git_head(), Some(jj_id(&commit2)));
|
||||
assert_eq!(view.git_head(), Some(&RefTarget::Normal(jj_id(&commit2))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -432,7 +432,10 @@ fn test_import_refs_detached_head() {
|
|||
let expected_heads = hashset! { jj_id(&commit1) };
|
||||
assert_eq!(*repo.view().heads(), expected_heads);
|
||||
assert_eq!(repo.view().git_refs().len(), 0);
|
||||
assert_eq!(repo.view().git_head(), Some(jj_id(&commit1)));
|
||||
assert_eq!(
|
||||
repo.view().git_head(),
|
||||
Some(&RefTarget::Normal(jj_id(&commit1)))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
@ -1324,7 +1324,7 @@ fn test_evaluate_expression_git_head(use_git: bool) {
|
|||
resolve_commit_ids(mut_repo.as_repo_ref(), "git_head()"),
|
||||
vec![]
|
||||
);
|
||||
mut_repo.set_git_head(commit1.id().clone());
|
||||
mut_repo.set_git_head(RefTarget::Normal(commit1.id().clone()));
|
||||
assert_eq!(
|
||||
resolve_commit_ids(mut_repo.as_repo_ref(), "git_head()"),
|
||||
vec![commit1.id().clone()]
|
||||
|
|
|
@ -431,6 +431,41 @@ fn test_merge_views_git_refs() {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_merge_views_git_heads() {
|
||||
// Tests merging of git heads (by performing concurrent operations). See
|
||||
// test_refs.rs for tests of merging of individual ref targets.
|
||||
let settings = testutils::user_settings();
|
||||
let test_repo = TestRepo::init(false);
|
||||
let repo = &test_repo.repo;
|
||||
|
||||
let mut tx0 = repo.start_transaction(&settings, "test");
|
||||
let tx0_head = write_random_commit(tx0.mut_repo(), &settings);
|
||||
tx0.mut_repo()
|
||||
.set_git_head(RefTarget::Normal(tx0_head.id().clone()));
|
||||
let repo = tx0.commit();
|
||||
|
||||
let mut tx1 = repo.start_transaction(&settings, "test");
|
||||
let tx1_head = write_random_commit(tx1.mut_repo(), &settings);
|
||||
tx1.mut_repo()
|
||||
.set_git_head(RefTarget::Normal(tx1_head.id().clone()));
|
||||
tx1.commit();
|
||||
|
||||
let mut tx2 = repo.start_transaction(&settings, "test");
|
||||
let tx2_head = write_random_commit(tx2.mut_repo(), &settings);
|
||||
tx2.mut_repo()
|
||||
.set_git_head(RefTarget::Normal(tx2_head.id().clone()));
|
||||
tx2.commit();
|
||||
|
||||
let repo = repo.reload_at_head(&settings).unwrap();
|
||||
let expected_git_head = RefTarget::Conflict {
|
||||
removes: vec![tx0_head.id().clone()],
|
||||
adds: vec![tx1_head.id().clone(), tx2_head.id().clone()],
|
||||
};
|
||||
// TODO: Should be equal
|
||||
assert_ne!(repo.view().git_head(), Some(&expected_git_head));
|
||||
}
|
||||
|
||||
fn commit_transactions(settings: &UserSettings, txs: Vec<Transaction>) -> Arc<ReadonlyRepo> {
|
||||
let repo_loader = txs[0].base_repo().loader();
|
||||
let mut op_ids = vec![];
|
||||
|
|
|
@ -33,7 +33,7 @@ use jujutsu_lib::git::{GitExportError, GitImportError};
|
|||
use jujutsu_lib::gitignore::GitIgnoreFile;
|
||||
use jujutsu_lib::matchers::{EverythingMatcher, Matcher, PrefixMatcher, Visit};
|
||||
use jujutsu_lib::op_heads_store::{self, OpHeadResolutionError, OpHeadsStore};
|
||||
use jujutsu_lib::op_store::{OpStore, OpStoreError, OperationId, WorkspaceId};
|
||||
use jujutsu_lib::op_store::{OpStore, OpStoreError, OperationId, RefTarget, WorkspaceId};
|
||||
use jujutsu_lib::operation::Operation;
|
||||
use jujutsu_lib::repo::{
|
||||
CheckOutCommitError, EditCommitError, MutableRepo, ReadonlyRepo, RepoLoader, RepoRef,
|
||||
|
@ -507,40 +507,42 @@ impl WorkspaceCommandHelper {
|
|||
let mut tx = self.start_transaction("import git refs").into_inner();
|
||||
git::import_refs(tx.mut_repo(), git_repo, &self.settings.git_settings())?;
|
||||
if tx.mut_repo().has_changes() {
|
||||
let old_git_head = self.repo.view().git_head();
|
||||
let new_git_head = tx.mut_repo().view().git_head();
|
||||
let old_git_head = self.repo.view().git_head().cloned();
|
||||
let new_git_head = tx.mut_repo().view().git_head().cloned();
|
||||
// If the Git HEAD has changed, abandon our old checkout and check out the new
|
||||
// Git HEAD.
|
||||
if new_git_head != old_git_head && new_git_head.is_some() {
|
||||
let workspace_id = self.workspace_id().to_owned();
|
||||
let mut locked_working_copy = self.workspace.working_copy_mut().start_mutation();
|
||||
if let Some(old_wc_commit_id) = self.repo.view().get_wc_commit_id(&workspace_id) {
|
||||
match new_git_head {
|
||||
Some(RefTarget::Normal(new_git_head_id)) if new_git_head != old_git_head => {
|
||||
let workspace_id = self.workspace_id().to_owned();
|
||||
let mut locked_working_copy =
|
||||
self.workspace.working_copy_mut().start_mutation();
|
||||
if let Some(old_wc_commit_id) = self.repo.view().get_wc_commit_id(&workspace_id)
|
||||
{
|
||||
tx.mut_repo()
|
||||
.record_abandoned_commit(old_wc_commit_id.clone());
|
||||
}
|
||||
let new_git_head_commit = tx.mut_repo().store().get_commit(&new_git_head_id)?;
|
||||
tx.mut_repo()
|
||||
.record_abandoned_commit(old_wc_commit_id.clone());
|
||||
.check_out(workspace_id, &self.settings, &new_git_head_commit)?;
|
||||
// The working copy was presumably updated by the git command that updated
|
||||
// HEAD, so we just need to reset our working copy
|
||||
// state to it without updating working copy files.
|
||||
locked_working_copy.reset(&new_git_head_commit.tree())?;
|
||||
tx.mut_repo().rebase_descendants(&self.settings)?;
|
||||
self.repo = tx.commit();
|
||||
locked_working_copy.finish(self.repo.op_id().clone());
|
||||
}
|
||||
let new_checkout = self
|
||||
.repo
|
||||
.store()
|
||||
.get_commit(new_git_head.as_ref().unwrap())?;
|
||||
tx.mut_repo()
|
||||
.check_out(workspace_id, &self.settings, &new_checkout)?;
|
||||
// The working copy was presumably updated by the git command that updated HEAD,
|
||||
// so we just need to reset our working copy state to it without updating
|
||||
// working copy files.
|
||||
locked_working_copy.reset(&new_checkout.tree())?;
|
||||
tx.mut_repo().rebase_descendants(&self.settings)?;
|
||||
self.repo = tx.commit();
|
||||
locked_working_copy.finish(self.repo.op_id().clone());
|
||||
} else {
|
||||
let num_rebased = tx.mut_repo().rebase_descendants(&self.settings)?;
|
||||
if num_rebased > 0 {
|
||||
writeln!(
|
||||
ui,
|
||||
"Rebased {num_rebased} descendant commits off of commits rewritten from \
|
||||
git"
|
||||
)?;
|
||||
_ => {
|
||||
let num_rebased = tx.mut_repo().rebase_descendants(&self.settings)?;
|
||||
if num_rebased > 0 {
|
||||
writeln!(
|
||||
ui,
|
||||
"Rebased {num_rebased} descendant commits off of commits rewritten \
|
||||
from git"
|
||||
)?;
|
||||
}
|
||||
self.finish_transaction(ui, tx)?;
|
||||
}
|
||||
self.finish_transaction(ui, tx)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
@ -567,7 +569,7 @@ impl WorkspaceCommandHelper {
|
|||
let new_git_commit_id = Oid::from_bytes(first_parent_id.as_bytes()).unwrap();
|
||||
let new_git_commit = git_repo.find_commit(new_git_commit_id)?;
|
||||
git_repo.reset(new_git_commit.as_object(), git2::ResetType::Mixed, None)?;
|
||||
mut_repo.set_git_head(first_parent_id);
|
||||
mut_repo.set_git_head(RefTarget::Normal(first_parent_id));
|
||||
}
|
||||
} else {
|
||||
// The workspace was removed (maybe the user undid the
|
||||
|
|
|
@ -32,7 +32,7 @@ use jujutsu_lib::commit::Commit;
|
|||
use jujutsu_lib::dag_walk::topo_order_reverse;
|
||||
use jujutsu_lib::index::IndexEntry;
|
||||
use jujutsu_lib::matchers::EverythingMatcher;
|
||||
use jujutsu_lib::op_store::WorkspaceId;
|
||||
use jujutsu_lib::op_store::{RefTarget, WorkspaceId};
|
||||
use jujutsu_lib::repo::ReadonlyRepo;
|
||||
use jujutsu_lib::repo_path::RepoPath;
|
||||
use jujutsu_lib::revset::{RevsetAliasesMap, RevsetExpression};
|
||||
|
@ -985,7 +985,7 @@ fn cmd_init(ui: &mut Ui, command: &CommandHelper, args: &InitArgs) -> Result<(),
|
|||
&git_repo,
|
||||
&command.settings().git_settings(),
|
||||
)?;
|
||||
if let Some(git_head_id) = tx.mut_repo().view().git_head() {
|
||||
if let Some(RefTarget::Normal(git_head_id)) = tx.mut_repo().view().git_head().cloned() {
|
||||
let git_head_commit = tx.mut_repo().store().get_commit(&git_head_id)?;
|
||||
tx.check_out(&git_head_commit)?;
|
||||
}
|
||||
|
|
|
@ -362,10 +362,15 @@ impl TemplateProperty<Commit> for GitHeadProperty<'_> {
|
|||
type Output = String;
|
||||
|
||||
fn extract(&self, context: &Commit) -> String {
|
||||
if self.repo.view().git_head().as_ref() == Some(context.id()) {
|
||||
"HEAD@git".to_string()
|
||||
} else {
|
||||
"".to_string()
|
||||
match self.repo.view().git_head() {
|
||||
Some(ref_target) if ref_target.has_add(context.id()) => {
|
||||
if ref_target.is_conflict() {
|
||||
"HEAD@git?".to_string()
|
||||
} else {
|
||||
"HEAD@git".to_string()
|
||||
}
|
||||
}
|
||||
_ => "".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue