Skip to content

perf(cli): Split off highly divergent branches #220

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Mar 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ Configuration is read from the following (in precedence order):
| stack.protected-branch | \- | multivar of globs | Branch names that match these globs (`.gitignore` syntax) are considered protected branches |
| stack.protect-commit-count | \- | integer | Protect commits that are on a branch with `count`+ commits |
| stack.protect-commit-age | \- | time delta (e.g. 10days) | Protect commits that older than the specified time |
| stack.auto-base-commit-count | \- | integer | Split off branches that are more than `count` commits away from the implied base |
| stack.stack | --stack | "current", "dependents", "descendants", "all" | Which development branch-stacks to operate on |
| stack.push-remote | \- | string | Development remote for pushing local branches |
| stack.pull-remote | \- | string | Upstream remote for pulling protected branches |
Expand Down
1 change: 1 addition & 0 deletions src/bin/git-stack/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ impl Args {
protected_branches: None,
protect_commit_count: None,
protect_commit_age: None,
auto_base_commit_count: None,
stack: self.stack,
push_remote: None,
pull_remote: None,
Expand Down
61 changes: 54 additions & 7 deletions src/bin/git-stack/stack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,13 @@ impl State {
(None, None, git_stack::config::Stack::All) => {
let mut stack_branches = std::collections::BTreeMap::new();
for (branch_id, branch) in branches.iter() {
let base_branch =
resolve_implicit_base(&repo, branch_id, &branches, &protected_branches);
let base_branch = resolve_implicit_base(
&repo,
branch_id,
&branches,
&protected_branches,
repo_config.auto_base_commit_count(),
);
stack_branches
.entry(base_branch)
.or_insert_with(git_stack::git::Branches::default)
Expand All @@ -163,6 +168,7 @@ impl State {
head_commit.id,
&branches,
&protected_branches,
repo_config.auto_base_commit_count(),
);
// HACK: Since `base` might have come back with a remote branch, treat it as an
// "onto" to find the local version.
Expand All @@ -175,6 +181,7 @@ impl State {
head_commit.id,
&branches,
&protected_branches,
repo_config.auto_base_commit_count(),
);
let base = resolve_base_from_onto(&repo, &onto);
(base, onto)
Expand Down Expand Up @@ -787,9 +794,45 @@ fn resolve_implicit_base(
head_oid: git2::Oid,
branches: &git_stack::git::Branches,
protected_branches: &git_stack::git::Branches,
auto_base_commit_count: Option<usize>,
) -> AnnotatedOid {
let branch = match git_stack::git::find_protected_base(repo, protected_branches, head_oid) {
match git_stack::git::find_protected_base(repo, protected_branches, head_oid) {
Some(branch) => {
let merge_base_id = repo
.merge_base(branch.id, head_oid)
.expect("to be a base, there must be a merge base");
if let Some(max_commit_count) = auto_base_commit_count {
let ahead_count = repo
.commit_count(merge_base_id, head_oid)
.expect("merge_base should ensure a count exists ");
let behind_count = repo
.commit_count(merge_base_id, branch.id)
.expect("merge_base should ensure a count exists ");
if max_commit_count <= ahead_count + behind_count {
let assumed_base_oid =
git_stack::git::infer_base(repo, head_oid).unwrap_or(head_oid);
log::warn!(
"{} is {} ahead and {} behind {}, using {} as --base instead",
branches
.get(head_oid)
.map(|b| b[0].to_string())
.or_else(|| {
repo.find_commit(head_oid)?
.summary
.to_str()
.ok()
.map(ToOwned::to_owned)
})
.unwrap_or_else(|| "target".to_owned()),
ahead_count,
behind_count,
branch,
assumed_base_oid
);
return AnnotatedOid::new(assumed_base_oid);
}
}

log::debug!(
"Chose branch {} as the base for {}",
branch,
Expand All @@ -808,11 +851,15 @@ fn resolve_implicit_base(
AnnotatedOid::with_branch(branch.to_owned())
}
None => {
log::warn!("Could not find protected branch for {}", head_oid);
AnnotatedOid::new(head_oid)
let assumed_base_oid = git_stack::git::infer_base(repo, head_oid).unwrap_or(head_oid);
log::warn!(
"Could not find protected branch for {}, assuming {}",
head_oid,
assumed_base_oid
);
AnnotatedOid::new(assumed_base_oid)
}
};
branch
}
}

fn resolve_base_from_onto(repo: &git_stack::git::GitRepo, onto: &AnnotatedOid) -> AnnotatedOid {
Expand Down
28 changes: 28 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ pub struct RepoConfig {
pub protected_branches: Option<Vec<String>>,
pub protect_commit_count: Option<usize>,
pub protect_commit_age: Option<std::time::Duration>,
pub auto_base_commit_count: Option<usize>,
pub stack: Option<Stack>,
pub push_remote: Option<String>,
pub pull_remote: Option<String>,
Expand All @@ -20,6 +21,7 @@ pub struct RepoConfig {
static PROTECTED_STACK_FIELD: &str = "stack.protected-branch";
static PROTECT_COMMIT_COUNT: &str = "stack.protect-commit-count";
static PROTECT_COMMIT_AGE: &str = "stack.protect-commit-age";
static AUTO_BASE_COMMIT_COUNT: &str = "stack.auto-base-commit-count";
static STACK_FIELD: &str = "stack.stack";
static PUSH_REMOTE_FIELD: &str = "stack.push-remote";
static PULL_REMOTE_FIELD: &str = "stack.pull-remote";
Expand All @@ -34,6 +36,7 @@ static DEFAULT_PROTECTED_BRANCHES: [&str; 4] = ["main", "master", "dev", "stable
static DEFAULT_PROTECT_COMMIT_COUNT: usize = 50;
static DEFAULT_PROTECT_COMMIT_AGE: std::time::Duration =
std::time::Duration::from_secs(60 * 60 * 24 * 14);
static DEFAULT_AUTO_BASE_COMMIT_COUNT: usize = 500;
const DEFAULT_CAPACITY: usize = 30;

impl RepoConfig {
Expand Down Expand Up @@ -132,6 +135,10 @@ impl RepoConfig {
{
config.protect_commit_age = Some(value);
}
} else if key == AUTO_BASE_COMMIT_COUNT {
if let Some(value) = value.as_ref().and_then(|v| FromStr::from_str(v).ok()) {
config.auto_base_commit_count = Some(value);
}
} else if key == STACK_FIELD {
if let Some(value) = value.as_ref().and_then(|v| FromStr::from_str(v).ok()) {
config.stack = Some(value);
Expand Down Expand Up @@ -190,6 +197,7 @@ impl RepoConfig {
let mut conf = Self::default();
conf.protect_commit_count = Some(conf.protect_commit_count().unwrap_or(0));
conf.protect_commit_age = Some(conf.protect_commit_age());
conf.auto_base_commit_count = Some(conf.auto_base_commit_count().unwrap_or(0));
conf.stack = Some(conf.stack());
conf.push_remote = Some(conf.push_remote().to_owned());
conf.pull_remote = Some(conf.pull_remote().to_owned());
Expand Down Expand Up @@ -240,6 +248,11 @@ impl RepoConfig {
.ok()
.and_then(|s| humantime::parse_duration(&s).ok());

let auto_base_commit_count = config
.get_i64(AUTO_BASE_COMMIT_COUNT)
.ok()
.map(|i| i.max(0) as usize);

let push_remote = config
.get_string(PUSH_REMOTE_FIELD)
.ok()
Expand Down Expand Up @@ -279,6 +292,7 @@ impl RepoConfig {
protected_branches,
protect_commit_count,
protect_commit_age,
auto_base_commit_count,
push_remote,
pull_remote,
stack,
Expand Down Expand Up @@ -320,6 +334,7 @@ impl RepoConfig {
}
self.protect_commit_count = other.protect_commit_count.or(self.protect_commit_count);
self.protect_commit_age = other.protect_commit_age.or(self.protect_commit_age);
self.auto_base_commit_count = other.auto_base_commit_count.or(self.auto_base_commit_count);
self.push_remote = other.push_remote.or(self.push_remote);
self.pull_remote = other.pull_remote.or(self.pull_remote);
self.stack = other.stack.or(self.stack);
Expand Down Expand Up @@ -349,6 +364,13 @@ impl RepoConfig {
.unwrap_or(DEFAULT_PROTECT_COMMIT_AGE)
}

pub fn auto_base_commit_count(&self) -> Option<usize> {
let auto_base_commit_count = self
.auto_base_commit_count
.unwrap_or(DEFAULT_AUTO_BASE_COMMIT_COUNT);
(auto_base_commit_count != 0).then(|| auto_base_commit_count)
}

pub fn push_remote(&self) -> &str {
self.push_remote.as_deref().unwrap_or("origin")
}
Expand Down Expand Up @@ -412,6 +434,12 @@ impl std::fmt::Display for RepoConfig {
PROTECT_COMMIT_AGE.split_once('.').unwrap().1,
humantime::format_duration(self.protect_commit_age())
)?;
writeln!(
f,
"\t{}={}",
AUTO_BASE_COMMIT_COUNT.split_once('.').unwrap().1,
self.auto_base_commit_count().unwrap_or(0)
)?;
writeln!(
f,
"\t{}={}",
Expand Down
23 changes: 23 additions & 0 deletions src/git/branches.rs
Original file line number Diff line number Diff line change
Expand Up @@ -279,3 +279,26 @@ pub fn find_protected_base<'b>(

None
}

pub fn infer_base(repo: &dyn crate::git::Repo, head_oid: git2::Oid) -> Option<git2::Oid> {
let head_commit = repo.find_commit(head_oid)?;
let head_committer = head_commit.committer.clone();

let mut next_oid = head_oid;
loop {
let next_commit = repo.find_commit(next_oid)?;
if next_commit.committer != head_committer {
return Some(next_oid);
}
let parent_ids = repo.parent_ids(next_oid).ok()?;
match parent_ids.len() {
1 => {
next_oid = parent_ids[0];
}
_ => {
// Assume merge-commits are topic branches being merged into the upstream
return Some(next_oid);
}
}
}
}
19 changes: 17 additions & 2 deletions src/git/repo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ pub struct GitRepo {
commits: std::cell::RefCell<std::collections::HashMap<git2::Oid, std::rc::Rc<Commit>>>,
interned_strings: std::cell::RefCell<std::collections::HashSet<std::rc::Rc<str>>>,
bases: std::cell::RefCell<std::collections::HashMap<(git2::Oid, git2::Oid), Option<git2::Oid>>>,
counts: std::cell::RefCell<std::collections::HashMap<(git2::Oid, git2::Oid), Option<usize>>>,
}

impl GitRepo {
Expand All @@ -135,6 +136,7 @@ impl GitRepo {
commits: Default::default(),
interned_strings: Default::default(),
bases: Default::default(),
counts: Default::default(),
}
}

Expand Down Expand Up @@ -194,11 +196,16 @@ impl GitRepo {
return Some(one);
}

let (smaller, larger) = if one < two { (one, two) } else { (two, one) };
*self
.bases
.borrow_mut()
.entry((one, two))
.or_insert_with(|| self.repo.merge_base(one, two).ok())
.entry((smaller, larger))
.or_insert_with(|| self.merge_base_raw(smaller, larger))
}

fn merge_base_raw(&self, one: git2::Oid, two: git2::Oid) -> Option<git2::Oid> {
self.repo.merge_base(one, two).ok()
}

pub fn find_commit(&self, id: git2::Oid) -> Option<std::rc::Rc<Commit>> {
Expand Down Expand Up @@ -284,6 +291,14 @@ impl GitRepo {
return Some(0);
}

*self
.counts
.borrow_mut()
.entry((base_id, head_id))
.or_insert_with(|| self.commit_count_raw(base_id, head_id))
}

fn commit_count_raw(&self, base_id: git2::Oid, head_id: git2::Oid) -> Option<usize> {
let merge_base_id = self.merge_base(base_id, head_id)?;
if merge_base_id != base_id {
return None;
Expand Down