chadtmiller.com/knotless
git that lives in your Atmosphere account
1
10
A moved pull merges with an older git
.changeset/merge-from-old-git.mdnew+7−0
| @@ -0,0 +1,7 @@ | ||
--- | ||
default: patch | ||
--- | ||
| ||
# A moved pull lands with an older git | ||
| ||
Merging or queueing a pull moved off a landed pull no longer needs git 2.40, and git failing during a merge is no longer reported as a conflict. | ||
crates/knotless/src/forge.rs+75−11
| @@ -3224,21 +3224,38 @@ pub async fn merge_tree_from( | ||
base: &str, | ||
head: &str, | ||
) -> Result<Result<String, Vec<String>>> { | ||
let from = since.map(|s| format!("--merge-base={s}")); | ||
let mut args = vec!["merge-tree", "--write-tree", "--name-only"]; | ||
args.extend(from.as_deref()); | ||
args.extend([base, head]); | ||
let ours = match since { | ||
Some(since) => on_start(git, since, base).await?, | ||
None => base.to_string(), | ||
}; | ||
let args = ["merge-tree", "--write-tree", "--name-only", &ours, head]; | ||
let out = git.output_of(&args).await?; | ||
let text = String::from_utf8_lossy(&out.stdout); | ||
let mut lines = text.lines(); | ||
let tree = lines.next().unwrap_or_default().to_string(); | ||
Ok(match out.status.success() { | ||
true => Ok(tree), | ||
false => Err(lines | ||
// It exits 1 for a conflict, having written a tree: git failing writes | ||
// none, whatever it exits with, and must not read as a conflict. | ||
match out.status.code() { | ||
Some(0) => Ok(Ok(tree)), | ||
Some(1) if !tree.is_empty() => Ok(Err(lines | ||
.take_while(|l| !l.is_empty()) | ||
.map(str::to_string) | ||
.collect()), | ||
}) | ||
.collect())), | ||
_ => bail!( | ||
"git merge-tree: {}", | ||
String::from_utf8_lossy(&out.stderr).trim() | ||
), | ||
} | ||
} | ||
| ||
/// A commit of `base`'s tree whose only parent is `since`, so a merge of it | ||
/// with a head built on `since` takes `since` as their merge base. Git has | ||
/// `merge-tree --merge-base` for this only from 2.40, and Debian's stable | ||
/// has an older one. | ||
async fn on_start(git: &Git, since: &str, base: &str) -> Result<String> { | ||
let tree = format!("{base}^{{tree}}"); | ||
let args = ["commit-tree", &tree, "-p", since, "-m", "Start"]; | ||
nobody(git).text(&args, None).await | ||
} | ||
| ||
/// The commit a pull is checked at: what merging `head` into `base` would | ||
| @@ -3264,6 +3281,13 @@ pub async fn merge_result( | ||
/// The merge commit of `head` into `base` at `tree` that a pull is checked | ||
/// at, the same whoever makes it. | ||
pub(crate) async fn checked_merge(git: &Git, tree: &str, base: &str, head: &str) -> Result<String> { | ||
let args = ["commit-tree", tree, "-p", base, "-p", head, "-m", "Merge"]; | ||
nobody(git).text(&args, None).await | ||
} | ||
| ||
/// `git`, committing as nobody at no time, so that the same commit made | ||
/// twice is the same one. | ||
fn nobody(git: &Git) -> Git { | ||
let mut git = git.clone(); | ||
for (var, value) in [ | ||
("GIT_AUTHOR_NAME", "knotless"), | ||
| @@ -3275,8 +3299,7 @@ pub(crate) async fn checked_merge(git: &Git, tree: &str, base: &str, head: &str) | ||
] { | ||
git.envs.push((var.into(), value.into())); | ||
} | ||
let args = ["commit-tree", tree, "-p", base, "-p", head, "-m", "Merge"]; | ||
git.text(&args, None).await | ||
git | ||
} | ||
| ||
/// `git`, committing as `me`. | ||
| @@ -3491,6 +3514,47 @@ mod tests { | ||
use super::*; | ||
use serde_json::json; | ||
| ||
#[tokio::test] | ||
async fn merging_from_a_start_leaves_out_what_is_under_it() { | ||
let dir = tempfile::tempdir().unwrap(); | ||
let bare = Git::init_bare(&dir.path().join("git")).await.unwrap(); | ||
let mut git = nobody(&bare); | ||
let work = dir.path().join("work"); | ||
std::fs::create_dir(&work).unwrap(); | ||
git.envs | ||
.push(("GIT_WORK_TREE".into(), work.display().to_string())); | ||
let commit = async |file: &str, text: &str| { | ||
std::fs::write(work.join(file), text).unwrap(); | ||
git.text(&["add", "-A"], None).await.unwrap(); | ||
git.text(&["commit", "-q", "-m", file], None).await.unwrap(); | ||
git.text(&["rev-parse", "HEAD"], None).await.unwrap() | ||
}; | ||
let first = commit("a", "a\n").await; | ||
// A pull adds f, one above it adds g. | ||
let low = commit("f", "low\n").await; | ||
let high = commit("g", "high\n").await; | ||
// The branch takes it squashed, and goes on to change f. | ||
git.text(&["reset", "-q", "--hard", &first], None) | ||
.await | ||
.unwrap(); | ||
commit("f", "changed\n").await; | ||
let base = git.text(&["rev-parse", "HEAD"], None).await.unwrap(); | ||
| ||
let plain = merge_tree_from(&git, None, &base, &high).await.unwrap(); | ||
assert_eq!(plain, Err(vec!["f".to_string()])); | ||
let tree = merge_tree_from(&git, Some(&low), &base, &high).await; | ||
let tree = tree.unwrap().unwrap(); | ||
let read = format!("{tree}:f"); | ||
assert_eq!(git.text(&["show", &read], None).await.unwrap(), "changed"); | ||
let read = format!("{tree}:g"); | ||
assert_eq!(git.text(&["show", &read], None).await.unwrap(), "high"); | ||
| ||
// Git failing is not a conflict. | ||
let gone = "0123456789012345678901234567890123456789"; | ||
let failed = merge_tree_from(&git, None, &base, gone).await.unwrap_err(); | ||
assert!(failed.to_string().contains("git merge-tree"), "{failed}"); | ||
} | ||
| ||
fn blob(cid: &str) -> BlobRef { | ||
BlobRef { | ||
kind: "blob".into(), | ||