use dynfmt::{Format, SimpleCurlyFormat};
use log::info;
use serde::{Deserialize, Serialize};
use std::{
collections::HashMap,
fs::File,
io::{BufRead, BufReader},
path::{Path, PathBuf},
};
use crate::{
errors::{Error, Result},
graph::ProjectGraph,
project::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)
}
}
pub struct Repository {
repo: git2::Repository,
upstream_name: String,
upstream_rc_name: String,
upstream_release_name: String,
release_tag_name_format: String,
}
impl Repository {
pub fn open_from_env() -> Result<Repository> {
let repo = git2::Repository::open_from_env()?;
if repo.is_bare() {
return Err(Error::BareRepository);
}
let mut upstream_name = None;
let mut n_remotes = 0;
for remote_name in &repo.remotes()? {
if let Some(remote_name) = remote_name {
n_remotes += 1;
if upstream_name.is_none() || remote_name == "origin" {
upstream_name = Some(remote_name.to_owned());
}
}
}
if upstream_name.is_none() || (n_remotes > 1 && upstream_name.as_deref() != Some("origin"))
{
return Err(Error::NoUpstreamRemote);
}
let upstream_name = upstream_name.unwrap();
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,
})
}
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(|| {
Error::Environment(format!(
"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(|| {
Error::Environment("current branch name not Unicode".to_owned())
})?
.to_owned(),
)
})
}
pub fn parse_commit_ref<T: AsRef<str>>(&self, text: T) -> Result<CommitRef> {
let text = text.as_ref();
if let Ok(id) = text.parse() {
Ok(CommitRef::Id(CommitId(id)))
} else if text.starts_with("thiscommit:") {
Ok(CommitRef::ThisCommit {
salt: text[11..].to_owned(),
})
} else {
Err(Error::InvalidCommitReference(text.to_owned()))
}
}
pub fn resolve_commit_ref(
&self,
cref: &CommitRef,
ref_source_path: &RepoPath,
) -> Result<CommitId> {
let cid = match cref {
CommitRef::Id(id) => id.clone(),
CommitRef::ThisCommit { ref salt } => lookup_this(self, salt, ref_source_path)?,
};
self.repo.find_commit(cid.0)?;
return Ok(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(Error::Environment(format!(
"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(|| {
Error::Environment(format!(
"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 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(|_| Error::OutsideOfRepository(c_p.display().to_string()))?;
RepoPathBuf::from_path(rel)
}
pub fn scan_paths<F>(&self, mut f: F) -> Result<()>
where
F: FnMut(&RepoPath) -> (),
{
let index = self.repo.index()?;
for entry in index.iter() {
f(RepoPath::new(&entry.path));
}
Ok(())
}
pub fn check_if_dirty(&self, ok_matchers: &[PathMatcher]) -> Result<Option<String>> {
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.escaped()));
}
}
}
Ok(None)
}
pub fn get_file_at_commit(&self, cid: &CommitId, path: &RepoPath) -> Result<Vec<u8>> {
let commit = self.repo.find_commit(cid.0)?;
let tree = commit.tree()?;
let entry = tree.get_path(path.as_path())?;
let object = entry.to_object(&self.repo)?;
let blob = object.as_blob().ok_or_else(|| {
Error::Environment(format!(
"path `{}` should correspond to a Git blob but does not",
path.escaped(),
))
})?;
Ok(blob.content().to_owned())
}
pub fn get_latest_release_info(&self) -> Result<ReleaseCommitInfo> {
if let Some(c) = self.try_get_release_commit()? {
Ok(self.parse_release_info_from_commit(&c)?)
} else {
Ok(ReleaseCommitInfo::default())
}
}
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) -> 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 proj in graph.toposort()? {
let age = if let Some(ri) = rel_info.lookup_project(proj) {
if proj.version.to_string() == ri.version {
ri.age + 1
} else {
0
}
} else {
0
};
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(|| Error::NotUnicodeError)?;
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.len() == 0 {
return Err(Error::InvalidCommitMessageFormat);
}
let srci: SerializedReleaseCommitInfo = toml::from_str(&data)?;
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()];
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.clone(), 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(Error::DirtyRepository(path.escaped()));
} 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(Error::DirtyRepository(path.escaped()));
}
}
}
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 mut info = SerializedRcCommitInfo::default();
info.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(|| Error::NotUnicodeError)?;
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.len() == 0 {
return Err(Error::InvalidCommitMessageFormat);
}
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.len() == 0 {
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());
Ok(SimpleCurlyFormat
.format(&self.release_tag_name_format, &tagname_args)?
.to_string())
}
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(())
}
pub fn find_earliest_release_containing(
&self,
proj: &Project,
cid: &CommitId,
) -> Result<CommitAvailability> {
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(CommitAvailability::ExistingRelease(v));
}
let head_ref = self.repo.head()?;
let head_commit = head_ref.peel_to_commit()?;
if self.repo.graph_descendant_of(head_commit.id(), cid.0)? {
Ok(CommitAvailability::NewRelease)
} else {
Ok(CommitAvailability::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(Clone, Debug, Eq, PartialEq)]
pub enum CommitAvailability {
ExistingRelease(Version),
NewRelease,
NotAvailable,
}
#[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());
}
}
#[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 release_info(&self, repo: &Repository) -> Result<Option<ReleaseCommitInfo>> {
if let Some(cid) = self.release_commit() {
let commit = repo.repo.find_commit(cid.0)?;
Ok(Some(repo.parse_release_info_from_commit(&commit)?))
} else {
Ok(None)
}
}
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 CommitRef {
Id(CommitId),
ThisCommit { salt: String },
}
#[derive(Debug, Eq, PartialEq)]
#[repr(transparent)]
pub struct RepoPath([u8]);
impl std::convert::AsRef<RepoPath> for [u8] {
fn as_ref(&self) -> &RepoPath {
unsafe { &*(self.as_ref() 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 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, 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)]
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 {
return Err(Error::OutsideOfRepository(format!(
"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().map(|c| std::ascii::escape_default(*c)).flatten());
buf.push(b'\"');
String::from_utf8(buf).unwrap()
}
}