Skip to content

Commit a2ca9d1

Browse files
authored
Merge pull request #220 from epage/base
perf(cli): Split off highly divergent branches
2 parents ab139a1 + a99bcca commit a2ca9d1

File tree

6 files changed

+124
-9
lines changed

6 files changed

+124
-9
lines changed

docs/reference.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ Configuration is read from the following (in precedence order):
129129
| stack.protected-branch | \- | multivar of globs | Branch names that match these globs (`.gitignore` syntax) are considered protected branches |
130130
| stack.protect-commit-count | \- | integer | Protect commits that are on a branch with `count`+ commits |
131131
| stack.protect-commit-age | \- | time delta (e.g. 10days) | Protect commits that older than the specified time |
132+
| stack.auto-base-commit-count | \- | integer | Split off branches that are more than `count` commits away from the implied base |
132133
| stack.stack | --stack | "current", "dependents", "descendants", "all" | Which development branch-stacks to operate on |
133134
| stack.push-remote | \- | string | Development remote for pushing local branches |
134135
| stack.pull-remote | \- | string | Upstream remote for pulling protected branches |

src/bin/git-stack/args.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ impl Args {
8989
protected_branches: None,
9090
protect_commit_count: None,
9191
protect_commit_age: None,
92+
auto_base_commit_count: None,
9293
stack: self.stack,
9394
push_remote: None,
9495
pull_remote: None,

src/bin/git-stack/stack.rs

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,13 @@ impl State {
135135
(None, None, git_stack::config::Stack::All) => {
136136
let mut stack_branches = std::collections::BTreeMap::new();
137137
for (branch_id, branch) in branches.iter() {
138-
let base_branch =
139-
resolve_implicit_base(&repo, branch_id, &branches, &protected_branches);
138+
let base_branch = resolve_implicit_base(
139+
&repo,
140+
branch_id,
141+
&branches,
142+
&protected_branches,
143+
repo_config.auto_base_commit_count(),
144+
);
140145
stack_branches
141146
.entry(base_branch)
142147
.or_insert_with(git_stack::git::Branches::default)
@@ -163,6 +168,7 @@ impl State {
163168
head_commit.id,
164169
&branches,
165170
&protected_branches,
171+
repo_config.auto_base_commit_count(),
166172
);
167173
// HACK: Since `base` might have come back with a remote branch, treat it as an
168174
// "onto" to find the local version.
@@ -175,6 +181,7 @@ impl State {
175181
head_commit.id,
176182
&branches,
177183
&protected_branches,
184+
repo_config.auto_base_commit_count(),
178185
);
179186
let base = resolve_base_from_onto(&repo, &onto);
180187
(base, onto)
@@ -787,9 +794,45 @@ fn resolve_implicit_base(
787794
head_oid: git2::Oid,
788795
branches: &git_stack::git::Branches,
789796
protected_branches: &git_stack::git::Branches,
797+
auto_base_commit_count: Option<usize>,
790798
) -> AnnotatedOid {
791-
let branch = match git_stack::git::find_protected_base(repo, protected_branches, head_oid) {
799+
match git_stack::git::find_protected_base(repo, protected_branches, head_oid) {
792800
Some(branch) => {
801+
let merge_base_id = repo
802+
.merge_base(branch.id, head_oid)
803+
.expect("to be a base, there must be a merge base");
804+
if let Some(max_commit_count) = auto_base_commit_count {
805+
let ahead_count = repo
806+
.commit_count(merge_base_id, head_oid)
807+
.expect("merge_base should ensure a count exists ");
808+
let behind_count = repo
809+
.commit_count(merge_base_id, branch.id)
810+
.expect("merge_base should ensure a count exists ");
811+
if max_commit_count <= ahead_count + behind_count {
812+
let assumed_base_oid =
813+
git_stack::git::infer_base(repo, head_oid).unwrap_or(head_oid);
814+
log::warn!(
815+
"{} is {} ahead and {} behind {}, using {} as --base instead",
816+
branches
817+
.get(head_oid)
818+
.map(|b| b[0].to_string())
819+
.or_else(|| {
820+
repo.find_commit(head_oid)?
821+
.summary
822+
.to_str()
823+
.ok()
824+
.map(ToOwned::to_owned)
825+
})
826+
.unwrap_or_else(|| "target".to_owned()),
827+
ahead_count,
828+
behind_count,
829+
branch,
830+
assumed_base_oid
831+
);
832+
return AnnotatedOid::new(assumed_base_oid);
833+
}
834+
}
835+
793836
log::debug!(
794837
"Chose branch {} as the base for {}",
795838
branch,
@@ -808,11 +851,15 @@ fn resolve_implicit_base(
808851
AnnotatedOid::with_branch(branch.to_owned())
809852
}
810853
None => {
811-
log::warn!("Could not find protected branch for {}", head_oid);
812-
AnnotatedOid::new(head_oid)
854+
let assumed_base_oid = git_stack::git::infer_base(repo, head_oid).unwrap_or(head_oid);
855+
log::warn!(
856+
"Could not find protected branch for {}, assuming {}",
857+
head_oid,
858+
assumed_base_oid
859+
);
860+
AnnotatedOid::new(assumed_base_oid)
813861
}
814-
};
815-
branch
862+
}
816863
}
817864

818865
fn resolve_base_from_onto(repo: &git_stack::git::GitRepo, onto: &AnnotatedOid) -> AnnotatedOid {

src/config.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ pub struct RepoConfig {
55
pub protected_branches: Option<Vec<String>>,
66
pub protect_commit_count: Option<usize>,
77
pub protect_commit_age: Option<std::time::Duration>,
8+
pub auto_base_commit_count: Option<usize>,
89
pub stack: Option<Stack>,
910
pub push_remote: Option<String>,
1011
pub pull_remote: Option<String>,
@@ -20,6 +21,7 @@ pub struct RepoConfig {
2021
static PROTECTED_STACK_FIELD: &str = "stack.protected-branch";
2122
static PROTECT_COMMIT_COUNT: &str = "stack.protect-commit-count";
2223
static PROTECT_COMMIT_AGE: &str = "stack.protect-commit-age";
24+
static AUTO_BASE_COMMIT_COUNT: &str = "stack.auto-base-commit-count";
2325
static STACK_FIELD: &str = "stack.stack";
2426
static PUSH_REMOTE_FIELD: &str = "stack.push-remote";
2527
static PULL_REMOTE_FIELD: &str = "stack.pull-remote";
@@ -34,6 +36,7 @@ static DEFAULT_PROTECTED_BRANCHES: [&str; 4] = ["main", "master", "dev", "stable
3436
static DEFAULT_PROTECT_COMMIT_COUNT: usize = 50;
3537
static DEFAULT_PROTECT_COMMIT_AGE: std::time::Duration =
3638
std::time::Duration::from_secs(60 * 60 * 24 * 14);
39+
static DEFAULT_AUTO_BASE_COMMIT_COUNT: usize = 500;
3740
const DEFAULT_CAPACITY: usize = 30;
3841

3942
impl RepoConfig {
@@ -132,6 +135,10 @@ impl RepoConfig {
132135
{
133136
config.protect_commit_age = Some(value);
134137
}
138+
} else if key == AUTO_BASE_COMMIT_COUNT {
139+
if let Some(value) = value.as_ref().and_then(|v| FromStr::from_str(v).ok()) {
140+
config.auto_base_commit_count = Some(value);
141+
}
135142
} else if key == STACK_FIELD {
136143
if let Some(value) = value.as_ref().and_then(|v| FromStr::from_str(v).ok()) {
137144
config.stack = Some(value);
@@ -190,6 +197,7 @@ impl RepoConfig {
190197
let mut conf = Self::default();
191198
conf.protect_commit_count = Some(conf.protect_commit_count().unwrap_or(0));
192199
conf.protect_commit_age = Some(conf.protect_commit_age());
200+
conf.auto_base_commit_count = Some(conf.auto_base_commit_count().unwrap_or(0));
193201
conf.stack = Some(conf.stack());
194202
conf.push_remote = Some(conf.push_remote().to_owned());
195203
conf.pull_remote = Some(conf.pull_remote().to_owned());
@@ -240,6 +248,11 @@ impl RepoConfig {
240248
.ok()
241249
.and_then(|s| humantime::parse_duration(&s).ok());
242250

251+
let auto_base_commit_count = config
252+
.get_i64(AUTO_BASE_COMMIT_COUNT)
253+
.ok()
254+
.map(|i| i.max(0) as usize);
255+
243256
let push_remote = config
244257
.get_string(PUSH_REMOTE_FIELD)
245258
.ok()
@@ -279,6 +292,7 @@ impl RepoConfig {
279292
protected_branches,
280293
protect_commit_count,
281294
protect_commit_age,
295+
auto_base_commit_count,
282296
push_remote,
283297
pull_remote,
284298
stack,
@@ -320,6 +334,7 @@ impl RepoConfig {
320334
}
321335
self.protect_commit_count = other.protect_commit_count.or(self.protect_commit_count);
322336
self.protect_commit_age = other.protect_commit_age.or(self.protect_commit_age);
337+
self.auto_base_commit_count = other.auto_base_commit_count.or(self.auto_base_commit_count);
323338
self.push_remote = other.push_remote.or(self.push_remote);
324339
self.pull_remote = other.pull_remote.or(self.pull_remote);
325340
self.stack = other.stack.or(self.stack);
@@ -349,6 +364,13 @@ impl RepoConfig {
349364
.unwrap_or(DEFAULT_PROTECT_COMMIT_AGE)
350365
}
351366

367+
pub fn auto_base_commit_count(&self) -> Option<usize> {
368+
let auto_base_commit_count = self
369+
.auto_base_commit_count
370+
.unwrap_or(DEFAULT_AUTO_BASE_COMMIT_COUNT);
371+
(auto_base_commit_count != 0).then(|| auto_base_commit_count)
372+
}
373+
352374
pub fn push_remote(&self) -> &str {
353375
self.push_remote.as_deref().unwrap_or("origin")
354376
}
@@ -412,6 +434,12 @@ impl std::fmt::Display for RepoConfig {
412434
PROTECT_COMMIT_AGE.split_once('.').unwrap().1,
413435
humantime::format_duration(self.protect_commit_age())
414436
)?;
437+
writeln!(
438+
f,
439+
"\t{}={}",
440+
AUTO_BASE_COMMIT_COUNT.split_once('.').unwrap().1,
441+
self.auto_base_commit_count().unwrap_or(0)
442+
)?;
415443
writeln!(
416444
f,
417445
"\t{}={}",

src/git/branches.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,3 +279,26 @@ pub fn find_protected_base<'b>(
279279

280280
None
281281
}
282+
283+
pub fn infer_base(repo: &dyn crate::git::Repo, head_oid: git2::Oid) -> Option<git2::Oid> {
284+
let head_commit = repo.find_commit(head_oid)?;
285+
let head_committer = head_commit.committer.clone();
286+
287+
let mut next_oid = head_oid;
288+
loop {
289+
let next_commit = repo.find_commit(next_oid)?;
290+
if next_commit.committer != head_committer {
291+
return Some(next_oid);
292+
}
293+
let parent_ids = repo.parent_ids(next_oid).ok()?;
294+
match parent_ids.len() {
295+
1 => {
296+
next_oid = parent_ids[0];
297+
}
298+
_ => {
299+
// Assume merge-commits are topic branches being merged into the upstream
300+
return Some(next_oid);
301+
}
302+
}
303+
}
304+
}

src/git/repo.rs

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ pub struct GitRepo {
124124
commits: std::cell::RefCell<std::collections::HashMap<git2::Oid, std::rc::Rc<Commit>>>,
125125
interned_strings: std::cell::RefCell<std::collections::HashSet<std::rc::Rc<str>>>,
126126
bases: std::cell::RefCell<std::collections::HashMap<(git2::Oid, git2::Oid), Option<git2::Oid>>>,
127+
counts: std::cell::RefCell<std::collections::HashMap<(git2::Oid, git2::Oid), Option<usize>>>,
127128
}
128129

129130
impl GitRepo {
@@ -135,6 +136,7 @@ impl GitRepo {
135136
commits: Default::default(),
136137
interned_strings: Default::default(),
137138
bases: Default::default(),
139+
counts: Default::default(),
138140
}
139141
}
140142

@@ -194,11 +196,16 @@ impl GitRepo {
194196
return Some(one);
195197
}
196198

199+
let (smaller, larger) = if one < two { (one, two) } else { (two, one) };
197200
*self
198201
.bases
199202
.borrow_mut()
200-
.entry((one, two))
201-
.or_insert_with(|| self.repo.merge_base(one, two).ok())
203+
.entry((smaller, larger))
204+
.or_insert_with(|| self.merge_base_raw(smaller, larger))
205+
}
206+
207+
fn merge_base_raw(&self, one: git2::Oid, two: git2::Oid) -> Option<git2::Oid> {
208+
self.repo.merge_base(one, two).ok()
202209
}
203210

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

294+
*self
295+
.counts
296+
.borrow_mut()
297+
.entry((base_id, head_id))
298+
.or_insert_with(|| self.commit_count_raw(base_id, head_id))
299+
}
300+
301+
fn commit_count_raw(&self, base_id: git2::Oid, head_id: git2::Oid) -> Option<usize> {
287302
let merge_base_id = self.merge_base(base_id, head_id)?;
288303
if merge_base_id != base_id {
289304
return None;

0 commit comments

Comments
 (0)