use anyhow::{anyhow, bail};
use dynfmt::{Format, SimpleCurlyFormat};
use log::{info, warn};
use serde::{Deserialize, Serialize};
use std::{
collections::{HashMap, HashSet},
fs::File,
io::{BufRead, BufReader, Read},
path::{Path, PathBuf},
};
use thiserror::Error as ThisError;
use crate::{
a_ok_or, atry,
bootstrap::BootstrapConfiguration,
config::RepoConfiguration,
errors::{Error, Result},
graph::ProjectGraph,
project::{DepRequirement, Project},
version::Version,
};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct CommitId(git2::Oid);
impl std::fmt::Display for CommitId {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
self.0.fmt(f)
}
}
#[derive(Debug, ThisError)]
#[error("cannot operate on a bare repository")]
pub struct BareRepositoryError;
#[derive(Debug, ThisError)]
pub struct DirtyRepositoryError(pub RepoPathBuf);
#[derive(Debug, ThisError)]
#[error("commit reference `{0}` is invalid or refers to a nonexistent commit")]
pub struct InvalidHistoryReferenceError(pub String);
impl std::fmt::Display for DirtyRepositoryError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
"the file backing repository is dirty: file {} has been modified",
self.0.escaped()
)
}
}
pub struct Repository {
repo: git2::Repository,
upstream_name: String,
upstream_rc_name: String,
upstream_release_name: String,
release_tag_name_format: String,
bootstrap_info: BootstrapConfiguration,
}
impl Repository {
pub fn open_from_env() -> Result<Repository> {
let repo = git2::Repository::open_from_env()?;
if repo.is_bare() {
return Err(BareRepositoryError.into());
}
let upstream_name = "origin".to_owned();
let upstream_rc_name = "rc".to_owned();
let upstream_release_name = "release".to_owned();
let release_tag_name_format = "{project_slug}@{version}".to_owned();
Ok(Repository {
repo,
upstream_name,
upstream_rc_name,
upstream_release_name,
release_tag_name_format,
bootstrap_info: BootstrapConfiguration::default(),
})
}
pub fn bootstrap_upstream(&mut self, name: Option<&str>) -> Result<String> {
let upstream_url = if let Some(name) = name {
let remote = atry!(
self.repo.find_remote(name);
["cannot look up the Git remote named `{}`", name]
);
remote
.url()
.ok_or_else(|| {
anyhow!(
"the URL of Git remote `{}` cannot be interpreted as UTF8",
name
)
})?
.to_owned()
} else {
let mut info = None;
let mut n_remotes = 0;
for remote_name in self.repo.remotes()?.into_iter().flatten() {
n_remotes += 1;
match self.repo.find_remote(remote_name) {
Err(e) => {
warn!("error querying Git remote `{}`: {}", remote_name, e);
}
Ok(remote) => {
if let Some(remote_url) = remote.url() {
if info.is_none() || remote_name == "origin" {
info = Some((remote_name.to_owned(), remote_url.to_owned()));
}
}
}
}
}
let (name, url) = info.ok_or_else(|| anyhow!("no usable remotes in the Git repo"))?;
if n_remotes > 1 && name != "origin" {
bail!("no way to choose among multiple Git remotes");
}
info!("using Git remote `{}` as the upstream", name);
url
};
Ok(upstream_url)
}
pub fn apply_config(&mut self, cfg: RepoConfiguration) -> Result<()> {
let mut first_upstream_name = None;
let mut n_remotes = 0;
let mut url_matched = None;
let mut saw_origin = false;
for remote_name in &self.repo.remotes()? {
if let Some(remote_name) = remote_name {
n_remotes += 1;
if first_upstream_name.is_none() {
first_upstream_name = Some(remote_name.to_owned());
}
if remote_name == "origin" {
saw_origin = true;
}
match self.repo.find_remote(remote_name) {
Err(e) => {
warn!("error querying Git remote `{}`: {}", remote_name, e);
}
Ok(remote) => {
if let Some(remote_url) = remote.url() {
for url in &cfg.upstream_urls {
if remote_url == url {
url_matched = Some(remote_name.to_owned());
break;
}
}
}
}
}
}
if url_matched.is_some() {
break;
}
}
self.upstream_name = if let Some(n) = url_matched {
n
} else if n_remotes == 1 {
first_upstream_name.unwrap()
} else if saw_origin {
"origin".to_owned()
} else {
bail!("cannot identify the upstream Git remote");
};
if let Some(n) = cfg.rc_name {
self.upstream_rc_name = n;
}
if let Some(n) = cfg.release_name {
self.upstream_release_name = n;
}
if let Some(n) = cfg.release_tag_name_format {
self.release_tag_name_format = n;
}
let mut bs_path = self.resolve_config_dir();
bs_path.push("bootstrap.toml");
let maybe_file = match File::open(&bs_path) {
Ok(f) => Some(f),
Err(e) => {
if e.kind() == std::io::ErrorKind::NotFound {
None
} else {
return Err(Error::new(e).context(format!(
"failed to open config file `{}`",
bs_path.display()
)));
}
}
};
if let Some(mut f) = maybe_file {
let mut text = String::new();
atry!(
f.read_to_string(&mut text);
["failed to read bootstrap file `{}`", bs_path.display()]
);
self.bootstrap_info = atry!(
toml::from_str(&text);
["could not parse bootstrap file `{}` as TOML", bs_path.display()]
);
}
Ok(())
}
pub fn upstream_rc_name(&self) -> &str {
&self.upstream_rc_name
}
pub fn upstream_release_name(&self) -> &str {
&self.upstream_release_name
}
pub fn upstream_url(&self) -> Result<String> {
let upstream = self.repo.find_remote(&self.upstream_name)?;
Ok(upstream
.url()
.ok_or_else(|| {
anyhow!(
"URL of upstream remote {} not parseable as Unicode",
self.upstream_name
)
})?
.to_owned())
}
pub fn current_branch_name(&self) -> Result<Option<String>> {
let head_ref = self.repo.head()?;
Ok(if !head_ref.is_branch() {
None
} else {
Some(
head_ref
.shorthand()
.ok_or_else(|| anyhow!("current branch name not Unicode"))?
.to_owned(),
)
})
}
pub fn parse_history_ref<T: AsRef<str>>(&self, text: T) -> Result<ParsedHistoryRef> {
let text = text.as_ref();
if let Ok(id) = text.parse() {
Ok(ParsedHistoryRef::Id(CommitId(id)))
} else if let Some(tctext) = text.strip_prefix("thiscommit:") {
Ok(ParsedHistoryRef::ThisCommit {
salt: tctext.to_owned(),
})
} else if let Some(manual_text) = text.strip_prefix("manual:") {
Ok(ParsedHistoryRef::Manual(manual_text.to_owned()))
} else {
Err(InvalidHistoryReferenceError(text.to_owned()).into())
}
}
pub fn resolve_history_ref(
&self,
href: &ParsedHistoryRef,
ref_source_path: &RepoPath,
) -> Result<DepRequirement> {
let cid = match href {
ParsedHistoryRef::Id(id) => *id,
ParsedHistoryRef::ThisCommit { ref salt } => lookup_this(self, salt, ref_source_path)?,
ParsedHistoryRef::Manual(t) => return Ok(DepRequirement::Manual(t.clone())),
};
self.repo.find_commit(cid.0)?;
return Ok(DepRequirement::Commit(cid));
fn lookup_this(
repo: &Repository,
salt: &str,
ref_source_path: &RepoPath,
) -> Result<CommitId> {
let file = File::open(repo.resolve_workdir(ref_source_path))?;
let reader = BufReader::new(file);
let mut line_no = 1; let mut found_it = false;
for maybe_line in reader.lines() {
let line = maybe_line?;
if line.contains(salt) {
found_it = true;
break;
}
line_no += 1;
}
if !found_it {
return Err(anyhow!(
"commit-ref key `{}` not found in contents of file {}",
salt,
ref_source_path.escaped(),
));
}
let blame = repo.repo.blame_file(ref_source_path.as_path(), None)?;
let hunk = blame.get_line(line_no).ok_or_else(|| {
anyhow!(
"commit-ref key `{}` found in non-existent line {} of file {}??",
salt,
line_no,
ref_source_path.escaped()
)
})?;
Ok(CommitId(hunk.final_commit_id()))
}
}
pub fn resolve_workdir(&self, p: &RepoPath) -> PathBuf {
let mut fullpath = self.repo.workdir().unwrap().to_owned();
fullpath.push(p.as_path());
fullpath
}
pub fn resolve_config_dir(&self) -> PathBuf {
self.resolve_workdir(RepoPath::new(b".config/cranko"))
}
pub fn convert_path<P: AsRef<Path>>(&self, p: P) -> Result<RepoPathBuf> {
let c_root = self.repo.workdir().unwrap().canonicalize()?;
let c_p = p.as_ref().canonicalize()?;
let rel = c_p.strip_prefix(&c_root).map_err(|_| {
anyhow!(
"path `{}` lies outside of the working directory",
c_p.display()
)
})?;
RepoPathBuf::from_path(rel)
}
pub fn scan_paths<F>(&self, mut f: F) -> Result<()>
where
F: FnMut(&RepoPath) -> Result<()>,
{
let index = self.repo.index()?;
for entry in index.iter() {
let p = RepoPath::new(&entry.path);
atry!(
f(p);
["encountered a problem while scanning repository entry `{}`", p.escaped()]
);
}
Ok(())
}
pub fn check_if_dirty(&self, ok_matchers: &[PathMatcher]) -> Result<Option<RepoPathBuf>> {
let mut opts = git2::StatusOptions::new();
for entry in self.repo.statuses(Some(&mut opts))?.iter() {
if entry.status() != git2::Status::CURRENT {
let repo_path = RepoPath::new(entry.path_bytes());
let mut is_ok = false;
for matcher in ok_matchers {
if matcher.repo_path_matches(repo_path) {
is_ok = true;
break;
}
}
if !is_ok {
return Ok(Some(repo_path.to_owned()));
}
}
}
Ok(None)
}
pub fn get_file_at_commit(&self, cid: &CommitId, path: &RepoPath) -> Result<Option<Vec<u8>>> {
let commit = self.repo.find_commit(cid.0)?;
let tree = commit.tree()?;
let entry = match tree.get_path(path.as_path()) {
Ok(e) => e,
Err(e) => {
return if e.code() == git2::ErrorCode::NotFound {
Ok(None)
} else {
Err(e.into())
};
}
};
let object = entry.to_object(&self.repo)?;
let blob = object.as_blob().ok_or_else(|| {
anyhow!(
"path `{}` should correspond to a Git blob but does not",
path.escaped(),
)
})?;
Ok(Some(blob.content().to_owned()))
}
fn get_bootstrap_release_info(&self) -> ReleaseCommitInfo {
let mut rel_info = ReleaseCommitInfo::default();
for bs_info in &self.bootstrap_info.project[..] {
rel_info.projects.push(ReleasedProjectInfo {
qnames: bs_info.qnames.clone(),
version: bs_info.version.clone(),
age: 999,
})
}
rel_info
}
pub fn get_latest_release_info(&self) -> Result<ReleaseCommitInfo> {
Ok(if let Some(c) = self.try_get_release_commit()? {
self.parse_release_info_from_commit(&c)?
} else {
self.get_bootstrap_release_info()
})
}
fn get_signature(&self) -> Result<git2::Signature> {
Ok(git2::Signature::now("cranko", "cranko@devnull")?)
}
fn try_get_release_commit(&self) -> Result<Option<git2::Commit>> {
let release_ref = match self.repo.resolve_reference_from_short_name(&format!(
"{}/{}",
self.upstream_name, self.upstream_release_name
)) {
Ok(r) => r,
Err(e) => {
return if e.code() == git2::ErrorCode::NotFound {
Ok(None)
} else {
Err(e.into())
};
}
};
Ok(Some(release_ref.peel_to_commit()?))
}
fn try_get_rc_commit(&self) -> Result<Option<git2::Commit>> {
let rc_ref = match self.repo.resolve_reference_from_short_name(&format!(
"{}/{}",
self.upstream_name, self.upstream_rc_name
)) {
Ok(r) => r,
Err(e) => {
return if e.code() == git2::ErrorCode::NotFound {
Ok(None)
} else {
Err(e.into())
};
}
};
Ok(Some(rc_ref.peel_to_commit()?))
}
pub fn make_release_commit(&mut self, graph: &ProjectGraph, rci: &RcCommitInfo) -> Result<()> {
let rel_info = self.get_latest_release_info()?;
let head_ref = self.repo.head()?;
let head_commit = head_ref.peel_to_commit()?;
let sig = self.get_signature()?;
let local_ref_name = format!("refs/heads/{}", self.upstream_release_name);
let mut info = SerializedReleaseCommitInfo::default();
for ident in graph.toposorted() {
let proj = graph.lookup(ident);
let (age, expose) = if let Some(ri) = rel_info.lookup_project(proj) {
if proj.version.to_string() == ri.version {
(ri.age + 1, true)
} else {
(0, true)
}
} else {
(0, rci.lookup_project(proj).is_some())
};
if expose {
info.projects.push(ReleasedProjectInfo {
qnames: proj.qualified_names().clone(),
version: proj.version.to_string(),
age,
});
}
}
let message = format!(
"Release commit created with Cranko.
+++ cranko-release-info-v1
{}
+++
",
toml::to_string(&info)?
);
let tree_oid = {
let mut index = self.repo.index()?;
index.write_tree()?
};
let tree = self.repo.find_tree(tree_oid)?;
let commit = |parents: &[&git2::Commit]| -> Result<git2::Oid> {
self.repo
.reference(&local_ref_name, parents[0].id(), true, "update release")?;
Ok(self.repo.commit(
Some(&local_ref_name), &sig, &sig, &message,
&tree,
parents,
)?)
};
let commit_id = if let Some(prev_cid) = rel_info.commit {
let prev_release_commit = self.repo.find_commit(prev_cid.0)?;
commit(&[&prev_release_commit, &head_commit])?
} else {
commit(&[&head_commit])?
};
info!("switching HEAD to `{}`", local_ref_name);
self.repo.set_head(&local_ref_name)?;
self.repo.reset(
self.repo.find_commit(commit_id)?.as_object(),
git2::ResetType::Mixed,
None,
)?;
Ok(())
}
pub fn parse_release_info_from_head(&self) -> Result<ReleaseCommitInfo> {
let head_ref = self.repo.head()?;
let head_commit = head_ref.peel_to_commit()?;
self.parse_release_info_from_commit(&head_commit)
}
fn parse_release_info_from_commit(&self, commit: &git2::Commit) -> Result<ReleaseCommitInfo> {
let msg = commit
.message()
.ok_or_else(|| anyhow!("cannot parse release commit message: it is not Unicode"))?;
let mut data = String::new();
let mut in_body = false;
for line in msg.lines() {
if in_body {
if line == "+++" {
in_body = false;
break;
} else {
data.push_str(line);
data.push('\n');
}
} else if line.starts_with("+++ cranko-release-info-v1") {
in_body = true;
}
}
if in_body {
println!("unterminated release info body; trying to proceed anyway");
}
if data.is_empty() {
bail!("empty cranko-release-info body in release commit message");
}
let mut srci: SerializedReleaseCommitInfo = toml::from_str(&data)?;
let mut bsri = self.get_bootstrap_release_info();
let seen_projects: HashSet<_> = srci.projects.iter().map(|p| p.qnames.clone()).collect();
for bs_proj in bsri.projects.drain(..) {
if !seen_projects.contains(&bs_proj.qnames) {
srci.projects.push(bs_proj);
}
}
Ok(ReleaseCommitInfo {
commit: Some(CommitId(commit.id())),
projects: srci.projects,
})
}
pub fn analyze_histories(&self, projects: &[Project]) -> Result<Vec<RepoHistory>> {
let mut histories = vec![
RepoHistory {
commits: Vec::new(),
release_commit: None,
};
projects.len()
];
let latest_release_commit = self.try_get_release_commit()?;
if let Some(mut commit) = latest_release_commit {
let mut n_found = 0;
loop {
let rel_info = self.parse_release_info_from_commit(&commit)?;
for (i, proj) in projects.iter().enumerate() {
if histories[i].release_commit.is_none()
&& rel_info.lookup_if_released(proj).is_some()
{
histories[i].release_commit = Some(CommitId(commit.id()));
n_found += 1;
}
}
if n_found == projects.len() {
break; }
if commit.parent_count() == 1 {
break;
}
commit = commit.parent(0)?;
}
}
let mut commit_data = lru::LruCache::new(512);
let mut trees = lru::LruCache::new(3);
let mut dopts = git2::DiffOptions::new();
dopts.include_typechange(true);
for proj_idx in 0..projects.len() {
let mut walk = self.repo.revwalk()?;
walk.push_head()?;
if let Some(release_commit_id) = histories[proj_idx].release_commit {
walk.hide(release_commit_id.0)?;
}
for maybe_oid in walk {
let oid = maybe_oid?;
if !commit_data.contains(&oid) {
let commit = self.repo.find_commit(oid)?;
let ctid = commit.tree_id();
let cur_tree = match trees.pop(&ctid) {
Some(t) => t,
None => self.repo.find_tree(ctid)?,
};
let (maybe_ptid, maybe_parent_tree) = if commit.parent_count() == 0 {
(None, None) } else {
let parent = commit.parent(0)?;
let ptid = parent.tree_id();
let parent_tree = match trees.pop(&ptid) {
Some(t) => t,
None => self.repo.find_tree(ptid)?,
};
(Some(ptid), Some(parent_tree))
};
let diff = self.repo.diff_tree_to_tree(
maybe_parent_tree.as_ref(),
Some(&cur_tree),
Some(&mut dopts),
)?;
trees.put(ctid, cur_tree);
if let (Some(ptid), Some(pt)) = (maybe_ptid, maybe_parent_tree) {
trees.put(ptid, pt);
}
let mut hit_buf = vec![false; projects.len()];
if commit.parent_count() < 2 {
for delta in diff.deltas() {
for file in &[delta.old_file(), delta.new_file()] {
if let Some(path_bytes) = file.path_bytes() {
let path = RepoPath::new(path_bytes);
for (idx, proj) in projects.iter().enumerate() {
if proj.repo_paths.repo_path_matches(path) {
hit_buf[idx] = true;
}
}
}
}
}
}
commit_data.put(oid, hit_buf);
}
let hits = commit_data.get(&oid).unwrap();
if hits[proj_idx] {
histories[proj_idx].commits.push(CommitId(oid));
}
}
}
Ok(histories)
}
pub fn get_commit_summary(&self, cid: CommitId) -> Result<String> {
let commit = self.repo.find_commit(cid.0)?;
if let Some(s) = commit.summary() {
Ok(s.to_owned())
} else {
Ok(format!("[commit {0}: non-Unicode summary]", cid.0))
}
}
pub fn scan_rc_info(
&self,
proj: &Project,
changes: &mut ChangeList,
dirty_allowed: bool,
) -> Result<Option<RcProjectInfo>> {
let mut saw_changelog = false;
let changelog_matcher = proj.changelog.create_path_matcher(proj)?;
let mut opts = git2::StatusOptions::new();
opts.include_untracked(true);
opts.include_ignored(true);
for entry in self.repo.statuses(Some(&mut opts))?.iter() {
let path = RepoPath::new(entry.path_bytes());
if !proj.repo_paths.repo_path_matches(path) {
continue;
}
let status = entry.status();
if changelog_matcher.repo_path_matches(path) {
if status.is_conflicted() {
return Err(DirtyRepositoryError(path.to_owned()).into());
} else if status.is_index_new()
|| status.is_index_modified()
|| status.is_wt_new()
|| status.is_wt_modified()
{
changes.add_path(path);
saw_changelog = true;
} } else if status.is_ignored() || status.is_wt_new() || status == git2::Status::CURRENT {
} else if !dirty_allowed {
return Err(DirtyRepositoryError(path.to_owned()).into());
}
}
if saw_changelog {
Ok(Some(proj.changelog.scan_rc_info(proj, self)?))
} else {
Ok(None)
}
}
pub fn make_rc_commit(
&mut self,
rcinfo: Vec<RcProjectInfo>,
changes: &ChangeList,
) -> Result<()> {
let maybe_rc_commit = self.try_get_rc_commit()?;
let head_ref = self.repo.head()?;
let head_commit = head_ref.peel_to_commit()?;
let sig = self.get_signature()?;
let local_ref_name = format!("refs/heads/{}", self.upstream_rc_name);
let info = SerializedRcCommitInfo { projects: rcinfo };
let message = format!(
"Release request commit created with Cranko.
+++ cranko-rc-info-v1
{}
+++
",
toml::to_string(&info)?
);
let tree_oid = {
let mut index = self.repo.index()?;
for p in &changes.paths {
index.add_path(p.as_path())?;
}
index.write_tree()?
};
let tree = self.repo.find_tree(tree_oid)?;
let commit = |parents: &[&git2::Commit]| -> Result<git2::Oid> {
self.repo
.reference(&local_ref_name, parents[0].id(), true, "update rc")?;
Ok(self.repo.commit(
Some(&local_ref_name), &sig, &sig, &message,
&tree,
parents,
)?)
};
if let Some(release_commit) = maybe_rc_commit {
commit(&[&release_commit, &head_commit])?;
} else {
commit(&[&head_commit])?;
};
Ok(())
}
pub fn parse_rc_info_from_head(&self) -> Result<RcCommitInfo> {
let head_ref = self.repo.head()?;
let head_commit = head_ref.peel_to_commit()?;
let msg = head_commit
.message()
.ok_or_else(|| anyhow!("cannot parse rc commit message: it is not Unicode"))?;
let mut data = String::new();
let mut in_body = false;
for line in msg.lines() {
if in_body {
if line == "+++" {
in_body = false;
break;
} else {
data.push_str(line);
data.push('\n');
}
} else if line.starts_with("+++ cranko-rc-info-v1") {
in_body = true;
}
}
if in_body {
println!("unterminated RC info body; trying to proceed anyway");
}
if data.is_empty() {
bail!("empty cranko-rc-info body in RC commit message");
}
let srci: SerializedRcCommitInfo = toml::from_str(&data)?;
Ok(RcCommitInfo {
commit: Some(CommitId(head_commit.id())),
projects: srci.projects,
})
}
pub fn hard_reset_changes(&self, changes: &ChangeList) -> Result<()> {
if changes.paths.is_empty() {
return Ok(());
}
let mut cb = git2::build::CheckoutBuilder::new();
cb.force();
for path in &changes.paths[..] {
let p: &RepoPath = path.as_ref();
cb.path(p);
}
self.repo.checkout_head(Some(&mut cb))?;
Ok(())
}
pub fn get_tag_name(&self, proj: &Project, rel: &ReleasedProjectInfo) -> Result<String> {
let mut tagname_args = HashMap::new();
tagname_args.insert("project_slug", proj.user_facing_name.to_owned());
tagname_args.insert("version", rel.version.clone());
let basis = SimpleCurlyFormat
.format(&self.release_tag_name_format, &tagname_args)
.map_err(|e| Error::msg(e.to_string()))?;
const REPLACEMENT: char = '_';
Ok(basis
.chars()
.map(|c| {
if c.is_alphanumeric() {
c
} else if c.is_control() {
REPLACEMENT
} else {
match c {
':' => '/',
' ' | '~' | '^' | '?' | '*' | '[' => REPLACEMENT,
c => c,
}
}
})
.collect())
}
pub fn tag_project_at_head(&self, proj: &Project, rel: &ReleasedProjectInfo) -> Result<()> {
let head_ref = self.repo.head()?;
let head_commit = head_ref.peel_to_commit()?;
let sig = self.get_signature()?;
let tagname = self.get_tag_name(proj, rel)?;
self.repo
.tag(&tagname, head_commit.as_object(), &sig, &tagname, false)?;
info!(
"created tag {} pointing at HEAD ({})",
&tagname,
head_commit.as_object().short_id()?.as_str().unwrap()
);
Ok(())
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ReleaseAvailability {
ExistingRelease(Version),
NewRelease,
NotAvailable,
}
impl Repository {
pub fn find_earliest_release_containing(
&self,
proj: &Project,
cid: &CommitId,
) -> Result<ReleaseAvailability> {
let maybe_rpi = self.find_published_release_containing(proj, cid)?;
if let Some(rpi) = maybe_rpi {
let v = Version::parse_like(&proj.version, rpi.version)?;
return Ok(ReleaseAvailability::ExistingRelease(v));
}
let head_ref = self.repo.head()?;
let head_commit = head_ref.peel_to_commit()?;
let head_id = head_commit.id();
if head_id == cid.0 || self.repo.graph_descendant_of(head_id, cid.0)? {
Ok(ReleaseAvailability::NewRelease)
} else {
Ok(ReleaseAvailability::NotAvailable)
}
}
fn find_published_release_containing(
&self,
proj: &Project,
cid: &CommitId,
) -> Result<Option<ReleasedProjectInfo>> {
let mut best_info = None;
let mut commit = if let Some(c) = self.try_get_release_commit()? {
c
} else {
return Ok(None);
};
loop {
if !self.repo.graph_descendant_of(commit.id(), cid.0)? {
break;
}
let release = self.parse_release_info_from_commit(&commit)?;
if let Some(cur_release) = release.lookup_if_released(proj) {
let cur_version = proj.version.parse_like(&cur_release.version)?;
if let Some((_, ref best_version)) = best_info {
if cur_version < *best_version {
best_info = Some((cur_release.clone(), cur_version));
}
} else {
best_info = Some((cur_release.clone(), cur_version));
}
}
if commit.parent_count() == 1 {
break;
}
commit = commit.parent(0)?;
}
Ok(best_info.map(|pair| pair.0))
}
}
#[derive(Clone, Debug, Default)]
pub struct ReleaseCommitInfo {
pub commit: Option<CommitId>,
pub projects: Vec<ReleasedProjectInfo>,
}
impl ReleaseCommitInfo {
pub fn lookup_project(&self, proj: &Project) -> Option<&ReleasedProjectInfo> {
for rpi in &self.projects {
if rpi.qnames == *proj.qualified_names() {
return Some(rpi);
}
}
None
}
pub fn lookup_if_released(&self, proj: &Project) -> Option<&ReleasedProjectInfo> {
self.lookup_project(proj).filter(|rel| rel.age == 0)
}
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
struct SerializedReleaseCommitInfo {
pub projects: Vec<ReleasedProjectInfo>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct ReleasedProjectInfo {
pub qnames: Vec<String>,
pub version: String,
pub age: usize,
}
#[derive(Clone, Debug, Default)]
pub struct RcCommitInfo {
pub commit: Option<CommitId>,
pub projects: Vec<RcProjectInfo>,
}
impl RcCommitInfo {
pub fn lookup_project(&self, proj: &Project) -> Option<&RcProjectInfo> {
for rci in &self.projects {
if rci.qnames == *proj.qualified_names() {
return Some(rci);
}
}
None
}
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
struct SerializedRcCommitInfo {
pub projects: Vec<RcProjectInfo>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct RcProjectInfo {
pub qnames: Vec<String>,
pub bump_spec: String,
}
#[derive(Debug, Default)]
pub struct ChangeList {
paths: Vec<RepoPathBuf>,
}
impl ChangeList {
pub fn add_path(&mut self, p: &RepoPath) {
self.paths.push(p.to_owned());
}
pub fn paths(&self) -> impl Iterator<Item = &RepoPath> {
self.paths[..].iter().map(|p| p.as_ref())
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct RepoHistory {
commits: Vec<CommitId>,
release_commit: Option<CommitId>,
}
impl RepoHistory {
pub fn release_commit(&self) -> Option<CommitId> {
self.release_commit
}
pub fn main_branch_commit(&self, repo: &Repository) -> Result<Option<CommitId>> {
let rcid = match self.release_commit {
Some(c) => c,
None => return Ok(None),
};
let release_commit = repo.repo.find_commit(rcid.0)?;
let rc_commit = a_ok_or!(
release_commit.parents().last();
["release commit has no parents?"]
);
let main_commit = a_ok_or!(
rc_commit.parents().last();
["rc commit has no parents?"]
);
Ok(Some(CommitId(main_commit.id())))
}
pub fn release_info(&self, repo: &Repository) -> Result<ReleaseCommitInfo> {
Ok(if let Some(cid) = self.release_commit() {
let commit = repo.repo.find_commit(cid.0)?;
repo.parse_release_info_from_commit(&commit)?
} else {
repo.get_bootstrap_release_info()
})
}
pub fn n_commits(&self) -> usize {
self.commits.len()
}
pub fn commits(&self) -> impl IntoIterator<Item = &CommitId> {
&self.commits[..]
}
}
#[derive(Debug)]
pub struct PathMatcher {
terms: Vec<PathMatcherTerm>,
}
impl PathMatcher {
pub fn new_include(p: RepoPathBuf) -> Self {
let terms = vec![PathMatcherTerm::Include(p)];
PathMatcher { terms }
}
pub fn make_disjoint(&mut self, other: &PathMatcher) -> &mut Self {
let mut new_terms = Vec::new();
for other_term in &other.terms {
if let PathMatcherTerm::Include(ref other_pfx) = other_term {
for term in &self.terms {
if let PathMatcherTerm::Include(ref pfx) = term {
if other_pfx.starts_with(pfx) {
new_terms.push(PathMatcherTerm::Exclude(other_pfx.clone()));
}
}
}
}
}
new_terms.append(&mut self.terms);
self.terms = new_terms;
self
}
pub fn repo_path_matches(&self, p: &RepoPath) -> bool {
for term in &self.terms {
match term {
PathMatcherTerm::Include(pfx) => {
if p.starts_with(pfx) {
return true;
}
}
PathMatcherTerm::Exclude(pfx) => {
if p.starts_with(pfx) {
return false;
}
}
}
}
false
}
}
#[derive(Debug)]
enum PathMatcherTerm {
Include(RepoPathBuf),
Exclude(RepoPathBuf),
}
pub enum ParsedHistoryRef {
Id(CommitId),
ThisCommit { salt: String },
Manual(String),
}
#[derive(Debug, Eq, Hash, PartialEq)]
#[repr(transparent)]
pub struct RepoPath([u8]);
impl std::convert::AsRef<RepoPath> for [u8] {
fn as_ref(&self) -> &RepoPath {
unsafe { &*(self as *const [_] as *const RepoPath) }
}
}
impl std::convert::AsRef<[u8]> for RepoPath {
fn as_ref(&self) -> &[u8] {
unsafe { &*(self.0.as_ref() as *const [u8]) }
}
}
impl RepoPath {
fn new(p: &[u8]) -> &Self {
p.as_ref()
}
pub fn split_basename(&self) -> (&RepoPath, &RepoPath) {
let basename = self.0.rsplit(|c| *c == b'/').next().unwrap();
let ndir = self.0.len() - basename.len();
return (self.0[..ndir].as_ref(), basename.as_ref());
}
pub fn pop_sep(&self) -> &RepoPath {
let n = self.0.len();
if n == 0 || self.0[n - 1] != b'/' {
self
} else {
self.0[..n - 1].as_ref()
}
}
pub fn len(&self) -> usize {
self.0.len()
}
pub fn as_path(&self) -> &Path {
bytes2path(&self.0)
}
pub fn to_owned(&self) -> RepoPathBuf {
RepoPathBuf::new(&self.0[..])
}
pub fn escaped(&self) -> String {
escape_pathlike(&self.0)
}
pub fn starts_with<P: AsRef<[u8]>>(&self, other: P) -> bool {
let other = other.as_ref();
let sn = self.len();
let on = other.len();
if sn < on {
false
} else {
&self.0[..on] == other
}
}
pub fn ends_with<P: AsRef<[u8]>>(&self, other: P) -> bool {
let other = other.as_ref();
let sn = self.len();
let on = other.len();
if sn < on {
false
} else {
&self.0[(sn - on)..] == other
}
}
}
impl git2::IntoCString for &RepoPath {
fn into_c_string(self) -> std::result::Result<std::ffi::CString, git2::Error> {
self.0.into_c_string()
}
}
#[cfg(unix)]
fn bytes2path(b: &[u8]) -> &Path {
use std::{ffi::OsStr, os::unix::prelude::*};
Path::new(OsStr::from_bytes(b))
}
#[cfg(windows)]
fn bytes2path(b: &[u8]) -> &Path {
use std::str;
Path::new(str::from_utf8(b).unwrap())
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
#[repr(transparent)]
pub struct RepoPathBuf(Vec<u8>);
impl std::convert::AsRef<RepoPath> for RepoPathBuf {
fn as_ref(&self) -> &RepoPath {
RepoPath::new(&self.0[..])
}
}
impl std::convert::AsRef<[u8]> for RepoPathBuf {
fn as_ref(&self) -> &[u8] {
&self.0[..]
}
}
impl RepoPathBuf {
pub fn new(b: &[u8]) -> Self {
RepoPathBuf(b.to_vec())
}
#[cfg(unix)]
#[allow(clippy::unnecessary_wraps)]
fn from_path<P: AsRef<Path>>(p: P) -> Result<Self> {
use std::os::unix::ffi::OsStrExt;
Ok(Self::new(p.as_ref().as_os_str().as_bytes()))
}
#[cfg(windows)]
fn from_path<P: AsRef<Path>>(p: P) -> Result<Self> {
let mut first = true;
let mut b = Vec::new();
for cmpt in p.as_ref().components() {
if first {
first = false;
} else {
b.push(b'/');
}
if let std::path::Component::Normal(c) = cmpt {
b.extend(c.to_str().unwrap().as_bytes());
} else {
bail!(
"path with unexpected components: `{}`",
p.as_ref().display()
);
}
}
Ok(RepoPathBuf(b))
}
pub fn truncate(&mut self, len: usize) {
self.0.truncate(len);
}
pub fn push<C: AsRef<[u8]>>(&mut self, component: C) {
let n = self.0.len();
if n > 0 && self.0[n - 1] != b'/' {
self.0.push(b'/');
}
self.0.extend(component.as_ref());
}
}
impl std::ops::Deref for RepoPathBuf {
type Target = RepoPath;
fn deref(&self) -> &RepoPath {
RepoPath::new(&self.0[..])
}
}
pub fn escape_pathlike(b: &[u8]) -> String {
if let Ok(s) = std::str::from_utf8(b) {
s.to_owned()
} else {
let mut buf = vec![b'\"'];
buf.extend(b.iter().flat_map(|c| std::ascii::escape_default(*c)));
buf.push(b'\"');
String::from_utf8(buf).unwrap()
}
}