use std::collections::HashSet;
use git2::{BranchType, Oid, Repository, StatusOptions};
use crate::error::{ChangesetError, Result};
use crate::stack::metadata::{self, StackMetadata};
use crate::stack::{gh_stack, graphite, StackModel};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ChangesetSource {
Committed { base: Oid, head: Oid },
Uncommitted,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Changeset {
pub name: String,
pub source: ChangesetSource,
pub title: Option<String>,
pub current: bool,
pub needs_restack: bool,
}
pub fn assemble_changesets(
repo: &Repository,
head_branch: &str,
model: StackModel,
) -> Result<Vec<Changeset>> {
match model {
StackModel::None => Ok(vec![]),
StackModel::Git => assemble_git(repo, head_branch),
StackModel::Graphite => {
assemble_from_metadata(repo, head_branch, &graphite::read_metadata(repo)?)
}
StackModel::GhStack => {
assemble_from_metadata(repo, head_branch, &gh_stack::read_metadata(repo)?)
}
}
}
fn assemble_from_metadata(
repo: &Repository,
head_branch: &str,
meta: &StackMetadata,
) -> Result<Vec<Changeset>> {
let trunks: HashSet<String> = meta.trunks.iter().cloned().collect();
if trunks.contains(head_branch) || !meta.parents.contains_key(head_branch) {
return assemble_git(repo, head_branch);
}
if repo.find_branch(head_branch, BranchType::Local).is_err() {
return Err(ChangesetError::UnresolvableBranch {
branch: head_branch.to_string(),
}
.into());
}
let ordered_names = metadata::changeset_walk(meta, head_branch);
let mut changesets: Vec<Changeset> = Vec::new();
let mut current_index: Option<usize> = None;
for name in ordered_names {
if !crate::resolve::branch_exists(repo, &name) {
continue;
}
let branch_ref = repo.find_branch(&name, BranchType::Local).map_err(|_| {
ChangesetError::UnresolvableBranch {
branch: name.clone(),
}
})?;
let head_oid =
branch_ref
.get()
.target()
.ok_or_else(|| ChangesetError::UnresolvableBranch {
branch: name.clone(),
})?;
let entry = &meta.parents[&name];
let (base_oid, needs_restack) = resolve_recorded_base(
repo,
meta,
&entry.parent,
entry.parent_revision.as_deref(),
head_oid,
&name,
)?;
let is_current = name == head_branch;
if is_current {
current_index = Some(changesets.len());
}
changesets.push(Changeset {
title: meta.pr_titles.get(&name).cloned(),
source: ChangesetSource::Committed {
base: base_oid,
head: head_oid,
},
current: is_current,
needs_restack,
name,
});
}
insert_uncommitted_layer(repo, head_branch, current_index, &mut changesets)?;
Ok(changesets)
}
fn resolve_recorded_base(
repo: &Repository,
meta: &StackMetadata,
parent: &str,
parent_revision: Option<&str>,
head: Oid,
branch: &str,
) -> Result<(Oid, bool)> {
match parent_revision {
Some(rev) => {
let oid = Oid::from_str(rev)
.ok()
.filter(|oid| repo.find_commit(*oid).is_ok())
.ok_or_else(|| ChangesetError::InvalidParentRevision {
branch: branch.to_string(),
revision: rev.to_string(),
})?;
let parent_live_tip = repo
.find_branch(parent, BranchType::Local)
.ok()
.and_then(|b| b.get().target());
let needs_restack = match parent_live_tip {
Some(tip) => oid != tip,
None => false,
};
Ok((oid, needs_restack))
}
None => {
let tip = resolve_live_ancestor_tip(repo, meta, parent).ok_or_else(|| {
ChangesetError::UnresolvableBranch {
branch: parent.to_string(),
}
})?;
let base = repo.merge_base(tip, head)?;
Ok((base, false))
}
}
}
fn resolve_live_ancestor_tip(repo: &Repository, meta: &StackMetadata, start: &str) -> Option<Oid> {
let mut walk = start.to_string();
let mut seen: HashSet<String> = HashSet::new();
seen.insert(walk.clone());
loop {
if let Some(tip) = repo
.find_branch(&walk, BranchType::Local)
.ok()
.and_then(|b| b.get().target())
{
return Some(tip);
}
if meta.trunks.iter().any(|t| t == &walk) {
return None;
}
match meta.parents.get(&walk) {
Some(entry) if seen.insert(entry.parent.clone()) => walk = entry.parent.clone(),
_ => return None, }
}
}
fn assemble_git(repo: &Repository, head_branch: &str) -> Result<Vec<Changeset>> {
let branch = repo.find_branch(head_branch, BranchType::Local)?;
let upstream = branch.upstream().map_err(|_| ChangesetError::NoUpstream {
branch: head_branch.to_string(),
})?;
let head_oid = branch
.get()
.target()
.ok_or_else(|| ChangesetError::NoUpstream {
branch: head_branch.to_string(),
})?;
let upstream_oid = upstream
.get()
.target()
.ok_or_else(|| ChangesetError::NoUpstream {
branch: head_branch.to_string(),
})?;
let mut revwalk = repo.revwalk()?;
revwalk.push(head_oid)?;
revwalk.hide(upstream_oid)?;
revwalk.set_sorting(git2::Sort::TOPOLOGICAL | git2::Sort::REVERSE)?;
revwalk.simplify_first_parent()?;
let mut changesets: Vec<Changeset> = Vec::new();
for oid in revwalk {
let oid = oid?;
let commit = repo.find_commit(oid)?;
let base = commit.parent_id(0).unwrap_or(oid);
changesets.push(Changeset {
name: short_id(oid),
source: ChangesetSource::Committed { base, head: oid },
title: commit.summary()?.map(str::to_string),
current: false,
needs_restack: false,
});
}
let current_index = if changesets.is_empty() {
None
} else {
let last = changesets.len() - 1;
changesets[last].current = true;
Some(last)
};
insert_uncommitted_layer(repo, head_branch, current_index, &mut changesets)?;
Ok(changesets)
}
fn short_id(oid: Oid) -> String {
oid.to_string()[..8].to_string()
}
fn insert_uncommitted_layer(
repo: &Repository,
current_branch: &str,
current_index: Option<usize>,
changesets: &mut Vec<Changeset>,
) -> Result<()> {
let mut opts = StatusOptions::new();
opts.include_untracked(true);
opts.include_ignored(false);
let statuses = repo.statuses(Some(&mut opts))?;
if statuses.is_empty() {
return Ok(());
}
if let Some(idx) = current_index {
changesets[idx].current = false;
}
let insert_at = current_index.map_or(changesets.len(), |i| i + 1);
changesets.insert(
insert_at,
Changeset {
name: current_branch.to_string(),
source: ChangesetSource::Uncommitted,
title: None,
current: true,
needs_restack: false,
},
);
Ok(())
}