[metadata: 3/3]: Use git-config branch keys to track stack metadata#56
Merged
slinder1 merged 1 commit intoJun 25, 2026
Merged
Conversation
Owner
Author
🛠️ Initial changes (click to expand):diff --git b/src/cgh.rs a/src/cgh.rs
@@ -5,7 +5,8 @@ use crate::change::{self, AnyChange, Change, LocalChange};
use crate::cli;
use crate::env;
use crate::gh::{self, Pr, PrState};
-use crate::util::{Extract, RepoExt};
+use crate::metadata::StackMetadata;
+use crate::util::Extract;
use anyhow::{Context, Result, bail};
use rayon::prelude::*;
use std::collections::HashSet;
@@ -26,15 +27,16 @@ pub fn cgh() -> Result<()> {
env::validate().context("invalid configuration")?;
match cli.command {
cli::Command::Push(ref cfg) => push(cfg),
+ cli::Command::Name(ref cfg) => name(cfg),
+ cli::Command::Merge(ref cfg) => merge(cfg),
cli::Command::Url(ref cfg) => url(cfg),
cli::Command::InstallHook(ref cfg) => install_hook(cfg),
}
}
fn push(cfg: &cli::Push) -> Result<()> {
- let repo = env::repo();
- let branch = repo.head_branch().context("HEAD must be a branch")?;
- let branch_desc = repo.branch_desc(branch).ok();
+ let stack_meta =
+ StackMetadata::from_repo().context("could not parse branch description metadata")?;
let mut reviewers = vec![];
for group_key in cfg.reviewer_groups.iter() {
let group = env::reviewer_groups()
@@ -47,6 +49,20 @@ fn push(cfg: &cli::Push) -> Result<()> {
change::get_local_changes().context("could not enumerate current local branch")?;
let mut prs_by_change_id = gh::prs_by_change_id(|pr| !pr.in_state(PrState::Closed))
.context("could not enumerate remote prs")?;
+ let mut merged_prs = vec![];
+ for merged_change_id in stack_meta.merged_change_ids {
+ let pr = prs_by_change_id
+ .remove(&merged_change_id)
+ .with_context(|| format!("merged change {} has no pr", merged_change_id))?;
+ if pr.in_state(PrState::Open) {
+ bail!(
+ "pr {} for merged change {} is still open",
+ pr.number,
+ merged_change_id,
+ );
+ }
+ merged_prs.push(pr);
+ }
let mut any_changes = vec![];
for local_change in local_changes {
any_changes.push(match prs_by_change_id.remove(&local_change.id) {
@@ -54,7 +70,7 @@ fn push(cfg: &cli::Push) -> Result<()> {
Some(pr) => {
if pr.in_state(PrState::Merged) {
bail!(
- "pr {} with Change-Id {} already merged",
+ "pr {} for unmerged change {} is already merged",
pr.number,
local_change.id
);
@@ -131,15 +147,13 @@ fn push(cfg: &cli::Push) -> Result<()> {
.next()
.map(|p| p.local_change.remote_branch())
.unwrap_or_else(|| env::base_branch().to_owned());
- if c.is_nonempty() {
- c.pr.set_base(base.as_ref()).with_context(|| {
- format!(
- "could not retarget pr {} to branch: {:?}",
- c.pr.number, base,
- )
- })?;
- }
- c.render_pr_ui(&changes, branch_desc.as_deref())
+ c.pr.set_base(base.as_ref()).with_context(|| {
+ format!(
+ "could not retarget pr {} to branch: {:?}",
+ c.pr.number, base,
+ )
+ })?;
+ c.render_pr_ui(&changes, &stack_meta.stack_name, &merged_prs)
.context("could not render pseudo-ui in pr title/body")
})
.collect::<Result<Vec<_>>>()
@@ -181,6 +195,42 @@ fn detect_cycles(any_changes: &[AnyChange]) -> bool {
false
}
+fn name(cfg: &cli::Name) -> Result<()> {
+ let mut stack_meta = StackMetadata::from_repo()?;
+ match cfg.new_name {
+ None => println!("{}", stack_meta.stack_name),
+ Some(ref new_name) => {
+ stack_meta.stack_name = new_name.to_string();
+ stack_meta.to_repo()?;
+ },
+ }
+ Ok(())
+}
+
+fn merge(_cfg: &cli::Merge) -> Result<()> {
+ let mut stack_meta =
+ StackMetadata::from_repo().context("could not parse branch description metadata")?;
+ let local_change = change::get_local_changes()
+ .context("could not enumerate current local branch")?
+ .pop()
+ .context("no local changes")?;
+ if stack_meta.merged_change_ids.contains(&local_change.id) {
+ bail!("change {} was already merged", local_change.id);
+ }
+ let mut prs_by_change_id = gh::prs_by_change_id(|pr| !pr.in_state(PrState::Closed))
+ .context("could not enumerate remote prs")?;
+ let pr = prs_by_change_id
+ .remove(&local_change.id)
+ .with_context(|| format!("change {} has no pr", local_change.id))?;
+ let change = Change { local_change, pr };
+ change.merge()?;
+ stack_meta
+ .merged_change_ids
+ .push(change.local_change.id.clone());
+ stack_meta.to_repo()?;
+ Ok(())
+}
+
fn url(_cfg: &cli::Url) -> Result<()> {
let local_changes =
change::get_local_changes().context("could not enumerate current local branch")?;
diff --git b/src/change.rs a/src/change.rs
@@ -143,7 +143,12 @@ pub struct Change {
}
impl Change {
- pub fn render_pr_ui(&self, changes: &[Self], branch_desc: Option<&str>) -> Result<()> {
+ pub fn render_pr_ui(
+ &self,
+ changes: &[Self],
+ stack_name: &str,
+ merged_prs: &[Pr],
+ ) -> Result<()> {
let commit = self.local_change.commit()?;
let mut index = None;
let title = String::from(
@@ -168,17 +173,20 @@ impl Change {
}
body.push('\n');
}
+ let index = index.expect(
+ "render_pr_ui asked to render into a stack of changes which does not contain self?",
+ );
+ for pr in merged_prs.iter().rev() {
+ body.push_str(&format!("- #{}\n", pr.number));
+ }
body.push_str(&format!("- `{}`\n\n<sub>(Note: Closed and merged PRs may not be reflected here and PR numbering is not stable.)</sub>\n", env::base_branch()));
- let count = changes.len();
- let position = count
- - index.expect(
- "render_pr_ui asked to render into a stack of changes which does not contain self?",
- );
- let prefix = branch_desc
- .map(|d| d.trim())
- .filter(|d| !d.is_empty())
- .map(|d| format!("{d}: "))
- .unwrap_or_else(|| "".into());
+ let count = changes.len() + merged_prs.len();
+ let position = count - index;
+ let prefix = if stack_name.is_empty() {
+ "".into()
+ } else {
+ format!("{}: ", stack_name)
+ };
self.pr
.set_title_and_body(&format!("[{prefix}{position}/{count}]: {title}"), &body)
}
@@ -224,6 +232,15 @@ impl Change {
.with_context(|| format!("failed to generate interdiff for change {change}"))?;
Ok(out)
}
+ pub fn merge(&self) -> Result<()> {
+ let commit = self.local_change.commit()?;
+ let subject_raw = commit.summary()?.context("couldn't get summary")?;
+ let subject = format!("{} (#{})", subject_raw, self.pr.number);
+ let body = commit.body()?.context("couldn't get summary")?;
+ let sha = format!("{}", self.local_change.oid);
+ self.pr.merge(&subject, body, &sha)?;
+ Ok(())
+ }
}
fn tree<'repo>(commit: &Commit<'repo>) -> Result<Tree<'repo>> {
diff --git b/src/cli.rs a/src/cli.rs
@@ -99,11 +99,24 @@ pub enum Command {
/// sequence, with additional trailers in the PR message body to help reviewers navigate the
/// stack.
///
- /// Note: This command will never modify the local repo. No local branches are created or
- /// destroyed, and no commits are touched. All mutation occurs exclusively on the `$remote`.
+ /// 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`.
#[command(visible_alias = "p")]
Push(Push),
- /// Print the PR URL of the top-most change which already has one.
+ /// Merge the next change.
+ ///
+ /// If successful, this will modify the local `branch.<name>.description` git config entry to
+ /// record the merged change. This allows future `push`es to include merged changes in the
+ /// reviewer stack "UI", and to keep the relative numbering of changes stable.
+ ///
+ /// If a change's PR is merged in any other way, it will "disappear" from the stack, affecting
+ /// all downstream numbering and the total number of changes in the stack. If you want to
+ /// manually correct this, edit `branch.<name>.description` using e.g. `git branch
+ /// --edit-description` and append the merged change's ID to the `merged_change_ids` list.
+ Merge(Merge),
+ /// With no arguments, print the current stack name. With an argument, set it.
+ Name(Name),
+ /// Print the PR URL of the top-most (i.e. last) change which already has one.
Url(Url),
/// Install a commit-msg hook in the current git repo to create `Change-Id:` trailers.
InstallHook(InstallHook),
@@ -126,6 +139,14 @@ pub struct Push {
}
#[derive(Args)]
+pub struct Name {
+ pub new_name: Option<String>,
+}
+
+#[derive(Args)]
+pub struct Merge;
+
+#[derive(Args)]
pub struct Url;
#[derive(Args)]
diff --git b/src/gh.rs a/src/gh.rs
@@ -223,6 +223,32 @@ impl Pr {
bail!("gh pr create did not produce a URL")
}
+ pub fn merge(&self, subject: &str, body: &str, sha: &str) -> Result<()> {
+ let mut cmd = gh();
+ let mut body_arg = ArgInlineOrFile::new("body");
+ let body_arg_string = body_arg.arg(body)?;
+ let args = self.args_for(
+ "merge",
+ [
+ "--squash",
+ "--match-head-commit",
+ sha,
+ "--subject",
+ subject,
+ &body_arg_string,
+ ],
+ );
+ cmd.args(args);
+ let output = exec!(dry_return = (), cmd);
+ // gh cli doesn't consider this a failure, but we want to so we don't mistakenly add an
+ // already-merged change to the metadata. We could instead infer that the change should be
+ // added to the metadata, but we can't necessarily assume it is the *next* merged change(?)
+ if String::from_utf8_lossy(output.stderr.as_ref()).contains("was already merged") {
+ bail!("pr {} was already merged", self.number);
+ }
+ Ok(())
+ }
+
pub fn get_url(&self) -> String {
format!("{}/pull/{}", REPO_URL.as_str(), self.number)
}
diff --git b/src/main.rs a/src/main.rs
@@ -6,6 +6,7 @@ mod change;
mod cli;
mod env;
mod gh;
+mod metadata;
mod util;
fn main() -> anyhow::Result<()> {
diff --git b/src/metadata.rs a/src/metadata.rs
@@ -0,0 +1,46 @@
+use crate::env;
+use crate::util::{RepoExt};
+use serde::{Deserialize, Serialize};
+use anyhow::{Result, Context, bail};
+use std::collections::HashSet;
+
+#[derive(Default, Serialize, Deserialize)]
+#[serde(default)]
+pub struct StackMetadata {
+ pub stack_name: String,
+ pub merged_change_ids: Vec<String>,
+}
+
+impl StackMetadata {
+ pub fn from_repo() -> Result<Self> {
+ let repo = env::repo();
+ let branch = repo.head_branch().context("HEAD must be a branch")?;
+ let branch_desc = repo.branch_desc(&branch);
+ let branch_desc = branch_desc.as_deref().unwrap_or("");
+ let res = Self::from_str(branch_desc)?;
+ let mut changed_set = HashSet::new();
+ for merged_change_id in res.merged_change_ids.iter() {
+ if !changed_set.insert(merged_change_id.as_str()) {
+ bail!(
+ "duplicate in merged_change_ids: {merged_change_id} (correct with `git branch --edit-description`)"
+ );
+ }
+ }
+ Ok(res)
+ }
+ pub fn to_repo(&self) -> Result<()> {
+ if env::dry_run() {
+ eprintln!("would-set-description: {:?}", self.to_string()?);
+ return Ok(());
+ }
+ let repo = env::repo();
+ let branch = repo.head_branch().context("HEAD must be a branch")?;
+ repo.set_branch_desc(&branch, &self.to_string()?)
+ }
+ pub fn from_str(s: &str) -> Result<Self> {
+ Ok(toml::from_str(s).context("branch description is not valid metadata TOML")?)
+ }
+ pub fn to_string(&self) -> Result<String> {
+ Ok(toml::to_string_pretty(self).context("could not serialize metadata TOML")?)
+ }
+}
diff --git b/src/util.rs a/src/util.rs
@@ -64,7 +64,8 @@ impl<T, E: Debug> Extract for std::result::Result<T, E> {
pub trait RepoExt {
fn head_branch<'repo>(&'repo self) -> Result<Branch<'repo>>;
- fn branch_desc(&self, str: Branch) -> Result<String>;
+ fn branch_desc(&self, branch: &Branch) -> Result<String>;
+ fn set_branch_desc(&self, branch: &Branch, desc: &str) -> Result<()>;
}
impl RepoExt for Repository {
@@ -75,7 +76,7 @@ impl RepoExt for Repository {
}
Ok(Branch::wrap(branch))
}
- fn branch_desc(&self, branch: Branch) -> Result<String> {
+ fn branch_desc(&self, branch: &Branch) -> Result<String> {
let branch_name = branch
.name()
.context("HEAD branch has no name")?
@@ -88,10 +89,18 @@ impl RepoExt for Repository {
let full_desc = config_entry
.value()
.context("branch description is not valid utf8")?;
- Ok(full_desc
- .split('\n')
- .next()
- .unwrap_or(full_desc)
- .to_string())
+ Ok(full_desc.to_string())
+ }
+ fn set_branch_desc(&self, branch: &Branch, desc: &str) -> Result<()> {
+ let branch_name = branch
+ .name()
+ .context("HEAD branch has no name")?
+ .context("HEAD branch name is not valid utf-8")?;
+ let mut repo_config = self.config().context("repo has no config")?;
+ let config_key = format!("branch.{branch_name}.description");
+ repo_config
+ .set_str(config_key.as_str(), desc)
+ .context("no branch description")?;
+ Ok(())
}
}
|
This was referenced Jun 25, 2026
4f133e4 to
56b6b61
Compare
slinder1
commented
Jun 25, 2026
| c.pr.number, base, | ||
| ) | ||
| })?; | ||
| c.render_pr_ui(&changes, &stack_meta.stack_name, &merged_prs) |
Owner
Author
There was a problem hiding this comment.
I am undecided on whether merged_prs should also have their UI updated. I think starting with "the tool doesn't modify merged PRs whatsoever" is best, but it might be OK to clean up their UI so it doesn't fall out of sync as the rest of the stack changes.
Owner
Author
🛠️ Changes since last version (click to expand):diff --git b/src/cgh.rs a/src/cgh.rs
@@ -153,7 +153,7 @@ fn push(cfg: &cli::Push) -> Result<()> {
c.pr.number, base,
)
})?;
- c.render_pr_ui(&changes, &stack_meta.stack_name, &merged_prs)
+ c.render_pr_ui(&changes, &stack_meta.short_name, &merged_prs)
.context("could not render pseudo-ui in pr title/body")
})
.collect::<Result<Vec<_>>>()
@@ -198,9 +198,9 @@ fn detect_cycles(any_changes: &[AnyChange]) -> bool {
fn name(cfg: &cli::Name) -> Result<()> {
let mut stack_meta = StackMetadata::from_repo()?;
match cfg.new_name {
- None => println!("{}", stack_meta.stack_name),
+ None => println!("{}", stack_meta.short_name),
Some(ref new_name) => {
- stack_meta.stack_name = new_name.to_string();
+ stack_meta.short_name = new_name.to_string();
stack_meta.to_repo()?;
},
}
diff --git b/src/change.rs a/src/change.rs
@@ -146,7 +146,7 @@ impl Change {
pub fn render_pr_ui(
&self,
changes: &[Self],
- stack_name: &str,
+ short_name: &str,
merged_prs: &[Pr],
) -> Result<()> {
let commit = self.local_change.commit()?;
@@ -182,10 +182,10 @@ impl Change {
body.push_str(&format!("- `{}`\n\n<sub>(Note: Closed and merged PRs may not be reflected here and PR numbering is not stable.)</sub>\n", env::base_branch()));
let count = changes.len() + merged_prs.len();
let position = count - index;
- let prefix = if stack_name.is_empty() {
+ let prefix = if short_name.is_empty() {
"".into()
} else {
- format!("{}: ", stack_name)
+ format!("{}: ", short_name)
};
self.pr
.set_title_and_body(&format!("[{prefix}{position}/{count}]: {title}"), &body)
diff --git b/src/cli.rs a/src/cli.rs
@@ -103,19 +103,26 @@ pub enum Command {
/// branches are created or destroyed. All mutation occurs exclusively on the `$remote`.
#[command(visible_alias = "p")]
Push(Push),
+ /// With no arguments, print the current stack's short-name. With an argument, set it.
+ ///
+ /// The short-name is tracked in the `branch.<name>.cgh-shortName` git config entry,
+ /// and is used to prefix the PR title, e.g. `[${short-name} 3/5] ...`
+ Name(Name),
/// Merge the next change.
///
- /// If successful, this will modify the local `branch.<name>.description` git config entry to
- /// record the merged change. This allows future `push`es to include merged changes in the
- /// reviewer stack "UI", and to keep the relative numbering of changes stable.
+ /// If successful, this will modify the local `branch.<name>.cgh-mergedChangeIds` git config
+ /// entry to record that the change was merged. This allows future `cgh push`es to include
+ /// merged changes in the reviewer stack "UI", keeping the relative numbering of changes stable.
///
/// If a change's PR is merged in any other way, it will "disappear" from the stack, affecting
/// all downstream numbering and the total number of changes in the stack. If you want to
- /// manually correct this, edit `branch.<name>.description` using e.g. `git branch
- /// --edit-description` and append the merged change's ID to the `merged_change_ids` list.
+ /// manually correct this, edit `branch.<name>.cgh-mergedChangeIds` (a ':' separated list) using
+ /// e.g. `git config --edit` and append the merged change's ID to the list (or create it, if it
+ /// does not already exist).
+ ///
+ /// If you don't care about the renumbering behavior, you can safely ignore this subcommand (it
+ /// is purely aesthetic).
Merge(Merge),
- /// With no arguments, print the current stack name. With an argument, set it.
- Name(Name),
/// Print the PR URL of the top-most (i.e. last) change which already has one.
Url(Url),
/// Install a commit-msg hook in the current git repo to create `Change-Id:` trailers.
diff --git b/src/metadata.rs a/src/metadata.rs
@@ -1,23 +1,33 @@
use crate::env;
use crate::util::{RepoExt};
-use serde::{Deserialize, Serialize};
use anyhow::{Result, Context, bail};
use std::collections::HashSet;
-#[derive(Default, Serialize, Deserialize)]
-#[serde(default)]
+#[derive(Default, Debug)]
pub struct StackMetadata {
- pub stack_name: String,
+ pub short_name: String,
pub merged_change_ids: Vec<String>,
}
+const SHORT_NAME_KEY: &'static str = "shortName";
+const MERGED_CHANGE_IDS_KEY: &'static str = "mergedChangeIds";
+
impl StackMetadata {
pub fn from_repo() -> Result<Self> {
let repo = env::repo();
let branch = repo.head_branch().context("HEAD must be a branch")?;
- let branch_desc = repo.branch_desc(&branch);
- let branch_desc = branch_desc.as_deref().unwrap_or("");
- let res = Self::from_str(branch_desc)?;
+ let branch_config = repo.branch_config(&branch)?;
+ let short_name = branch_config.get(SHORT_NAME_KEY)?;
+ let merged_change_ids = branch_config
+ .get(MERGED_CHANGE_IDS_KEY)?
+ .split(':')
+ .filter(|s| !s.is_empty())
+ .map(String::from)
+ .collect();
+ let res = StackMetadata {
+ short_name,
+ merged_change_ids,
+ };
let mut changed_set = HashSet::new();
for merged_change_id in res.merged_change_ids.iter() {
if !changed_set.insert(merged_change_id.as_str()) {
@@ -30,17 +40,14 @@ impl StackMetadata {
}
pub fn to_repo(&self) -> Result<()> {
if env::dry_run() {
- eprintln!("would-set-description: {:?}", self.to_string()?);
+ eprintln!("would-set: {:?}", self);
return Ok(());
}
let repo = env::repo();
let branch = repo.head_branch().context("HEAD must be a branch")?;
- repo.set_branch_desc(&branch, &self.to_string()?)
- }
- pub fn from_str(s: &str) -> Result<Self> {
- Ok(toml::from_str(s).context("branch description is not valid metadata TOML")?)
- }
- pub fn to_string(&self) -> Result<String> {
- Ok(toml::to_string_pretty(self).context("could not serialize metadata TOML")?)
+ let mut branch_config = repo.branch_config(&branch)?;
+ branch_config.set(SHORT_NAME_KEY, &self.short_name)?;
+ branch_config.set(MERGED_CHANGE_IDS_KEY, &self.merged_change_ids.join(":"))?;
+ Ok(())
}
}
diff --git b/src/util.rs a/src/util.rs
@@ -2,7 +2,7 @@
// SPDX-License-Identifier: MIT
use anyhow::{Context, Result, bail};
-use git2::{Branch, Repository};
+use git2::{Branch, Repository, Config};
use std::fmt::Debug;
use std::process::{Command, Output};
@@ -63,9 +63,8 @@ impl<T, E: Debug> Extract for std::result::Result<T, E> {
}
pub trait RepoExt {
- fn head_branch<'repo>(&'repo self) -> Result<Branch<'repo>>;
- fn branch_desc(&self, branch: &Branch) -> Result<String>;
- fn set_branch_desc(&self, branch: &Branch, desc: &str) -> Result<()>;
+ fn head_branch(&self) -> Result<Branch<'_>>;
+ fn branch_config<'repo>(&self, branch: &'repo Branch) -> Result<BranchConfig<'repo>>;
}
impl RepoExt for Repository {
@@ -76,31 +75,58 @@ impl RepoExt for Repository {
}
Ok(Branch::wrap(branch))
}
- fn branch_desc(&self, branch: &Branch) -> Result<String> {
- let branch_name = branch
- .name()
- .context("HEAD branch has no name")?
- .context("HEAD branch name is not valid utf-8")?;
- let repo_config = self.config().context("repo has no config")?;
- let config_key = format!("branch.{branch_name}.description");
- let config_entry = repo_config
- .get_entry(config_key.as_str())
- .context("no branch description")?;
- let full_desc = config_entry
- .value()
- .context("branch description is not valid utf8")?;
- Ok(full_desc.to_string())
+ fn branch_config<'repo>(&self, branch: &'repo Branch) -> Result<BranchConfig<'repo>> {
+ BranchConfig::new(self, branch)
}
- fn set_branch_desc(&self, branch: &Branch, desc: &str) -> Result<()> {
- let branch_name = branch
- .name()
- .context("HEAD branch has no name")?
- .context("HEAD branch name is not valid utf-8")?;
- let mut repo_config = self.config().context("repo has no config")?;
- let config_key = format!("branch.{branch_name}.description");
- repo_config
- .set_str(config_key.as_str(), desc)
- .context("no branch description")?;
+}
+
+pub struct BranchConfig<'repo> {
+ branch_name: &'repo str,
+ config: Config,
+}
+
+impl<'repo> BranchConfig<'repo> {
+ pub fn new(repo: &Repository, branch: &'repo Branch<'_>) -> Result<Self> {
+ let branch_name = branch_name(branch)?;
+ let config = config(repo)?;
+ Ok(Self {
+ branch_name,
+ config,
+ })
+ }
+ fn format_key(&self, key: &str) -> String {
+ format!("branch.{}.cgh-{key}", self.branch_name)
+ }
+ pub fn get(&self, key: &str) -> Result<String> {
+ let config_key = self.format_key(key);
+ let value_result = self.config.get_entry(config_key.as_str());
+ let value = match value_result {
+ Ok(ce) => ce
+ .value()
+ .context("error getting config entry value")?
+ .to_string(),
+ // all the config values can safely default to the empty string
+ Err(e) if e.code() == git2::ErrorCode::NotFound => "".to_string(),
+ Err(e) => return Err(anyhow::Error::new(e).context("error getting config entry")),
+ };
+ Ok(value)
+ }
+ pub fn set(&mut self, key: &str, val: &str) -> Result<()> {
+ let config_key = self.format_key(key);
+ self.config
+ .set_str(config_key.as_str(), val)
+ .with_context(|| format!("could not update config key {config_key}"))?;
Ok(())
}
}
+
+fn branch_name<'repo>(branch: &'repo Branch) -> Result<&'repo str> {
+ Ok(branch
+ .name()
+ .context("HEAD branch has no name")?
+ .context("HEAD branch name is not valid utf-8")?)
+}
+
+fn config(repo: &Repository) -> Result<Config> {
+ Ok(repo.config().context("repo has no config")?)
+}
|
Owner
Author
🛠️ Changes since last version (click to expand):diff --git b/src/cgh.rs a/src/cgh.rs
@@ -201,7 +201,7 @@ fn name(cfg: &cli::Name) -> Result<()> {
Some(ref new_name) => {
stack_meta.short_name = new_name.to_string();
stack_meta.to_repo()?;
- },
+ }
}
Ok(())
}
diff --git b/src/metadata.rs a/src/metadata.rs
@@ -1,6 +1,6 @@
use crate::env;
-use crate::util::{RepoExt};
-use anyhow::{Result, Context, bail};
+use crate::util::RepoExt;
+use anyhow::{Context, Result, bail};
use std::collections::HashSet;
#[derive(Default, Debug)]
@@ -9,8 +9,8 @@ pub struct StackMetadata {
pub merged_change_ids: Vec<String>,
}
-const SHORT_NAME_KEY: &'static str = "shortName";
-const MERGED_CHANGE_IDS_KEY: &'static str = "mergedChangeIds";
+const SHORT_NAME_KEY: &str = "shortName";
+const MERGED_CHANGE_IDS_KEY: &str = "mergedChangeIds";
impl StackMetadata {
pub fn from_repo() -> Result<Self> {
diff --git b/src/util.rs a/src/util.rs
@@ -2,7 +2,7 @@
// SPDX-License-Identifier: MIT
use anyhow::{Context, Result, bail};
-use git2::{Branch, Repository, Config};
+use git2::{Branch, Config, Repository};
use std::fmt::Debug;
use std::process::{Command, Output};
@@ -121,12 +121,12 @@ impl<'repo> BranchConfig<'repo> {
}
fn branch_name<'repo>(branch: &'repo Branch) -> Result<&'repo str> {
- Ok(branch
+ branch
.name()
.context("HEAD branch has no name")?
- .context("HEAD branch name is not valid utf-8")?)
+ .context("HEAD branch name is not valid utf-8")
}
fn config(repo: &Repository) -> Result<Config> {
- Ok(repo.config().context("repo has no config")?)
+ repo.config().context("repo has no config")
}
|
56b6b61 to
2723877
Compare
265efd1 to
24ec532
Compare
0e87fa6 to
1c0aade
Compare
Base automatically changed from
users/slinder1/I3212a00b2ed3a52952eb3a9ccf9d30e40a51c3f4
to
main
June 25, 2026 19:39
We were using the `branch.<name>.description` entry already for the short-name of the stack, but really it is meant for longer form content that other tools already digest. Add a `name` subcommand to edit and print the stack short-name, saved to `branch.<name>.cgh-shortName`. Add a `merge` subcommand to automatically maintain the `branch.<name>.cgh-mergedChangeIds` metadata. If a user doesn't care about the apparent size of the stack shrinking as it "forgets" about merged changes, then they can ignore this and use the web-UI or `gh pr merge` or `git push` themselves. Note that git-config doesn't seem to be able to nest keys any deeper than `branch.<name>.<key>`, hence we namespace our sub-keys with the `cgh-` prefix. Also note that both entries default to the empty string, and are assumed to be the default if not present in the config. This means a user without interest in the features that depend on the keys won't see them in their config. Change-Id: Ib6b188c23fc84460fad74460e05fd6ba7a02e8ca
24ec532 to
d4d4455
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
We were using the
branch.<name>.descriptionentry already for theshort-name of the stack, but really it is meant for longer form content
that other tools already digest.
Add a
namesubcommand to edit and print the stack short-name, savedto
branch.<name>.cgh-shortName.Add a
mergesubcommand to automatically maintain thebranch.<name>.cgh-mergedChangeIdsmetadata. If a user doesn't careabout the apparent size of the stack shrinking as it "forgets" about
merged changes, then they can ignore this and use the web-UI or
gh pr mergeorgit pushthemselves.Note that git-config doesn't seem to be able to nest keys any deeper
than
branch.<name>.<key>, hence we namespace our sub-keys with thecgh-prefix.Also note that both entries default to the empty string, and are assumed
to be the default if not present in the config. This means a user
without interest in the features that depend on the keys won't see them
in their config.
Change-Id: Ib6b188c23fc84460fad74460e05fd6ba7a02e8ca
Stack:
main(Note: Closed and merged PRs may not be reflected here and PR numbering is not stable.)