diff --git a/src/cgh.rs b/src/cgh.rs index 1a51123..5b0534b 100644 --- a/src/cgh.rs +++ b/src/cgh.rs @@ -119,6 +119,12 @@ fn push(cfg: &cli::Push) -> Result<()> { .context("could not build diffs")?; LocalChange::push_all(any_changes.iter().map(|ac| ac.local_change())) .context("could not push all local changes")?; + any_changes + .first() + .context("no local changes to tag")? + .local_change() + .push_tag() + .context("could not push tag for final local change")?; // FIXME: Should try to restore the original branch contents if we fail from this point on. It // would be at least an attempt at being "atomic" about the push, and it would mean we don't // lose the interdiff in a future re-run. diff --git a/src/change.rs b/src/change.rs index f470021..6815e42 100644 --- a/src/change.rs +++ b/src/change.rs @@ -81,11 +81,17 @@ impl LocalChange { let remote_branch_ref = self.remote_branch_ref(); format!("{oid}:{remote_branch_ref}") } + pub fn tag_refspec(&self) -> String { + let oid = self.oid; + let tag_name = format!("{}{oid}", env::user_branch_prefix()); + format!("{oid}:refs/tags/{tag_name}") + } pub fn push_all<'a, I: Iterator>(iterator: I) -> Result<()> { - let refspecs: Vec = iterator.map(|lc| lc.push_refspec()).collect(); - if refspecs.is_empty() { + let local_changes: Vec<&Self> = iterator.collect(); + if local_changes.is_empty() { bail!("no refs to push"); } + let refspecs: Vec = local_changes.iter().map(|lc| lc.push_refspec()).collect(); let mut cmd = Command::new("git"); let mut args = vec![ "push".to_string(), @@ -98,6 +104,13 @@ impl LocalChange { exec!(dry_return = (), cmd); Ok(()) } + pub fn push_tag(&self) -> Result<()> { + let tag_refspec = self.tag_refspec(); + let mut cmd = Command::new("git"); + cmd.args(["push", env::tag_remote(), "--atomic", tag_refspec.as_str()]); + exec!(dry_return = (), cmd); + Ok(()) + } pub fn fetch_all<'a, I: Iterator>(iterator: I) -> Result<()> { let refspecs: Vec = iterator .map(|lc| format!("{}:", lc.remote_branch_ref())) diff --git a/src/cli.rs b/src/cli.rs index 19d2a87..da4b18c 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -74,6 +74,10 @@ pub struct Globals { /// The name of the git remote corresponding to the GitHub repo to operate on. #[arg(long, global = true)] pub remote: Option, + /// The name of the git remote corresponding to the GitHub repo to push one tag per stack + /// revision, to retain history after force-pushes. + #[arg(long, global = true)] + pub tag_remote: Option, /// The branch on `remote` which acts as the "base" branch, which all PRs are ultimately /// relative to. #[arg(long, global = true)] @@ -101,13 +105,15 @@ pub enum Command { /// /// The commits `${base}..HEAD` must each have a `Change-Id:` trailer. Each commit will be /// force-pushed to a corresponding branch named `${user_branch_prefix}${change_id}` on - /// `${remote}`. Each commit will be matched to its existing PR or else a new PR will be + /// `${remote}`. A tag named `${user_branch_prefix}${final_commit_oid}` will also be pushed to + /// `${tag_remote}`. Each commit will be matched to its existing PR or else a new PR will be /// created for it. The PRs will be "stacked" such that they reproduce the local branch /// sequence, with additional trailers in the PR message body to help reviewers navigate the /// stack. /// /// Note: This command will never modify your commits or refs, even their messages. No local - /// branches are created or destroyed. All mutation occurs exclusively on the `$remote`. + /// branches are created or destroyed. All mutation occurs exclusively on `$remote` and + /// `$tag_remote`. #[command(visible_alias = "p")] Push(Push), /// With no arguments, print the current stack's short-name. With an argument, set it. diff --git a/src/env.rs b/src/env.rs index da43115..e26d273 100644 --- a/src/env.rs +++ b/src/env.rs @@ -34,6 +34,7 @@ impl ThreadLocalRepo { #[derive(Default, Serialize, Deserialize)] pub struct Config { remote: String, + tag_remote: String, base_branch: String, user_branch_prefix: String, reviewer_groups: Option>>, @@ -86,6 +87,9 @@ pub fn validate() -> Result<()> { if remote().is_empty() { bail!("field `remote` cannot be empty"); } + if tag_remote().is_empty() { + bail!("field `tag_remote` cannot be empty"); + } if base_branch().is_empty() { bail!("field `base_branch` cannot be empty"); } @@ -106,6 +110,13 @@ pub fn remote() -> &'static str { .unwrap_or(CONFIG.remote.as_str()) } +pub fn tag_remote() -> &'static str { + CLI.globals + .tag_remote + .as_deref() + .unwrap_or(CONFIG.tag_remote.as_str()) +} + pub fn base_branch() -> &'static str { CLI.globals .base_branch