forked from mirrors/jj
git: include reason for each failed ref export
This commit is contained in:
parent
f6c24fbcef
commit
ef550a9d6d
3 changed files with 119 additions and 54 deletions
|
@ -32,7 +32,9 @@ use indexmap::IndexSet;
|
|||
use itertools::Itertools;
|
||||
use jj_lib::backend::{BackendError, ChangeId, CommitId, MergedTreeId, ObjectId};
|
||||
use jj_lib::commit::Commit;
|
||||
use jj_lib::git::{GitConfigParseError, GitExportError, GitImportError, GitRemoteManagementError};
|
||||
use jj_lib::git::{
|
||||
FailedRefExport, GitConfigParseError, GitExportError, GitImportError, GitRemoteManagementError,
|
||||
};
|
||||
use jj_lib::git_backend::GitBackend;
|
||||
use jj_lib::gitignore::GitIgnoreFile;
|
||||
use jj_lib::hex_util::to_reverse_hex;
|
||||
|
@ -55,7 +57,6 @@ use jj_lib::revset::{
|
|||
use jj_lib::settings::{ConfigResultExt as _, UserSettings};
|
||||
use jj_lib::transaction::Transaction;
|
||||
use jj_lib::tree::TreeMergeError;
|
||||
use jj_lib::view::RefName;
|
||||
use jj_lib::working_copy::{
|
||||
CheckoutStats, LockedWorkingCopy, ResetError, SnapshotError, SnapshotOptions, TreeStateError,
|
||||
WorkingCopy,
|
||||
|
@ -1727,13 +1728,16 @@ pub fn print_checkout_stats(ui: &mut Ui, stats: CheckoutStats) -> Result<(), std
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn print_failed_git_export(ui: &Ui, failed_branches: &[RefName]) -> Result<(), std::io::Error> {
|
||||
pub fn print_failed_git_export(
|
||||
ui: &Ui,
|
||||
failed_branches: &[FailedRefExport],
|
||||
) -> Result<(), std::io::Error> {
|
||||
if !failed_branches.is_empty() {
|
||||
writeln!(ui.warning(), "Failed to export some branches:")?;
|
||||
let mut formatter = ui.stderr_formatter();
|
||||
for branch_ref in failed_branches {
|
||||
for failed_ref_export in failed_branches {
|
||||
formatter.write_str(" ")?;
|
||||
write!(formatter.labeled("branch"), "{branch_ref}")?;
|
||||
write!(formatter.labeled("branch"), "{}", failed_ref_export.name)?;
|
||||
formatter.write_str("\n")?;
|
||||
}
|
||||
drop(formatter);
|
||||
|
|
115
lib/src/git.rs
115
lib/src/git.rs
|
@ -81,10 +81,11 @@ fn parse_git_ref(ref_name: &str) -> Option<RefName> {
|
|||
|
||||
fn to_git_ref_name(parsed_ref: &RefName) -> Option<String> {
|
||||
match parsed_ref {
|
||||
RefName::LocalBranch(branch) => (branch != "HEAD").then(|| format!("refs/heads/{branch}")),
|
||||
RefName::RemoteBranch { branch, remote } => {
|
||||
(branch != "HEAD").then(|| format!("refs/remotes/{remote}/{branch}"))
|
||||
RefName::LocalBranch(branch) => {
|
||||
(!branch.is_empty() && branch != "HEAD").then(|| format!("refs/heads/{branch}"))
|
||||
}
|
||||
RefName::RemoteBranch { branch, remote } => (!branch.is_empty() && branch != "HEAD")
|
||||
.then(|| format!("refs/remotes/{remote}/{branch}")),
|
||||
RefName::Tag(tag) => Some(format!("refs/tags/{tag}")),
|
||||
RefName::GitRef(name) => Some(name.to_owned()),
|
||||
}
|
||||
|
@ -379,6 +380,33 @@ pub enum GitExportError {
|
|||
InternalGitError(#[from] git2::Error),
|
||||
}
|
||||
|
||||
/// A ref we failed to export to Git, along with the reason it failed.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct FailedRefExport {
|
||||
pub name: RefName,
|
||||
pub reason: FailedRefExportReason,
|
||||
}
|
||||
|
||||
/// The reason we failed to export a ref to Git.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum FailedRefExportReason {
|
||||
/// The name is not allowed in Git.
|
||||
InvalidGitName,
|
||||
/// The ref was in a conflicted state from the last import. A re-import
|
||||
/// should fix it.
|
||||
ConflictedOldState,
|
||||
/// We wanted to delete it, but it had been modified in Git.
|
||||
DeletedInJjModifiedInGit,
|
||||
/// We wanted to add it, but Git had added it with a different target
|
||||
AddedInJjAddedInGit,
|
||||
/// We wanted to modify it, but Git had deleted it
|
||||
ModifiedInJjDeletedInGit,
|
||||
/// Failed to delete the ref from the Git repo
|
||||
FailedToDelete(git2::Error),
|
||||
/// Failed to set the ref in the Git repo
|
||||
FailedToSet(git2::Error),
|
||||
}
|
||||
|
||||
/// Export changes to branches made in the Jujutsu repo compared to our last
|
||||
/// seen view of the Git repo in `mut_repo.view().git_refs()`. Returns a list of
|
||||
/// refs that failed to export.
|
||||
|
@ -394,7 +422,7 @@ pub enum GitExportError {
|
|||
pub fn export_refs(
|
||||
mut_repo: &mut MutableRepo,
|
||||
git_repo: &git2::Repository,
|
||||
) -> Result<Vec<RefName>, GitExportError> {
|
||||
) -> Result<Vec<FailedRefExport>, GitExportError> {
|
||||
export_some_refs(mut_repo, git_repo, |_| true)
|
||||
}
|
||||
|
||||
|
@ -402,7 +430,7 @@ pub fn export_some_refs(
|
|||
mut_repo: &mut MutableRepo,
|
||||
git_repo: &git2::Repository,
|
||||
git_ref_filter: impl Fn(&RefName) -> bool,
|
||||
) -> Result<Vec<RefName>, GitExportError> {
|
||||
) -> Result<Vec<FailedRefExport>, GitExportError> {
|
||||
// First find the changes we want need to make without modifying mut_repo
|
||||
let mut branches_to_update = BTreeMap::new();
|
||||
let mut branches_to_delete = BTreeMap::new();
|
||||
|
@ -446,7 +474,10 @@ pub fn export_some_refs(
|
|||
view.get_git_ref(&name)
|
||||
} else {
|
||||
// Invalid branch name in Git sense
|
||||
failed_branches.push(jj_known_ref);
|
||||
failed_branches.push(FailedRefExport {
|
||||
name: jj_known_ref,
|
||||
reason: FailedRefExportReason::InvalidGitName,
|
||||
});
|
||||
continue;
|
||||
};
|
||||
if new_branch == old_branch {
|
||||
|
@ -457,7 +488,10 @@ pub fn export_some_refs(
|
|||
} else if old_branch.has_conflict() {
|
||||
// The old git ref should only be a conflict if there were concurrent import
|
||||
// operations while the value changed. Don't overwrite these values.
|
||||
failed_branches.push(jj_known_ref);
|
||||
failed_branches.push(FailedRefExport {
|
||||
name: jj_known_ref,
|
||||
reason: FailedRefExportReason::ConflictedOldState,
|
||||
});
|
||||
continue;
|
||||
} else {
|
||||
assert!(old_branch.is_absent());
|
||||
|
@ -494,70 +528,91 @@ pub fn export_some_refs(
|
|||
}
|
||||
for (parsed_ref_name, old_oid) in branches_to_delete {
|
||||
let git_ref_name = to_git_ref_name(&parsed_ref_name).unwrap();
|
||||
let success = if let Ok(mut git_repo_ref) = git_repo.find_reference(&git_ref_name) {
|
||||
let reason = if let Ok(mut git_repo_ref) = git_repo.find_reference(&git_ref_name) {
|
||||
if git_repo_ref.target() == Some(old_oid) {
|
||||
// The branch has not been updated by git, so go ahead and delete it
|
||||
git_repo_ref.delete().is_ok()
|
||||
git_repo_ref
|
||||
.delete()
|
||||
.err()
|
||||
.map(FailedRefExportReason::FailedToDelete)
|
||||
} else {
|
||||
// The branch was updated by git
|
||||
false
|
||||
Some(FailedRefExportReason::DeletedInJjModifiedInGit)
|
||||
}
|
||||
} else {
|
||||
// The branch is already deleted
|
||||
true
|
||||
None
|
||||
};
|
||||
if success {
|
||||
mut_repo.set_git_ref_target(&git_ref_name, RefTarget::absent());
|
||||
if let Some(reason) = reason {
|
||||
failed_branches.push(FailedRefExport {
|
||||
name: parsed_ref_name,
|
||||
reason,
|
||||
});
|
||||
} else {
|
||||
failed_branches.push(parsed_ref_name);
|
||||
mut_repo.set_git_ref_target(&git_ref_name, RefTarget::absent());
|
||||
}
|
||||
}
|
||||
for (parsed_ref_name, (old_oid, new_oid)) in branches_to_update {
|
||||
let git_ref_name = to_git_ref_name(&parsed_ref_name).unwrap();
|
||||
let success = match old_oid {
|
||||
let reason = match old_oid {
|
||||
None => {
|
||||
if let Ok(git_repo_ref) = git_repo.find_reference(&git_ref_name) {
|
||||
// The branch was added in jj and in git. We're good if and only if git
|
||||
// pointed it to our desired target.
|
||||
git_repo_ref.target() == Some(new_oid)
|
||||
if git_repo_ref.target() == Some(new_oid) {
|
||||
None
|
||||
} else {
|
||||
Some(FailedRefExportReason::AddedInJjAddedInGit)
|
||||
}
|
||||
} else {
|
||||
// The branch was added in jj but still doesn't exist in git, so add it
|
||||
git_repo
|
||||
.reference(&git_ref_name, new_oid, true, "export from jj")
|
||||
.is_ok()
|
||||
.err()
|
||||
.map(FailedRefExportReason::FailedToSet)
|
||||
}
|
||||
}
|
||||
Some(old_oid) => {
|
||||
// The branch was modified in jj. We can use libgit2's API for updating under a
|
||||
// lock.
|
||||
if git_repo
|
||||
.reference_matching(&git_ref_name, new_oid, true, old_oid, "export from jj")
|
||||
.is_ok()
|
||||
{
|
||||
// Successfully updated from old_oid to new_oid (unchanged in git)
|
||||
true
|
||||
} else {
|
||||
if let Err(err) = git_repo.reference_matching(
|
||||
&git_ref_name,
|
||||
new_oid,
|
||||
true,
|
||||
old_oid,
|
||||
"export from jj",
|
||||
) {
|
||||
// The reference was probably updated in git
|
||||
if let Ok(git_repo_ref) = git_repo.find_reference(&git_ref_name) {
|
||||
// We still consider this a success if it was updated to our desired target
|
||||
git_repo_ref.target() == Some(new_oid)
|
||||
if git_repo_ref.target() == Some(new_oid) {
|
||||
None
|
||||
} else {
|
||||
Some(FailedRefExportReason::FailedToSet(err))
|
||||
}
|
||||
} else {
|
||||
// The reference was deleted in git and moved in jj
|
||||
false
|
||||
Some(FailedRefExportReason::ModifiedInJjDeletedInGit)
|
||||
}
|
||||
} else {
|
||||
// Successfully updated from old_oid to new_oid (unchanged in git)
|
||||
None
|
||||
}
|
||||
}
|
||||
};
|
||||
if success {
|
||||
if let Some(reason) = reason {
|
||||
failed_branches.push(FailedRefExport {
|
||||
name: parsed_ref_name,
|
||||
reason,
|
||||
});
|
||||
} else {
|
||||
mut_repo.set_git_ref_target(
|
||||
&git_ref_name,
|
||||
RefTarget::normal(CommitId::from_bytes(new_oid.as_bytes())),
|
||||
);
|
||||
} else {
|
||||
failed_branches.push(parsed_ref_name);
|
||||
}
|
||||
}
|
||||
failed_branches.sort();
|
||||
failed_branches.sort_by_key(|failed| failed.name.clone());
|
||||
Ok(failed_branches)
|
||||
}
|
||||
|
||||
|
|
|
@ -26,7 +26,10 @@ use jj_lib::backend::{
|
|||
use jj_lib::commit::Commit;
|
||||
use jj_lib::commit_builder::CommitBuilder;
|
||||
use jj_lib::git;
|
||||
use jj_lib::git::{GitFetchError, GitImportError, GitPushError, GitRefUpdate, SubmoduleConfig};
|
||||
use jj_lib::git::{
|
||||
FailedRefExportReason, GitFetchError, GitImportError, GitPushError, GitRefUpdate,
|
||||
SubmoduleConfig,
|
||||
};
|
||||
use jj_lib::git_backend::GitBackend;
|
||||
use jj_lib::op_store::{BranchTarget, RefTarget};
|
||||
use jj_lib::repo::{MutableRepo, ReadonlyRepo, Repo};
|
||||
|
@ -1348,14 +1351,14 @@ fn test_export_partial_failure() {
|
|||
// `main/sub` will conflict with `main` in Git, at least when using loose ref
|
||||
// storage
|
||||
mut_repo.set_local_branch_target("main/sub", target);
|
||||
assert_eq!(
|
||||
git::export_refs(mut_repo, &git_repo),
|
||||
Ok(vec![
|
||||
RefName::LocalBranch("".to_string()),
|
||||
RefName::LocalBranch("HEAD".to_string()),
|
||||
RefName::LocalBranch("main/sub".to_string())
|
||||
])
|
||||
);
|
||||
let failed = git::export_refs(mut_repo, &git_repo).unwrap();
|
||||
assert_eq!(failed.len(), 3);
|
||||
assert_eq!(failed[0].name, RefName::LocalBranch("".to_string()));
|
||||
assert_matches!(failed[0].reason, FailedRefExportReason::InvalidGitName);
|
||||
assert_eq!(failed[1].name, RefName::LocalBranch("HEAD".to_string()));
|
||||
assert_matches!(failed[1].reason, FailedRefExportReason::InvalidGitName);
|
||||
assert_eq!(failed[2].name, RefName::LocalBranch("main/sub".to_string()));
|
||||
assert_matches!(failed[2].reason, FailedRefExportReason::FailedToSet(_));
|
||||
|
||||
// The `main` branch should have succeeded but the other should have failed
|
||||
assert!(git_repo.find_reference("refs/heads/").is_err());
|
||||
|
@ -1373,13 +1376,12 @@ fn test_export_partial_failure() {
|
|||
// Now remove the `main` branch and make sure that the `main/sub` gets exported
|
||||
// even though it didn't change
|
||||
mut_repo.set_local_branch_target("main", RefTarget::absent());
|
||||
assert_eq!(
|
||||
git::export_refs(mut_repo, &git_repo),
|
||||
Ok(vec![
|
||||
RefName::LocalBranch("".to_string()),
|
||||
RefName::LocalBranch("HEAD".to_string()),
|
||||
])
|
||||
);
|
||||
let failed = git::export_refs(mut_repo, &git_repo).unwrap();
|
||||
assert_eq!(failed.len(), 2);
|
||||
assert_eq!(failed[0].name, RefName::LocalBranch("".to_string()));
|
||||
assert_matches!(failed[0].reason, FailedRefExportReason::InvalidGitName);
|
||||
assert_eq!(failed[1].name, RefName::LocalBranch("HEAD".to_string()));
|
||||
assert_matches!(failed[1].reason, FailedRefExportReason::InvalidGitName);
|
||||
assert!(git_repo.find_reference("refs/heads/").is_err());
|
||||
assert!(git_repo.find_reference("refs/heads/HEAD").is_err());
|
||||
assert!(git_repo.find_reference("refs/heads/main").is_err());
|
||||
|
@ -1471,11 +1473,15 @@ fn test_export_reexport_transitions() {
|
|||
// export. They should have been unchanged in git and in
|
||||
// mut_repo.view().git_refs().
|
||||
assert_eq!(
|
||||
git::export_refs(mut_repo, &git_repo),
|
||||
Ok(["ABC", "ABX", "AXB", "XAB"]
|
||||
git::export_refs(mut_repo, &git_repo)
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|failed| failed.name)
|
||||
.collect_vec(),
|
||||
vec!["ABC", "ABX", "AXB", "XAB"]
|
||||
.into_iter()
|
||||
.map(|s| RefName::LocalBranch(s.to_string()))
|
||||
.collect_vec())
|
||||
.collect_vec()
|
||||
);
|
||||
for branch in ["AAX", "ABX", "AXA", "AXX"] {
|
||||
assert!(
|
||||
|
|
Loading…
Reference in a new issue