[F] Rebase commit committer should be kept
This commit is contained in:
+101
-4
@@ -86,6 +86,12 @@ pub struct GitMirror {
|
|||||||
dry_run: bool,
|
dry_run: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct CommitterIdentity {
|
||||||
|
name: String,
|
||||||
|
email: String,
|
||||||
|
date: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Redactor {
|
pub struct Redactor {
|
||||||
secrets: Vec<String>,
|
secrets: Vec<String>,
|
||||||
@@ -582,6 +588,7 @@ impl GitMirror {
|
|||||||
return Ok(format!("dry-run-rebased-{}", short_sha(tip)));
|
return Ok(format!("dry-run-rebased-{}", short_sha(tip)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let commits = self.replay_commits(base, tip)?;
|
||||||
let worktree = tempfile::TempDir::new().context("failed to create temporary worktree")?;
|
let worktree = tempfile::TempDir::new().context("failed to create temporary worktree")?;
|
||||||
let worktree_path = worktree.path().to_path_buf();
|
let worktree_path = worktree.path().to_path_buf();
|
||||||
self.run([
|
self.run([
|
||||||
@@ -589,12 +596,13 @@ impl GitMirror {
|
|||||||
"add",
|
"add",
|
||||||
"--detach",
|
"--detach",
|
||||||
worktree_path.to_str().unwrap(),
|
worktree_path.to_str().unwrap(),
|
||||||
tip,
|
onto,
|
||||||
])?;
|
])?;
|
||||||
|
|
||||||
let rebase_result = self.worktree_git(&worktree_path, ["rebase", "--onto", onto, base]);
|
let replay_result = self.replay_commits_preserving_committer(&worktree_path, &commits);
|
||||||
if let Err(error) = rebase_result {
|
if let Err(error) = replay_result {
|
||||||
let _ = self.worktree_git(&worktree_path, ["rebase", "--abort"]);
|
let _ = self.worktree_git(&worktree_path, ["cherry-pick", "--abort"]);
|
||||||
|
let _ = self.worktree_git(&worktree_path, ["reset", "--hard"]);
|
||||||
let _ = self.run([
|
let _ = self.run([
|
||||||
"worktree",
|
"worktree",
|
||||||
"remove",
|
"remove",
|
||||||
@@ -613,6 +621,66 @@ impl GitMirror {
|
|||||||
Ok(rebased.trim().to_string())
|
Ok(rebased.trim().to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn replay_commits(&self, base: &str, tip: &str) -> Result<Vec<String>> {
|
||||||
|
let range = format!("{base}..{tip}");
|
||||||
|
Ok(self
|
||||||
|
.output([
|
||||||
|
"rev-list",
|
||||||
|
"--reverse",
|
||||||
|
"--topo-order",
|
||||||
|
"--no-merges",
|
||||||
|
&range,
|
||||||
|
])?
|
||||||
|
.lines()
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|line| !line.is_empty())
|
||||||
|
.map(ToOwned::to_owned)
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn replay_commits_preserving_committer(
|
||||||
|
&self,
|
||||||
|
worktree: &Path,
|
||||||
|
commits: &[String],
|
||||||
|
) -> Result<()> {
|
||||||
|
for commit in commits {
|
||||||
|
let committer = self.committer_identity(commit)?;
|
||||||
|
self.worktree_git(worktree, ["cherry-pick", "--no-commit", commit])?;
|
||||||
|
self.worktree_git_with_committer(
|
||||||
|
worktree,
|
||||||
|
["commit", "--no-gpg-sign", "-C", commit],
|
||||||
|
&committer,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn committer_identity(&self, commit: &str) -> Result<CommitterIdentity> {
|
||||||
|
let output = self.output(["show", "-s", "--format=%cn%x00%ce%x00%cI", commit])?;
|
||||||
|
let output = output.trim_end_matches('\n');
|
||||||
|
let mut parts = output.split('\0');
|
||||||
|
let name = parts
|
||||||
|
.next()
|
||||||
|
.filter(|value| !value.is_empty())
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("commit {commit} has no committer name"))?;
|
||||||
|
let email = parts
|
||||||
|
.next()
|
||||||
|
.filter(|value| !value.is_empty())
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("commit {commit} has no committer email"))?;
|
||||||
|
let date = parts
|
||||||
|
.next()
|
||||||
|
.filter(|value| !value.is_empty())
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("commit {commit} has no committer date"))?;
|
||||||
|
if parts.next().is_some() {
|
||||||
|
bail!("commit {commit} has unexpected committer metadata");
|
||||||
|
}
|
||||||
|
Ok(CommitterIdentity {
|
||||||
|
name: name.to_string(),
|
||||||
|
email: email.to_string(),
|
||||||
|
date: date.to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn is_ancestor(&self, ancestor: &str, descendant: &str) -> Result<bool> {
|
pub fn is_ancestor(&self, ancestor: &str, descendant: &str) -> Result<bool> {
|
||||||
let status = self
|
let status = self
|
||||||
.command()
|
.command()
|
||||||
@@ -709,6 +777,35 @@ impl GitMirror {
|
|||||||
.into())
|
.into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn worktree_git_with_committer<const N: usize>(
|
||||||
|
&self,
|
||||||
|
worktree: &Path,
|
||||||
|
args: [&str; N],
|
||||||
|
committer: &CommitterIdentity,
|
||||||
|
) -> Result<()> {
|
||||||
|
let output = Command::new("git")
|
||||||
|
.arg("-C")
|
||||||
|
.arg(worktree)
|
||||||
|
.args(args)
|
||||||
|
.env("GIT_COMMITTER_NAME", &committer.name)
|
||||||
|
.env("GIT_COMMITTER_EMAIL", &committer.email)
|
||||||
|
.env("GIT_COMMITTER_DATE", &committer.date)
|
||||||
|
.output()
|
||||||
|
.with_context(|| "failed to run git")?;
|
||||||
|
if output.status.success() {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(GitCommandError::new(
|
||||||
|
"git",
|
||||||
|
self.redactor
|
||||||
|
.redact(&String::from_utf8_lossy(&output.stdout)),
|
||||||
|
self.redactor
|
||||||
|
.redact(&String::from_utf8_lossy(&output.stderr)),
|
||||||
|
)
|
||||||
|
.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn short_sha(sha: &str) -> &str {
|
fn short_sha(sha: &str) -> &str {
|
||||||
|
|||||||
+56
-1
@@ -178,7 +178,15 @@ fn auto_rebase_branch_conflict_replays_later_tip_and_marks_force_targets() {
|
|||||||
let a_tip = fixture.commit_file("a", "a.txt", "a\n", 1_700_000_100);
|
let a_tip = fixture.commit_file("a", "a.txt", "a\n", 1_700_000_100);
|
||||||
fixture.push_head(&fixture.remote_a, "main");
|
fixture.push_head(&fixture.remote_a, "main");
|
||||||
fixture.reset_hard(&base);
|
fixture.reset_hard(&base);
|
||||||
let b_tip = fixture.commit_file("b", "b.txt", "b\n", 1_700_000_200);
|
let b_tip = fixture.commit_file_with_committer(
|
||||||
|
"b",
|
||||||
|
"b.txt",
|
||||||
|
"b\n",
|
||||||
|
1_700_000_200,
|
||||||
|
"Original Committer",
|
||||||
|
"original-committer@example.test",
|
||||||
|
1_700_000_250,
|
||||||
|
);
|
||||||
fixture.push_head(&fixture.remote_b, "main");
|
fixture.push_head(&fixture.remote_b, "main");
|
||||||
|
|
||||||
let mirror = fixture.mirror();
|
let mirror = fixture.mirror();
|
||||||
@@ -205,6 +213,10 @@ fn auto_rebase_branch_conflict_replays_later_tip_and_marks_force_targets() {
|
|||||||
.find(|update| update.target_remote == "b")
|
.find(|update| update.target_remote == "b")
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(b_update.force);
|
assert!(b_update.force);
|
||||||
|
assert_eq!(
|
||||||
|
fixture.mirror_committer(&decision.sha),
|
||||||
|
fixture.mirror_committer(&b_tip)
|
||||||
|
);
|
||||||
|
|
||||||
mirror
|
mirror
|
||||||
.push_branch_updates(&fixture.remotes(), &decision.updates)
|
.push_branch_updates(&fixture.remotes(), &decision.updates)
|
||||||
@@ -495,6 +507,35 @@ impl GitFixture {
|
|||||||
self.head()
|
self.head()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn commit_file_with_committer(
|
||||||
|
&self,
|
||||||
|
message: &str,
|
||||||
|
file_name: &str,
|
||||||
|
contents: &str,
|
||||||
|
author_timestamp: i64,
|
||||||
|
committer_name: &str,
|
||||||
|
committer_email: &str,
|
||||||
|
committer_timestamp: i64,
|
||||||
|
) -> String {
|
||||||
|
let path = self.work.join(file_name);
|
||||||
|
fs::write(path, contents).unwrap();
|
||||||
|
git(Some(&self.work), ["add", file_name]);
|
||||||
|
|
||||||
|
let author_date = format!("@{author_timestamp} +0000");
|
||||||
|
let committer_date = format!("@{committer_timestamp} +0000");
|
||||||
|
let output = Command::new("git")
|
||||||
|
.current_dir(&self.work)
|
||||||
|
.env("GIT_AUTHOR_DATE", &author_date)
|
||||||
|
.env("GIT_COMMITTER_NAME", committer_name)
|
||||||
|
.env("GIT_COMMITTER_EMAIL", committer_email)
|
||||||
|
.env("GIT_COMMITTER_DATE", &committer_date)
|
||||||
|
.args(["commit", "-m", message])
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
assert_success(&output, "git commit");
|
||||||
|
self.head()
|
||||||
|
}
|
||||||
|
|
||||||
fn head(&self) -> String {
|
fn head(&self) -> String {
|
||||||
git_output(Some(&self.work), ["rev-parse", "HEAD"])
|
git_output(Some(&self.work), ["rev-parse", "HEAD"])
|
||||||
}
|
}
|
||||||
@@ -559,6 +600,20 @@ impl GitFixture {
|
|||||||
.status
|
.status
|
||||||
.success()
|
.success()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn mirror_committer(&self, reference: &str) -> String {
|
||||||
|
git_output(
|
||||||
|
None,
|
||||||
|
[
|
||||||
|
"--git-dir",
|
||||||
|
self.mirror_path.to_str().unwrap(),
|
||||||
|
"show",
|
||||||
|
"-s",
|
||||||
|
"--format=%cn <%ce> %cI",
|
||||||
|
reference,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn git<const N: usize>(current_dir: Option<&Path>, args: [&str; N]) {
|
fn git<const N: usize>(current_dir: Option<&Path>, args: [&str; N]) {
|
||||||
|
|||||||
Reference in New Issue
Block a user