Skip to content
Draft
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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,13 +90,15 @@ This can be changed, but it is heavily discouraged.
| revision | String | Optional | A revision to checkout, this can either be a tagged version or a commit hash | `v0.2` |
| branch | Boolean | Optional | A branch to checkout, fetches last commit | `feature/v2` |
| protocol | String | Optional | A protocol to use: [ssh, https] | `ssh` |
| root | String | Optional | Repository subdirectory used as the root for descriptor loading, policies, and content roots | `"apis/users"` |
| allow_policies | [String] | Optional | Allow policy rules (`*` at the beginning or end matches arbitrary directory depth) | `"/prefix/*"`, `"*/subpath/*"`, `"/path/to/file.proto"` |
| deny_policies | [String] | Optional | Deny policy rules (`*` at the beginning or end matches arbitrary directory depth) | `"/prefix/*"`, `"*/subpath/*"`, `"/path/to/file.proto"` |
| prune | bool | Optional | Whether to prune unneeded transitive proto files | `true` / `false` |
| transitive | bool | Optional | Flags this dependency as transitive | `true` / `false` |
| content_roots | [String] | Optional | Which subdirectories to import from | `["/myservice", "/com/org/client"]` |

The patterns in `allow_policies` and `deny_policies` are matched against the paths relative to the nearest path in `content_roots`.
If `root` is set, dependency files, the dependency's own `protofetch.toml`, `content_roots`, and policies are all resolved relative to that root.

### Protofetch dependency toml example

Expand All @@ -123,6 +125,7 @@ transitive = true
[scoped-down-dep4]
url = "github.com/org/dep4"
revision = "v1.1"
root = "/apis"
content_roots = ["/scope/path"]
allow_policies = ["prefix/subpath/scoped_path/*"]
```
Expand Down
5 changes: 3 additions & 2 deletions src/cache/git.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::path::PathBuf;

use crate::{
git::cache::ProtofetchGitCache,
model::protofetch::{Coordinate, RevisionSpecification},
model::protofetch::{Coordinate, DependencyRoot, RevisionSpecification},
};

use super::RepositoryCache;
Expand All @@ -23,10 +23,11 @@ impl RepositoryCache for ProtofetchGitCache {
&self,
coordinate: &Coordinate,
commit_hash: &str,
roots: &[DependencyRoot],
) -> anyhow::Result<PathBuf> {
let path = self
.repository(coordinate)?
.create_worktree(coordinate, commit_hash)?;
.create_worktree(coordinate, commit_hash, roots)?;
Ok(path)
}
}
6 changes: 4 additions & 2 deletions src/cache/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ mod git;

use std::{path::PathBuf, sync::Arc};

use crate::model::protofetch::{Coordinate, RevisionSpecification};
use crate::model::protofetch::{Coordinate, DependencyRoot, RevisionSpecification};

pub trait RepositoryCache: Send + Sync {
fn fetch(
Expand All @@ -16,6 +16,7 @@ pub trait RepositoryCache: Send + Sync {
&self,
coordinate: &Coordinate,
commit_hash: &str,
roots: &[DependencyRoot],
) -> anyhow::Result<PathBuf>;
}

Expand All @@ -36,7 +37,8 @@ where
&self,
coordinate: &Coordinate,
commit_hash: &str,
roots: &[DependencyRoot],
) -> anyhow::Result<PathBuf> {
T::create_worktree(self, coordinate, commit_hash)
T::create_worktree(self, coordinate, commit_hash, roots)
}
}
2 changes: 2 additions & 0 deletions src/fetch/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ pub(crate) mod tests {
&dependency.specification,
None,
&dependency.name,
dependency.rules.root.as_ref(),
)
.map_err(FetchError::Resolver)?;

Expand Down Expand Up @@ -220,6 +221,7 @@ pub(crate) mod tests {
specification: &RevisionSpecification,
_: Option<&str>,
_: &ModuleName,
_: Option<&crate::model::protofetch::DependencyRoot>,
) -> anyhow::Result<CommitAndDescriptor> {
Ok(self
.entries
Expand Down
9 changes: 8 additions & 1 deletion src/fetch/parallel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,13 @@ where
let _g = coord_lock.lock().expect("coord lock poisoned");
info!("Resolving {}", dep.coordinate);
let result = resolver
.resolve(&dep.coordinate, &dep.specification, None, &dep.name)
.resolve(
&dep.coordinate,
&dep.specification,
None,
&dep.name,
dep.rules.root.as_ref(),
)
.map_err(FetchError::Resolver)?;
Ok((idx, dep, result))
},
Expand Down Expand Up @@ -237,6 +243,7 @@ mod tests {
specification: &RevisionSpecification,
_: Option<&str>,
_: &ModuleName,
_: Option<&crate::model::protofetch::DependencyRoot>,
) -> anyhow::Result<CommitAndDescriptor> {
let now = self.in_flight.fetch_add(1, Ordering::SeqCst) + 1;
self.max_in_flight.fetch_max(now, Ordering::SeqCst);
Expand Down
39 changes: 34 additions & 5 deletions src/git/repository.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
use std::{path::PathBuf, str::Utf8Error};
use std::{
path::{Path, PathBuf},
str::Utf8Error,
};

use crate::model::protofetch::{
Coordinate, Descriptor, ModuleName, Revision, RevisionSpecification,
Coordinate, DependencyRoot, Descriptor, ModuleName, Revision, RevisionSpecification,
};
use git2::{Oid, Repository, ResetType, WorktreeAddOptions};
use log::{debug, warn};
Expand Down Expand Up @@ -108,10 +111,15 @@ impl ProtoGitRepository<'_> {
&self,
dep_name: &ModuleName,
commit_hash: &str,
root: Option<&DependencyRoot>,
) -> Result<Descriptor, ProtoRepoError> {
let result = self
.git_repo
.revparse_single(&format!("{commit_hash}:protofetch.toml"));
let descriptor_path = root
.map(|root| root.value.join("protofetch.toml"))
.unwrap_or_else(|| PathBuf::from("protofetch.toml"));
let result = self.git_repo.revparse_single(&format!(
"{commit_hash}:{}",
git_object_path(&descriptor_path)
));

match result {
Err(e) if e.code() == git2::ErrorCode::NotFound => {
Expand Down Expand Up @@ -184,7 +192,9 @@ impl ProtoGitRepository<'_> {
&self,
coordinate: &Coordinate,
commit_hash: &str,
_roots: &[DependencyRoot],
) -> Result<PathBuf, ProtoRepoError> {
// TODO: Use dependency roots to configure a sparse-checkout limited to those root paths.
let mut base_path = self.cache.worktrees_path();
base_path.push(coordinate.to_path());

Expand Down Expand Up @@ -267,3 +277,22 @@ impl ProtoGitRepository<'_> {
Ok(self.git_repo.merge_base(a, b)? == a)
}
}

fn git_object_path(path: &Path) -> String {
path.to_string_lossy().replace('\\', "/")
}

#[cfg(test)]
mod tests {
use std::path::PathBuf;

use super::git_object_path;

#[test]
fn git_object_path_uses_forward_slashes() {
assert_eq!(
git_object_path(&PathBuf::from(r"service\protofetch.toml")),
"service/protofetch.toml"
);
}
}
32 changes: 30 additions & 2 deletions src/model/protofetch/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -280,13 +280,27 @@ impl Display for RevisionSpecification {

#[derive(Default, Clone, Debug, Ord, PartialOrd, PartialEq, Eq, Hash)]
pub struct Rules {
pub root: Option<DependencyRoot>,
pub prune: bool,
pub transitive: bool,
pub content_roots: BTreeSet<ContentRoot>,
pub allow_policies: AllowPolicies,
pub deny_policies: DenyPolicies,
}

/// A dependency root path for a repository.
#[derive(Ord, PartialOrd, PartialEq, Eq, Hash, Debug, Clone)]
pub struct DependencyRoot {
pub value: PathBuf,
}

impl DependencyRoot {
pub fn from_string(s: &str) -> DependencyRoot {
let path = PathBuf::from(s.strip_prefix('/').unwrap_or(s));
DependencyRoot { value: path }
}
}

/// A content root path for a repository.
#[derive(Ord, PartialOrd, PartialEq, Eq, Hash, Debug, Clone)]
pub struct ContentRoot {
Expand All @@ -295,8 +309,7 @@ pub struct ContentRoot {

impl ContentRoot {
pub fn from_string(s: &str) -> ContentRoot {
let path = PathBuf::from(s);
let path = path.strip_prefix("/").unwrap_or(&path).to_path_buf();
let path = PathBuf::from(s.strip_prefix('/').unwrap_or(s));
ContentRoot { value: path }
}
}
Expand Down Expand Up @@ -654,6 +667,12 @@ impl Descriptor {
if let Some(branch) = d.specification.branch {
dependency.insert("branch".to_owned(), Value::String(branch));
}
if let Some(root) = d.rules.root {
dependency.insert(
"root".to_owned(),
Value::String(root.value.to_string_lossy().to_string()),
);
}
description.insert(d.name.to_string(), Value::Table(dependency));
}
Value::Table(description)
Expand Down Expand Up @@ -692,6 +711,12 @@ fn parse_dependency(name: String, value: &toml::Value) -> Result<Dependency, Par
.map_or(Ok(None), |v| v.map(Some))?
.unwrap_or(false);

let root = value
.get("root")
.map(|v| v.clone().try_into::<String>())
.map_or(Ok(None), |v| v.map(Some))?
.map(|str| DependencyRoot::from_string(&str));

let content_roots = value
.get("content_roots")
.map(|v| v.clone().try_into::<Vec<String>>())
Expand All @@ -711,6 +736,7 @@ fn parse_dependency(name: String, value: &toml::Value) -> Result<Dependency, Par
let deny_policies = DenyPolicies::new(parse_policies(value, "deny_policies")?);

let rules = Rules {
root,
prune,
transitive,
content_roots,
Expand Down Expand Up @@ -830,6 +856,7 @@ mod tests {
protocol = "https"
url = "github.com/org/repo"
revision = "1.0.0"
root = "/apis/users"
prune = true
content_roots = ["src"]
allow_policies = ["/foo/proto/file.proto", "/foo/other/*", "*/some/path/*", "re://_(?:test|unittest)\\.proto"]
Expand All @@ -851,6 +878,7 @@ mod tests {
branch: None,
},
rules: Rules {
root: Some(DependencyRoot::from_string("/apis/users")),
prune: true,
content_roots: BTreeSet::from([ContentRoot::from_string("src")]),
transitive: false,
Expand Down
19 changes: 10 additions & 9 deletions src/model/protofetch/resolved.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,21 +31,22 @@ impl ResolvedDependency {
!self.rules.is_empty() && self.rules.iter().all(|r| r.transitive)
}

pub fn all_content_roots(&self) -> impl Iterator<Item = &ContentRoot> {
self.rules.iter().flat_map(|r| r.content_roots.iter())
}

/// Returns true if `path` (pre-zoom, relative to the dep's cache dir) is
/// accepted by at least one occurrence's (allow ∧ ¬deny) policy after
/// zooming with that occurrence's content roots.
/// An empty `rules` means no filtering — allow all.
pub fn is_file_allowed(&self, path: &Path) -> bool {
self.rules.is_empty()
|| self.rules.iter().any(|rules| {
let zoomed = zoom_in_content_roots(&rules.content_roots, path);
AllowPolicies::should_allow_file(&rules.allow_policies, &zoomed)
&& !DenyPolicies::should_deny_file(&rules.deny_policies, &zoomed)
})
|| self
.rules
.iter()
.any(|rules| Self::is_file_allowed_by_rules(rules, path))
}

pub fn is_file_allowed_by_rules(rules: &Rules, path: &Path) -> bool {
let zoomed = zoom_in_content_roots(&rules.content_roots, path);
AllowPolicies::should_allow_file(&rules.allow_policies, &zoomed)
&& !DenyPolicies::should_deny_file(&rules.deny_policies, &zoomed)
}
}

Expand Down
Loading
Loading