Skip to content

Commit

Permalink
Attempt to rewrite/rebase local heads on reclone when the base commit…
Browse files Browse the repository at this point in the history
…s have changed from the reclone
  • Loading branch information
glandium committed Nov 3, 2023
1 parent 950530c commit 546afc0
Show file tree
Hide file tree
Showing 5 changed files with 222 additions and 20 deletions.
12 changes: 12 additions & 0 deletions src/cinnabar-notes.c
Original file line number Diff line number Diff line change
Expand Up @@ -230,3 +230,15 @@ void consolidate_notes(struct cinnabar_notes_tree *t) {
}
free(notes_ref);
}

struct cinnabar_notes_tree *new_notes_tree(const char *notes_ref)
{
struct cinnabar_notes_tree *result = calloc(1, sizeof(*result));
cinnabar_init_notes(result, notes_ref, combine_notes_ignore, 0);
return result;
}

void destroy_notes_tree(struct cinnabar_notes_tree *t) {
cinnabar_free_notes(t);
free(t);
}
4 changes: 4 additions & 0 deletions src/cinnabar-notes.h
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,8 @@ int write_notes_tree(struct notes_tree *t, struct object_id *result,

void consolidate_notes(struct notes_tree *t);

struct notes_tree *new_notes_tree(const char *notes_ref);

void destroy_notes_tree(struct notes_tree *t);

#endif
1 change: 1 addition & 0 deletions src/libgit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ impl From<object_id> for GitObjectId {
extern "C" {
pub static mut metadata_oid: object_id;
pub static mut changesets_oid: object_id;
pub static mut git2hg_oid: object_id;
}

#[allow(non_camel_case_types)]
Expand Down
206 changes: 193 additions & 13 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ use std::fs::File;
use std::hash::Hash;
use std::io::{stderr, stdin, stdout, BufRead, BufWriter, IsTerminal, Write};
use std::iter::repeat;
use std::ops::{Deref, DerefMut};
use std::os::raw::{c_char, c_int, c_void};
#[cfg(windows)]
use std::os::windows::ffi::OsStrExt as WinOsStrExt;
Expand All @@ -105,15 +106,15 @@ use cinnabar::{
GitChangesetId, GitFileId, GitFileMetadataId, GitManifestId, GitManifestTree, GitManifestTreeId,
};
use cstr::cstr;
use git::{BlobId, CommitId, GitObjectId};
use git::{BlobId, CommitId, GitObjectId, TreeIsh};
use git_version::git_version;
#[cfg(windows)]
use windows_sys::Win32;

use graft::{graft_finish, grafted, init_graft};
use hg::{HgChangesetId, HgFileId, HgManifestId, ManifestEntry};
use hg_connect::{get_bundle, get_clonebundle_url, get_connection, get_store_bundle, HgRepo};
use libcinnabar::{files_meta, git2hg, hg2git, hg_object_id};
use libcinnabar::{files_meta, git2hg, git_notes_tree, hg2git, hg_object_id};
use libgit::{
commit, config_get_value, die, diff_tree_with_copies, for_each_ref_in, for_each_remote,
get_oid_committish, get_unique_abbrev, lookup_commit, lookup_replace_commit, metadata_oid,
Expand Down Expand Up @@ -837,9 +838,62 @@ extern "C" {

extern "C" {
fn term_columns() -> c_int;

fn new_notes_tree(notes_ref: *const c_char) -> *mut git_notes_tree;

fn destroy_notes_tree(t: *mut git_notes_tree);
}

struct NotesBox(*mut git_notes_tree);

impl Deref for NotesBox {
type Target = git_notes_tree;

fn deref(&self) -> &Self::Target {
unsafe { self.0.as_ref().unwrap() }
}
}

impl DerefMut for NotesBox {
fn deref_mut(&mut self) -> &mut Self::Target {
unsafe { self.0.as_mut().unwrap() }
}
}

impl Drop for NotesBox {
fn drop(&mut self) {
unsafe {
destroy_notes_tree(self.0);
}
}
}

fn do_reclone() -> Result<(), String> {
let mut heads = Vec::new();
for prefix in ["refs/tags/", "refs/heads/"] {
// TODO: this doesn't handle tag objects, only tags that point directly to commits.
// Ideally, we'd print out that we can't update those, but at the moment we can't
// even enumerate them. We don't expect tag objects on cinnabar-produced commits,
// though.
for_each_ref_in(prefix, |refname, cid| -> Result<(), String> {
let mut full_ref = OsString::from(prefix);
full_ref.push(refname);
heads.push((full_ref, cid));
Ok(())
})?;
}

let old_changesets_oid = GitObjectId::from(unsafe { libgit::changesets_oid.clone() });
let mut old_git2hg = {
let git2hg_oid = GitObjectId::from(unsafe { libgit::git2hg_oid.clone() });
if git2hg_oid.is_null() {
None
} else {
let git2hg_oid = CString::new(git2hg_oid.to_string()).unwrap();
Some(NotesBox(unsafe { new_notes_tree(git2hg_oid.as_ptr()) }))
}
};

let current_metadata_oid = unsafe {
let current_metadata_oid = metadata_oid.clone();
do_reload(&object_id::default());
Expand All @@ -853,7 +907,21 @@ fn do_reclone() -> Result<(), String> {
init_graft();
}

let mut update_refs_by_remote = Vec::new();
let mut old_to_hg = |cid| {
let old_git2hg = old_git2hg.as_mut().unwrap();
// Manual reimplementation of to_hg. Can't wait to have non-global
// metadata structs. This shouldn't fail, but in case the original
// metadata was busted...
RawGitChangesetMetadata::read_from_notes_tree(
old_git2hg,
GitChangesetId::from_unchecked(cid),
)
.as_ref()
.and_then(RawGitChangesetMetadata::parse)
.map(|m| m.changeset_id())
};

let mut update_refs_by_category = Vec::new();

for_each_remote(|remote| {
if remote.skip_default_update() {
Expand Down Expand Up @@ -943,15 +1011,17 @@ fn do_reclone() -> Result<(), String> {
}
if !update_refs.is_empty() {
update_refs.sort();
update_refs_by_remote.push((remote.get_url().to_os_string(), update_refs));
update_refs_by_category.push((
format!("From {}", remote.get_url().to_string_lossy()),
update_refs,
));
}
Ok(())
})
.and_then(|()| {
// If all the changesets we had in store weren't pulled from the remotes
// above, try fetching them from skip-default-update remotes.
let old_changesets_oid =
CommitId::from_unchecked(GitObjectId::from(unsafe { libgit::changesets_oid.clone() }));
let old_changesets_oid = CommitId::from_unchecked(old_changesets_oid);
if old_changesets_oid.is_null() {
return Ok(());
}
Expand Down Expand Up @@ -1030,13 +1100,120 @@ fn do_reclone() -> Result<(), String> {
})
.collect_vec();
if !update_refs.is_empty() {
update_refs_by_remote.push((remote.get_url().to_os_string(), update_refs));
update_refs_by_category.push((
format!("From {}", remote.get_url().to_string_lossy()),
update_refs,
));
}
}
Ok(())
})
})
.and_then(|()| {
let mut to_rewrite = if old_changesets_oid.is_null() {
Vec::new()
} else {
let mut args = vec!["--full-history".to_string(), "--topo-order".to_string()];
for (_, cid) in &heads {
args.push(cid.to_string());
}
args.push("--not".to_string());
args.push(old_changesets_oid.to_string());
rev_list(args).collect_vec()
};

let mut rewritten = BTreeMap::new();

while let Some(cid) = to_rewrite.pop() {
let commit = RawCommit::read(cid).unwrap();
let commit = commit.parse().unwrap();
let (new_parents, need_parents): (Vec<_>, Vec<_>) = commit
.parents()
.iter()
.map(|p| {
if let Some(p) = rewritten.get(p) {
(*p, None)
} else {
old_to_hg(*p).map_or((None, Some(*p)), |csid| {
let new_cid = csid.to_git().map(CommitId::from).filter(|new_cid| {
p == new_cid || p.get_tree_id() == new_cid.get_tree_id()
});
(new_cid, None)
})
}
})
.unzip();
let new_parents = new_parents.into_iter().flatten().collect_vec();
let need_parents = need_parents.into_iter().flatten().collect_vec();
if !need_parents.is_empty() {
to_rewrite.push(cid);
for p in need_parents.into_iter() {
to_rewrite.push(p);
}
continue;
}
if new_parents == commit.parents() {
assert!(rewritten.insert(cid, Some(cid)).is_none());
} else if new_parents.len() == commit.parents().len() {
let mut buf = strbuf::new();
buf.extend_from_slice(b"tree ");
buf.extend_from_slice(commit.tree().to_string().as_bytes());
for p in new_parents {
buf.extend_from_slice(b"\nparent ");
buf.extend_from_slice(p.to_string().as_bytes());
}
buf.extend_from_slice(b"\nauthor ");
buf.extend_from_slice(commit.author());
buf.extend_from_slice(b"\ncommitter ");
buf.extend_from_slice(commit.committer());
buf.extend_from_slice(b"\n\n");
buf.extend_from_slice(commit.body());
let mut new_oid = object_id::default();
unsafe {
store::store_git_commit(&buf, &mut new_oid);
}
let new_cid = CommitId::from_unchecked(new_oid.into());
assert!(rewritten.insert(cid, Some(new_cid)).is_none());
} else {
assert!(rewritten.insert(cid, None).is_none());
}
}

let (update_refs, cant_update_refs): (Vec<_>, Vec<_>) = heads
.into_iter()
.filter_map(|(refname_or_head, old_cid)| {
let cid = rewritten.get(&old_cid).and_then(Clone::clone).or_else(|| {
old_to_hg(old_cid)
.and_then(HgChangesetId::to_git)
.map(Into::into)
});
(Some(old_cid) != cid).then_some((refname_or_head, old_cid, cid))
})
.partition_map(|(refname_or_head, old_cid, cid)| {
if let Some(cid) = cid {
Either::Left((refname_or_head, old_cid, cid))
} else {
Either::Right(refname_or_head)
}
});
for (category, update_refs) in &update_refs.into_iter().group_by(|(refname, _, _)| {
if refname.as_bytes().starts_with(b"refs/tags/") {
"Rewritten tags"
} else if refname.as_bytes().starts_with(b"refs/heads/") {
"Rewritten branches"
} else {
unreachable!();
}
}) {
let update_refs = update_refs
.map(|(refname, old_cid, cid)| {
let refname = refname.as_bytes().as_bstr().to_boxed();
(refname.clone(), Some(refname), Some(cid), Some(old_cid))
})
.collect_vec();
update_refs_by_category.push((category.to_string(), update_refs));
}

unsafe {
metadata_oid = current_metadata_oid;
}
Expand All @@ -1046,8 +1223,8 @@ fn do_reclone() -> Result<(), String> {
.ok_or_else(|| "Fatal error".to_string())?;
let mut transaction = RefTransaction::new().unwrap();
let mut out = Vec::new();
for (remote_url, update_refs) in update_refs_by_remote {
writeln!(out, "From {}", remote_url.to_string_lossy()).unwrap();
for (category, update_refs) in update_refs_by_category {
writeln!(out, "{}", category).unwrap();
let update_refs = update_refs
.into_iter()
.map(|(peer_ref, refname, cid, old_cid)| {
Expand Down Expand Up @@ -1148,12 +1325,15 @@ fn do_reclone() -> Result<(), String> {
}
transaction.commit().unwrap();
stderr().write_all(&out).unwrap();
if !cant_update_refs.is_empty() {
eprintln!("Could not rewrite the following refs:");
for refname in cant_update_refs {
eprintln!(" {}", refname.as_bytes().as_bstr());
}
eprintln!("They may still be based on the old remote branches.");
}
Ok(())
})
.map(|()| {
println!("Please note that reclone left your local branches untouched.");
println!("They may be based on entirely different commits.");
})
}

fn do_rollback(
Expand Down
19 changes: 12 additions & 7 deletions src/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ use crate::hg_bundle::{
};
use crate::hg_connect_http::HttpRequest;
use crate::hg_data::{hash_data, GitAuthorship, HgAuthorship, HgCommitter};
use crate::libcinnabar::{files_meta, git2hg, hg2git, hg_object_id, strslice, strslice_mut};
use crate::libcinnabar::{files_meta, git2hg, git_notes_tree, hg2git, hg_object_id, strslice, strslice_mut};
use crate::libgit::{
changesets_oid, die, get_oid_blob, object_id, strbuf, Commit, RawBlob, RawCommit, RawTree,
RefTransaction,
Expand Down Expand Up @@ -104,11 +104,16 @@ pub struct RawGitChangesetMetadata(RawBlob);

impl RawGitChangesetMetadata {
pub fn read(changeset_id: GitChangesetId) -> Option<Self> {
let note = unsafe {
git2hg
.get_note(CommitId::from(changeset_id).into())
.map(BlobId::from_unchecked)?
};
Self::read_from_notes_tree(unsafe { &mut git2hg }, changeset_id)
}

pub fn read_from_notes_tree(
notes: &mut git_notes_tree,
changeset_id: GitChangesetId,
) -> Option<Self> {
let note = notes
.get_note(CommitId::from(changeset_id).into())
.map(BlobId::from_unchecked)?;
RawBlob::read(note).map(Self)
}

Expand Down Expand Up @@ -1028,7 +1033,7 @@ extern "C" {
pub fn ensure_store_init();
pub fn store_git_blob(blob_buf: *const strbuf, result: *mut object_id);
fn store_git_tree(tree_buf: *const strbuf, reference: *const object_id, result: *mut object_id);
fn store_git_commit(commit_buf: *const strbuf, result: *mut object_id);
pub fn store_git_commit(commit_buf: *const strbuf, result: *mut object_id);
pub fn do_set(what: *const c_char, hg_id: *const hg_object_id, git_id: *const object_id);
pub fn do_set_replace(replaced: *const object_id, replace_with: *const object_id);
fn create_git_tree(
Expand Down

0 comments on commit 546afc0

Please sign in to comment.