diff --git a/Cargo.lock b/Cargo.lock index edd9a8c7..34e73150 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -542,7 +542,7 @@ checksum = "fbdcdcb6d86f71c5e97409ad45898af11cbc995b4ee8112d59095a28d376c935" [[package]] name = "content-addressed-cache" -version = "0.7.0" +version = "0.7.1" dependencies = [ "anyhow", "ciborium", @@ -1062,7 +1062,7 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "focus-commands" -version = "0.7.0" +version = "0.7.1" dependencies = [ "anyhow", "chrono", @@ -1097,7 +1097,7 @@ dependencies = [ [[package]] name = "focus-internals" -version = "0.7.0" +version = "0.7.1" dependencies = [ "anyhow", "chrono", @@ -1139,7 +1139,7 @@ dependencies = [ [[package]] name = "focus-migrations" -version = "0.7.0" +version = "0.7.1" dependencies = [ "anyhow", "focus-internals", @@ -1161,7 +1161,7 @@ dependencies = [ [[package]] name = "focus-operations" -version = "0.7.0" +version = "0.7.1" dependencies = [ "anyhow", "assert_cmd", @@ -1206,7 +1206,7 @@ dependencies = [ [[package]] name = "focus-platform" -version = "0.7.0" +version = "0.7.1" dependencies = [ "chrono", "serde", @@ -1216,7 +1216,7 @@ dependencies = [ [[package]] name = "focus-repo-management" -version = "0.7.0" +version = "0.7.1" dependencies = [ "anyhow", "focus-internals", @@ -1232,7 +1232,7 @@ dependencies = [ [[package]] name = "focus-testing" -version = "0.7.0" +version = "0.7.1" dependencies = [ "anyhow", "assert_cmd", @@ -1249,7 +1249,7 @@ dependencies = [ [[package]] name = "focus-tracing" -version = "0.7.0" +version = "0.7.1" dependencies = [ "anyhow", "chrono", @@ -1284,7 +1284,7 @@ dependencies = [ [[package]] name = "focus-util" -version = "0.7.0" +version = "0.7.1" dependencies = [ "anyhow", "assert_cmd", @@ -3533,7 +3533,7 @@ dependencies = [ [[package]] name = "tool-insights-client" -version = "0.7.0" +version = "0.7.1" dependencies = [ "anyhow", "libc", diff --git a/content-addressed-cache/Cargo.toml b/content-addressed-cache/Cargo.toml index 735ff18c..815aa9fc 100644 --- a/content-addressed-cache/Cargo.toml +++ b/content-addressed-cache/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "content-addressed-cache" -version = "0.7.0" +version = "0.7.1" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/focus/commands/Cargo.toml b/focus/commands/Cargo.toml index 81575bb7..cacf6295 100644 --- a/focus/commands/Cargo.toml +++ b/focus/commands/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "focus-commands" -version = "0.7.0" +version = "0.7.1" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/focus/internals/Cargo.toml b/focus/internals/Cargo.toml index b9ceaf94..a7e7a145 100644 --- a/focus/internals/Cargo.toml +++ b/focus/internals/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "focus-internals" -version = "0.7.0" +version = "0.7.1" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/focus/internals/src/lib/model/data_paths.rs b/focus/internals/src/lib/model/data_paths.rs index c6eb796c..1ce9fbb1 100644 --- a/focus/internals/src/lib/model/data_paths.rs +++ b/focus/internals/src/lib/model/data_paths.rs @@ -4,7 +4,7 @@ use anyhow::{Context, Result}; use tracing::warn; -use std::path::PathBuf; +use std::{path::PathBuf, sync::Arc}; use crate::model::selection::WorkingTree; @@ -17,7 +17,7 @@ pub struct DataPaths { } impl DataPaths { - pub fn from_working_tree(working_tree: &WorkingTree) -> Result { + pub fn from_working_tree(working_tree: Arc) -> Result { let dot_focus_dir = working_tree.work_dir().join(".focus"); let focus_dir = working_tree.work_dir().join("focus"); let data_dir = dot_focus_dir.join("focus"); diff --git a/focus/internals/src/lib/model/repo.rs b/focus/internals/src/lib/model/repo.rs index 50e92608..42c7e53f 100644 --- a/focus/internals/src/lib/model/repo.rs +++ b/focus/internals/src/lib/model/repo.rs @@ -71,6 +71,12 @@ pub const PROJECT_CACHE_INCLUDE_HEADERS_FILE_CONFIG_KEY: &str = "focus.project-cache.include-headers-from"; pub const BAZEL_ONE_SHOT_RESOLUTION_CONFIG_KEY: &str = "focus.bazel.one-shot"; +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum WorkingTreeKind { + Sparse, + Dense, +} + /// Models a Git working tree. pub struct WorkingTree { repo: git2::Repository, @@ -110,6 +116,14 @@ impl WorkingTree { Self::new(repo) } + pub fn kind(&self) -> WorkingTreeKind { + if self.sparse_checkout_path().is_file() { + WorkingTreeKind::Sparse + } else { + WorkingTreeKind::Dense + } + } + fn info_dir(&self) -> PathBuf { self.repo.path().join("info") } @@ -407,6 +421,11 @@ impl WorkingTree { } pub fn configure(&self, app: Arc) -> Result<()> { + if self.kind() == WorkingTreeKind::Dense { + // We do not perform this configuration in dense repos. + return Ok(()); + } + let config_snapshot = self.repo.config()?.snapshot()?; if config_snapshot.get_str(INDEX_SPARSE_CONFIG_KEY).is_err() { @@ -501,13 +520,29 @@ impl WorkingTree { } } +pub trait Outliner { + /// Get the patterns associated with the provided targets at a given revision. + fn outline( + &self, + commit_id: git2::Oid, + target_set: &TargetSet, + resolution_options: &ResolutionOptions, + snapshot: Option, + app: Arc, + ) -> Result<(PatternSet, ResolutionResult)>; + + fn underlying(&self) -> Arc; + + fn identity(&self) -> &str; +} + /// A specialization of a WorkingTree used for outlining tasks, containing only files related to, and necessary for querying, the build graph. #[derive(Debug, PartialEq, Eq)] -pub struct OutliningTree { +pub struct OutliningTreeOutliner { underlying: Arc, } -impl OutliningTree { +impl OutliningTreeOutliner { pub fn new(underlying: Arc) -> Self { Self { underlying } } @@ -566,16 +601,10 @@ impl OutliningTree { Ok(pattern_container.patterns) } +} - fn resolver(&self) -> Result { - let cache_dir = dirs::cache_dir() - .context("failed to determine cache dir")? - .join("focus") - .join("cache"); - Ok(RoutingResolver::new(cache_dir.as_path())) - } - - pub fn outline( +impl Outliner for OutliningTreeOutliner { + fn outline( &self, commit_id: git2::Oid, target_set: &TargetSet, @@ -583,110 +612,192 @@ impl OutliningTree { snapshot: Option, app: Arc, ) -> Result<(PatternSet, ResolutionResult)> { + let repo = self.underlying(); + let git_repo = repo.git_repo(); self.apply_configured_outlining_patterns(commit_id, app.clone()) .context("Applying configured outlining patterns failed")?; self.underlying() .switch_to_commit(commit_id, true, true, app.clone()) .context("Failed to switch to commit")?; - - let mut patterns = PatternSet::new(); - - let repo_path = self.underlying().work_dir().to_owned(); - if let Some(snapshot_path) = snapshot { - git::snapshot::apply(snapshot_path, repo_path.as_path(), false, app.clone()) + let repo_workdir = git_repo + .workdir() + .ok_or_else(|| anyhow::anyhow!("Repository has no workdir"))?; + git::snapshot::apply(snapshot_path, repo_workdir, false, app.clone()) .context("Applying patch to outlining tree failed")?; } + outline_common(git_repo, target_set, resolution_options, app, commit_id) + } - let cache_options = CacheOptions::default(); - let request = ResolutionRequest { - repo: repo_path.clone(), - targets: target_set.clone(), - options: resolution_options.clone(), - }; - let resolver = self.resolver().context("Failed to create resolver")?; - let result = resolver.resolve(&request, &cache_options, app)?; - - let treat_path = |p: &Path| -> Result> { - let p = p - .strip_prefix(&repo_path) - .context("Failed to strip repo path prefix")?; - if p == paths::MAIN_SEPARATOR_PATH.as_path() { - Ok(None) - } else { - Ok(Some(p.to_owned())) - } - }; + fn underlying(&self) -> Arc { + self.underlying.clone() + } - for path in result.paths.iter() { - let qualified_path = repo_path.join(path); - - let path = self - .find_closest_directory_with_build_file(commit_id, &qualified_path) - .context("locating closest build file")? - .unwrap_or(qualified_path); - if let Some(path) = treat_path(&path)? { - patterns.insert(Pattern::Directory { - precedence: LAST, - path, - recursive: true, - }); - } - } + fn identity(&self) -> &str { + "OutliningTreeOutliner" + } +} + +/// The dense repo outliner performs outlining directly in a dense working tree. +#[derive(Debug, PartialEq, Eq)] +pub struct DenseRepoOutliner { + underlying: Arc, +} - Ok((patterns, result)) +impl DenseRepoOutliner { + pub fn new(underlying: Arc) -> Self { + Self { underlying } } +} - fn find_closest_directory_with_build_file( +impl Outliner for DenseRepoOutliner { + /// Outline in the dense repo. Cannot switch commits. Never applies snapshots, complains if they are passed. + fn outline( &self, commit_id: git2::Oid, - path: impl AsRef, - // ceiling: impl AsRef, - ) -> Result> { - let path = path.as_ref(); - let git_repo = self.underlying.git_repo(); - let tree = git_repo - .find_commit(commit_id) - .context("Resolving commit")? - .tree() - .context("Resolving tree")?; - - let mut path = path.to_owned(); - loop { - if let Ok(tree_entry) = tree.get_path(&path) { - info!(?path, "Current"); - // If the entry is a tree, get it. - if tree_entry.kind() == Some(ObjectType::Tree) { - let tree_object = tree_entry - .to_object(git_repo) - .with_context(|| format!("Resolving tree {}", path.display()))?; - let current_tree = tree_object.as_tree().unwrap(); - // Iterate through the tree to see if there is a build file. - for entry in current_tree.iter() { - if entry.kind() == Some(ObjectType::Blob) { - if let Some(name) = entry.name() { - info!(?name, "Considering file"); - - let candidate_path = PathBuf::from_str(name)?; - if is_build_definition(candidate_path) { - info!(?name, "Found build definition"); - - return Ok(Some(path)); - } + target_set: &TargetSet, + resolution_options: &ResolutionOptions, + snapshot: Option, + app: Arc, + ) -> Result<(PatternSet, ResolutionResult)> { + let underlying_repo = self.underlying(); + let checked_out_commit = underlying_repo.get_head_commit()?; + let checked_out_commit_id = checked_out_commit.id(); + if checked_out_commit_id != commit_id { + bail!( + "Dense tree is at commit {} rather than the expected commit {}", + hex::encode(checked_out_commit.id().as_bytes()), + hex::encode(commit_id.as_bytes()) + ) + } + + if snapshot.is_some() { + bail!("Cannot outline in a dense repo with changes present"); + } + + outline_common( + self.underlying().git_repo(), + target_set, + resolution_options, + app, + commit_id, + ) + } + + fn underlying(&self) -> Arc { + self.underlying.clone() + } + + fn identity(&self) -> &str { + "DenseRepoOutliner" + } +} + +impl DenseRepoOutliner {} + +fn make_routing_resolver() -> Result { + let cache_dir = dirs::cache_dir() + .context("failed to determine cache dir")? + .join("focus") + .join("cache"); + Ok(RoutingResolver::new(cache_dir.as_path())) +} + +fn outline_common( + repository: &Repository, + target_set: &HashSet, + resolution_options: &ResolutionOptions, + app: Arc, + commit_id: Oid, +) -> Result<(PatternSet, ResolutionResult), anyhow::Error> { + let repo_workdir = repository + .workdir() + .ok_or_else(|| anyhow::anyhow!("Repository has no workdir"))?; + let cache_options = CacheOptions::default(); + let request = ResolutionRequest { + repo: repo_workdir.to_owned(), + targets: target_set.clone(), + options: resolution_options.clone(), + }; + let mut patterns = PatternSet::new(); + let resolver = make_routing_resolver()?; + let result = resolver.resolve(&request, &cache_options, app)?; + for path in result.paths.iter() { + let qualified_path = repo_workdir.join(path); + + let path = find_closest_directory_with_build_file(repository, commit_id, &qualified_path) + .context("Failed locating closest build file")? + .unwrap_or(qualified_path); + if let Some(path) = treat_path(repo_workdir, &path)? { + patterns.insert(Pattern::Directory { + precedence: LAST, + path, + recursive: true, + }); + } + } + Ok((patterns, result)) +} + +fn treat_path(repo_path: impl AsRef, path: impl AsRef) -> Result> { + let repo_path = repo_path.as_ref(); + let p = path.as_ref(); + let p = p + .strip_prefix(repo_path) + .context("Failed to strip repo path prefix")?; + + if p == paths::MAIN_SEPARATOR_PATH.as_path() { + Ok(None) + } else { + Ok(Some(p.to_owned())) + } +} + +fn find_closest_directory_with_build_file( + git_repo: &git2::Repository, + commit_id: git2::Oid, + path: impl AsRef, + // ceiling: impl AsRef, +) -> Result> { + let path = path.as_ref(); + let tree = git_repo + .find_commit(commit_id) + .context("Resolving commit")? + .tree() + .context("Resolving tree")?; + + let mut path = path.to_owned(); + loop { + if let Ok(tree_entry) = tree.get_path(&path) { + // If the entry is a tree, get it. + if tree_entry.kind() == Some(ObjectType::Tree) { + let tree_object = tree_entry + .to_object(git_repo) + .with_context(|| format!("Resolving tree {}", path.display()))?; + let current_tree = tree_object.as_tree().unwrap(); + // Iterate through the tree to see if there is a build file. + for entry in current_tree.iter() { + if entry.kind() == Some(ObjectType::Blob) { + if let Some(name) = entry.name() { + let candidate_path = PathBuf::from_str(name)?; + if is_build_definition(candidate_path) { + info!(?name, ?path, "Found build definition"); + + return Ok(Some(path)); } } } } } - - if !path.pop() { - // We have reached the root with no match. - break; - } } - Ok(None) + if !path.pop() { + // We have reached the root with no match. + break; + } } + + Ok(None) } const OUTLINING_TREE_NAME: &str = "outlining-tree"; @@ -694,8 +805,8 @@ const OUTLINING_TREE_NAME: &str = "outlining-tree"; pub struct Repo { path: PathBuf, git_dir: PathBuf, - working_tree: Option, - outlining_tree: Option, + working_tree: Option>, + outliner: Option>, repo: git2::Repository, config: Configuration, app: Arc, @@ -710,22 +821,27 @@ impl Repo { bail!("Bare repos are not supported"); } let git_dir = repo.path().to_owned(); - let working_tree = match repo.workdir() { + let working_tree: Option> = match repo.workdir() { Some(work_dir) => { let repo = git2::Repository::open(work_dir)?; - Some(WorkingTree::new(repo)?) + Some(Arc::new(WorkingTree::new(repo)?)) } None => None, }; let outlining_tree_path = Self::outlining_tree_path(&git_dir); let outlining_tree_git_dir = git_dir.join("worktrees").join(OUTLINING_TREE_NAME); - let outlining_tree = if outlining_tree_path.is_dir() { - Some(OutliningTree::new(Arc::new(WorkingTree::from_git_dir( - &outlining_tree_git_dir, - )?))) + let outlining_tree: Option> = if outlining_tree_path.is_dir() { + Some(Arc::new(OutliningTreeOutliner::new(Arc::new( + WorkingTree::from_git_dir(&outlining_tree_git_dir)?, + )))) } else { - None + match working_tree.as_ref() { + Some(working_tree) if working_tree.kind() == WorkingTreeKind::Dense => { + Some(Arc::new(DenseRepoOutliner::new(working_tree.clone()))) + } + _ => None, + } }; let config = Configuration::new(path).context("Loading configuration")?; @@ -735,7 +851,7 @@ impl Repo { path, git_dir, working_tree, - outlining_tree, + outliner: outlining_tree, repo, config, app, @@ -758,6 +874,10 @@ impl Repo { Self::focus_git_dir_path(git_dir).join(OUTLINING_TREE_NAME) } + pub fn path(&self) -> &Path { + &self.path + } + /// Run a sync, returning the number of patterns that were applied and whether a checkout occured as a result of the profile changing. pub fn sync( &self, @@ -768,7 +888,7 @@ impl Repo { cache: Option<&RocksDBCache>, snapshot: Option, ) -> Result<(usize, bool)> { - let (working_tree, outlining_tree) = match (&self.working_tree, &self.outlining_tree) { + let (working_tree, outlining_tree) = match (&self.working_tree, &self.outliner) { (Some(working_tree), Some(outlining_tree)) => (working_tree, outlining_tree), _ => { // TODO: we might succeed in synchronization without an outlining tree. @@ -789,13 +909,19 @@ impl Repo { self.sync_incremental( commit_id, targets, - outlining_tree, + outlining_tree.as_ref(), cache, snapshot, app.clone(), ) } else { - self.sync_one_shot(commit_id, targets, outlining_tree, snapshot, app.clone()) + self.sync_one_shot( + commit_id, + targets, + outlining_tree.as_ref(), + snapshot, + app.clone(), + ) }?; outline_patterns.extend(working_tree.default_working_tree_patterns()?); @@ -816,7 +942,7 @@ impl Repo { &self, commit_id: Oid, targets: &HashSet, - outlining_tree: &OutliningTree, + outliner: &dyn Outliner, snapshot: Option, app: Arc, ) -> Result { @@ -824,7 +950,7 @@ impl Repo { let resolution_options = ResolutionOptions { bazel_resolution_strategy: BazelResolutionStrategy::OneShot, }; - let (outline_patterns, _resolution_result) = outlining_tree + let (outline_patterns, _resolution_result) = outliner .outline(commit_id, targets, &resolution_options, snapshot, app) .context("Failed to outline")?; Ok(outline_patterns) @@ -835,7 +961,7 @@ impl Repo { &self, commit_id: Oid, targets: &HashSet, - outlining_tree: &OutliningTree, + outliner: &dyn Outliner, cache: &RocksDBCache, snapshot: Option, app: Arc, @@ -922,7 +1048,7 @@ impl Repo { let resolution_options = ResolutionOptions { bazel_resolution_strategy: BazelResolutionStrategy::Incremental, }; - let (outline_patterns, resolution_result) = outlining_tree + let (outline_patterns, resolution_result) = outliner .outline( commit_id, targets, @@ -989,7 +1115,7 @@ impl Repo { .context("Fetching content failed")?; } - let working_tree = self.working_tree().ok_or_else(|| anyhow::anyhow!("Synchronization from the project cache is only possible in a repo with a working tree"))?; + let working_tree = self.working_tree()?; let mut outline_patterns = working_tree.default_working_tree_patterns()?; let mut missing_projects = Vec::<&String>::new(); @@ -1083,7 +1209,7 @@ impl Repo { let working_tree = WorkingTree::new(git2::Repository::open(self.outlining_tree_git_dir())?)?; - let outlining_tree = OutliningTree::new(Arc::new(working_tree)); + let outlining_tree = OutliningTreeOutliner::new(Arc::new(working_tree)); let commit_id = self.get_head_commit()?.id(); outlining_tree.apply_configured_outlining_patterns(commit_id, self.app.clone())?; Ok(()) @@ -1118,13 +1244,22 @@ impl Repo { } /// Get a reference to the repo's outlining tree. - pub fn outlining_tree(&self) -> Option<&OutliningTree> { - self.outlining_tree.as_ref() + pub fn outliner(&self) -> Option> { + self.outliner.clone() + } + + pub fn dense_outlining_tree(&self) -> Result { + todo!("impl") } - /// Get a reference to the repo's working tree. - pub fn working_tree(&self) -> Option<&WorkingTree> { - self.working_tree.as_ref() + /// Get a reference to the working tree + pub fn working_tree(&self) -> Result> { + Ok(self + .working_tree + .as_ref() + .ok_or_else(|| anyhow::anyhow!("Repository has no working tree")) + .context("A working tree is required")? + .clone()) } /// Get a reference to the repo's git dir. @@ -1209,9 +1344,7 @@ impl Repo { /// Set whether preemptive sync is enabled in the Git config. pub fn set_preemptive_sync_enabled(&self, enabled: bool) -> Result<()> { - let working_tree = self - .working_tree() - .ok_or_else(|| anyhow::anyhow!("No working tree"))?; + let working_tree = self.working_tree()?; git_helper::write_config( working_tree.work_dir(), @@ -1249,9 +1382,7 @@ impl Repo { /// Write the configured preemptive sync idle threshold duration. pub fn set_preemptive_sync_idle_threshold(&self, duration: Duration) -> Result<()> { - let working_tree = self - .working_tree() - .ok_or_else(|| anyhow::anyhow!("No working tree"))?; + let working_tree = self.working_tree()?; git_helper::write_config( working_tree.work_dir(), @@ -1299,9 +1430,7 @@ impl Repo { } pub fn set_project_cache_include_header_file(&self, value: &str) -> Result<()> { - let working_tree = self - .working_tree() - .ok_or_else(|| anyhow::anyhow!("No working tree"))?; + let working_tree = self.working_tree()?; git_helper::write_config( working_tree.work_dir(), diff --git a/focus/internals/src/lib/model/selection/selection.rs b/focus/internals/src/lib/model/selection/selection.rs index 476accf2..ff7cb570 100644 --- a/focus/internals/src/lib/model/selection/selection.rs +++ b/focus/internals/src/lib/model/selection/selection.rs @@ -202,9 +202,7 @@ pub struct SelectionManager { impl SelectionManager { pub fn from_repo(repo: &Repo) -> Result { - let working_tree = repo - .working_tree() - .ok_or_else(|| anyhow::anyhow!("The repo must have a working tree"))?; + let working_tree = repo.working_tree()?; let paths = DataPaths::from_working_tree(working_tree)?; let project_catalog = ProjectCatalog::new(&paths)?; Self::new(&paths.selection_file, project_catalog) diff --git a/focus/internals/src/lib/project_cache/mod.rs b/focus/internals/src/lib/project_cache/mod.rs index 0d68d998..95cf7002 100644 --- a/focus/internals/src/lib/project_cache/mod.rs +++ b/focus/internals/src/lib/project_cache/mod.rs @@ -207,11 +207,12 @@ impl<'cache> ProjectCache<'cache> { resolution_options: &ResolutionOptions, snapshot: Option, ) -> anyhow::Result { - let outlining_tree = self + let outliner = self .repo - .outlining_tree() - .ok_or_else(|| anyhow::anyhow!("Missing outlining tree"))?; - let (patterns, _resolution_result) = outlining_tree + .outliner() + .ok_or_else(|| anyhow::anyhow!("No outliner available"))?; + tracing::info!(outliner = outliner.identity()); + let (patterns, _resolution_result) = outliner .outline( commit_id, targets, @@ -403,15 +404,14 @@ impl<'cache> ProjectCache<'cache> { info!(project = ?project_name, "Outlining"); let resolution_options = ResolutionOptions::default(); - if let Ok(patterns) = - self.outline(commit_id, &targets, &resolution_options, snapshot.clone()) - { - // Remove ignored patterns. - let patterns: PatternSet = - patterns.difference(ignored_patterns).cloned().collect(); - Ok(Some(Value::OptionalProjectPatternSet(patterns))) - } else { - Ok(None) + match self.outline(commit_id, &targets, &resolution_options, snapshot.clone()) { + Ok(patterns) => { + // Remove ignored patterns. + let patterns: PatternSet = + patterns.difference(ignored_patterns).cloned().collect(); + Ok(Some(Value::OptionalProjectPatternSet(patterns))) + } + Err(e) => Err(e), } } else { Err(anyhow::anyhow!("Unsupported key type")) diff --git a/focus/internals/src/lib/tracker.rs b/focus/internals/src/lib/tracker.rs index 0b1457b0..d2142bed 100644 --- a/focus/internals/src/lib/tracker.rs +++ b/focus/internals/src/lib/tracker.rs @@ -36,7 +36,8 @@ impl TrackedRepo { pub fn get_or_generate_uuid(repo_path: &Path, app: Arc) -> Result { let repo = Repo::open(repo_path, app)?; - if let Some(working_tree) = repo.working_tree() { + if let Ok(working_tree) = repo.working_tree() { + let working_tree = working_tree.as_ref(); match working_tree.read_uuid() { Ok(Some(uuid)) => Ok(uuid), _ => working_tree.write_generated_uuid(), @@ -214,7 +215,7 @@ impl Tracker { let repo = Repo::open(&canonical_path, app.clone())?; let uuid_from_config = - if let Some(working_tree) = repo.working_tree() { + if let Ok(working_tree) = repo.working_tree() { Ok(working_tree.read_uuid()?) } else { Err("No working tree") diff --git a/focus/migrations/Cargo.toml b/focus/migrations/Cargo.toml index 406de460..925f4c65 100644 --- a/focus/migrations/Cargo.toml +++ b/focus/migrations/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "focus-migrations" -version = "0.7.0" +version = "0.7.1" edition = "2021" [dependencies] diff --git a/focus/operations/Cargo.toml b/focus/operations/Cargo.toml index af1ecb54..73193259 100644 --- a/focus/operations/Cargo.toml +++ b/focus/operations/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "focus-operations" -version = "0.7.0" +version = "0.7.1" edition = "2021" [features] diff --git a/focus/operations/src/clone.rs b/focus/operations/src/clone.rs index 0704cfb2..0a8f5da2 100644 --- a/focus/operations/src/clone.rs +++ b/focus/operations/src/clone.rs @@ -584,7 +584,8 @@ fn set_up_sparse_repo( // N.B. we must re-open the repo because otherwise it has no trees... let repo = Repo::open(sparse_repo_path, app.clone()).context("Failed to open repo")?; // before running rest of setup config worktree view to be filtered - repo.working_tree().unwrap().set_filter_config(true)?; + let working_tree = repo.working_tree()?; + working_tree.set_filter_config(true)?; let head_commit = repo.get_head_commit().context("Resolving head commit")?; let target_set = compute_and_store_initial_selection(&repo, projects_and_targets, template)?; debug!(target_set = ?target_set, "Complete target set"); @@ -605,7 +606,7 @@ fn set_up_sparse_repo( ) .context("Sync failed")?; - repo.working_tree().unwrap().write_sync_point_ref()?; + repo.working_tree()?.write_sync_point_ref()?; info!("Writing git config to support instrumentation"); repo.write_git_config_to_support_instrumentation() @@ -1285,7 +1286,7 @@ mod twttr_test { // Check tree contents { - let outlining_tree = model_repo.outlining_tree().unwrap(); + let outlining_tree = model_repo.outliner().unwrap(); let outlining_tree_underlying = outlining_tree.underlying(); let outlining_tree_path = outlining_tree_underlying.work_dir(); let walker = walkdir::WalkDir::new(outlining_tree_path).follow_links(false); diff --git a/focus/operations/src/detect_build_graph_changes.rs b/focus/operations/src/detect_build_graph_changes.rs index aa62214b..ca0bcac7 100644 --- a/focus/operations/src/detect_build_graph_changes.rs +++ b/focus/operations/src/detect_build_graph_changes.rs @@ -16,14 +16,7 @@ use focus_util::{ fn find_committed_changes(app: Arc, repo_path: &Path) -> Result> { let repo = Repo::open(repo_path, app.clone())?; - let working_tree = { - if let Some(t) = repo.working_tree() { - t - } else { - bail!("No working tree"); - } - }; - + let working_tree = repo.working_tree()?; let sync_state_oid = { if let Some(sync_point) = working_tree .read_sparse_sync_point_ref() diff --git a/focus/operations/src/ensure_clean.rs b/focus/operations/src/ensure_clean.rs index 5fcb9b50..22b16085 100644 --- a/focus/operations/src/ensure_clean.rs +++ b/focus/operations/src/ensure_clean.rs @@ -13,10 +13,7 @@ use super::util::perform; pub fn run(sparse_repo_path: &Path, app: Arc) -> Result<()> { let repo = Repo::open(sparse_repo_path, app.clone()) .with_context(|| format!("Opening repo in {}", sparse_repo_path.display()))?; - let working_tree = match repo.working_tree() { - Some(t) => t, - None => bail!("No working tree"), - }; + let working_tree = repo.working_tree()?; let clean = perform("Checking that sparse repo is in a clean state", || { working_tree.is_clean(app.clone()) diff --git a/focus/operations/src/filter.rs b/focus/operations/src/filter.rs index 4dc2425e..4098ed26 100644 --- a/focus/operations/src/filter.rs +++ b/focus/operations/src/filter.rs @@ -16,7 +16,8 @@ pub fn run( run_sync: bool, ) -> Result { let repo = Repo::open(sparse_repo.as_ref(), app.clone())?; - let prev_filter_value = repo.working_tree().unwrap().get_filter_config()?; + let working_tree = repo.working_tree()?; + let prev_filter_value = working_tree.get_filter_config()?; // no change, no need to update the work tree if prev_filter_value == filter_value { @@ -30,16 +31,14 @@ pub fn run( } // set to the new passed in value - repo.working_tree() - .unwrap() - .set_filter_config(filter_value)?; + working_tree.set_filter_config(filter_value)?; if !filter_value { info!("Turning filter off. Going into unfiltered view."); - repo.working_tree().unwrap().switch_filter_off(app)?; + working_tree.switch_filter_off(app)?; } else { info!("Turning filter on. Going into filtered view."); - repo.working_tree().unwrap().switch_filter_on(app.clone())?; + working_tree.switch_filter_on(app.clone())?; if run_sync { crate::sync::run( &SyncRequest::new(sparse_repo.as_ref(), crate::sync::SyncMode::Incremental), diff --git a/focus/operations/src/status.rs b/focus/operations/src/status.rs index 9f3df8f3..c5675465 100644 --- a/focus/operations/src/status.rs +++ b/focus/operations/src/status.rs @@ -16,7 +16,8 @@ pub fn run( let repo = Repo::open(sparse_repo.as_ref(), app)?; let selections = repo.selection_manager()?; let selection = selections.selection()?; - let is_filter_view = repo.working_tree().unwrap().get_filter_config()?; + let working_tree = repo.working_tree()?; + let is_filter_view = working_tree.get_filter_config()?; eprintln!(); if is_filter_view { diff --git a/focus/operations/src/sync.rs b/focus/operations/src/sync.rs index 1386a329..fae66700 100644 --- a/focus/operations/src/sync.rs +++ b/focus/operations/src/sync.rs @@ -150,8 +150,8 @@ pub struct SyncResult { pub fn run(request: &SyncRequest, app: Arc) -> Result { let repo = Repo::open(request.sparse_repo_path(), app.clone()).context("Failed to open the repo")?; - - if !repo.working_tree().unwrap().get_filter_config()? { + let working_tree = repo.working_tree()?; + if !working_tree.get_filter_config()? { info!("Sync does not run when focus filter is off. Run \"focus filter on\" to turn filter back on."); return Ok(SyncResult { checked_out: false, @@ -298,7 +298,7 @@ pub fn run(request: &SyncRequest, app: Arc) -> Result { }; if preemptive { - if let Some(working_tree) = repo.working_tree() { + if let Ok(working_tree) = repo.working_tree() { if let Ok(Some(sync_point)) = working_tree.read_sparse_sync_point_ref() { if sync_point == commit.id() { // The sync point is already set to this ref. We don't need to bother. @@ -382,9 +382,7 @@ pub fn run(request: &SyncRequest, app: Arc) -> Result { if preemptive { perform("Updating the sync point", || { - repo.working_tree() - .unwrap() - .write_preemptive_sync_point_ref(commit.id()) + working_tree.write_preemptive_sync_point_ref(commit.id()) })?; } else { ti_client @@ -397,7 +395,7 @@ pub fn run(request: &SyncRequest, app: Arc) -> Result { .get_context() .add_to_custom_map("sync_commit_id", commit.id().to_string()); perform("Updating the sync point", || { - repo.working_tree().unwrap().write_sync_point_ref() + working_tree.write_sync_point_ref() })?; // The profile was successfully applied, so do not restore the backup. diff --git a/focus/operations/src/testing/integration.rs b/focus/operations/src/testing/integration.rs index c3216688..a6bc3bee 100644 --- a/focus/operations/src/testing/integration.rs +++ b/focus/operations/src/testing/integration.rs @@ -220,7 +220,7 @@ impl RepoPairFixture { self.dense_repo_path.to_owned(), )]; if let Ok(sparse_repo) = self.sparse_repo() { - if let Some(working_tree) = sparse_repo.working_tree() { + if let Ok(working_tree) = sparse_repo.working_tree() { commands.push(( Command::new("bazel") .arg("shutdown") @@ -229,7 +229,7 @@ impl RepoPairFixture { working_tree.work_dir().to_owned(), )); } - if let Some(outlining_tree) = sparse_repo.outlining_tree() { + if let Some(outlining_tree) = sparse_repo.outliner() { commands.push(( Command::new("bazel") .arg("shutdown") @@ -324,9 +324,9 @@ mod twttr_test { let repo = fixture.dense_repo.repo()?; - assert!( - repo.config()?.get_string("ci.alt.remote")? - == "https://git.example.com/focus-test-repo-ci" + assert_eq!( + repo.config()?.get_string("ci.alt.remote")?, + "https://git.twitter.biz/focus-test-repo-ci" ); assert!(repo.config()?.get_bool("ci.alt.enabled")?); diff --git a/focus/operations/src/testing/repo.rs b/focus/operations/src/testing/repo.rs index 1d77d9a5..39cbba4b 100644 --- a/focus/operations/src/testing/repo.rs +++ b/focus/operations/src/testing/repo.rs @@ -26,7 +26,7 @@ fn repo_register_after_move() -> Result<()> { tracker.ensure_registered(&fixture.sparse_repo_path, fixture.app.clone())?; let repo = fixture.sparse_repo()?; - let working_tree = repo.working_tree().unwrap(); + let working_tree = repo.working_tree()?; let id = { working_tree.read_uuid()?.unwrap() }; { diff --git a/focus/operations/src/testing/sync.rs b/focus/operations/src/testing/sync.rs index fc5c6332..1f56532e 100644 --- a/focus/operations/src/testing/sync.rs +++ b/focus/operations/src/testing/sync.rs @@ -416,7 +416,7 @@ fn clone_contains_top_level_internal(sync_mode: SyncMode) -> Result<()> { fixture.perform_clone()?; let sparse_repo = fixture.sparse_repo()?; - let outlining_tree = sparse_repo.outlining_tree().unwrap(); + let outlining_tree = sparse_repo.outliner().unwrap(); let underlying = outlining_tree.underlying(); let outlining_tree_root = underlying.work_dir(); @@ -469,8 +469,7 @@ fn sync_skips_checkout_with_unchanged_profile_internal( let profile_path = fixture .sparse_repo() .unwrap() - .working_tree() - .unwrap() + .working_tree()? .sparse_checkout_path(); let initial_profile_contents = std::fs::read_to_string(&profile_path)?; @@ -596,7 +595,7 @@ fn sync_configures_working_and_outlining_trees() -> Result<()> { // Check outlining tree let outlining_tree_config = fixture .sparse_repo()? - .outlining_tree() + .outliner() .unwrap() .underlying() .git_repo() diff --git a/focus/operations/src/testing/sync_with_project_cache.rs b/focus/operations/src/testing/sync_with_project_cache.rs index e050e8a3..411078f3 100644 --- a/focus/operations/src/testing/sync_with_project_cache.rs +++ b/focus/operations/src/testing/sync_with_project_cache.rs @@ -1,9 +1,11 @@ // Copyright 2022 Twitter, Inc. // SPDX-License-Identifier: Apache-2.0 +use std::path::Path; + use anyhow::{bail, Context, Result}; use focus_internals::model::repo::PROJECT_CACHE_ENDPOINT_CONFIG_KEY; -use focus_testing::init_logging; +use focus_testing::{init_logging, GitBinary}; use focus_util::{app::ExitCode, git_helper}; use crate::{ @@ -12,22 +14,73 @@ use crate::{ testing::integration::RepoPairFixture, }; +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum Location { + Sparse, + Dense, +} + struct Fixture { underlying: RepoPairFixture, + location: Location, } impl Fixture { - fn new() -> Result { + fn new(location: Location) -> Result { let underlying = RepoPairFixture::new()?; underlying.perform_clone()?; - Ok(Self { underlying }) + Ok(Self { + underlying, + location, + }) + } + + fn repo_path(&self) -> &Path { + self.repo_path_for_location(self.location) + } + + fn repo_path_for_location(&self, location: Location) -> &Path { + match location { + Location::Sparse => self.underlying.sparse_repo_path.as_path(), + Location::Dense => self.underlying.dense_repo_path.as_path(), + } + } + + fn configure_remote(&self, repo_name: &str, location: Location) -> Result<()> { + let git_binary = GitBinary::for_testing()?; + let path = self.repo_path_for_location(location); + tracing::debug!(?path, "Configuring remote"); + let upstream_url = format!("https://example.com/{}", repo_name); + + // Remove existing 'origin' remote if it exists. We allow this to fail. + let _ = git_binary + .command() + .arg("remote") + .arg("remove") + .arg("origin") + .current_dir(path) + .status(); + + if !git_binary + .command() + .arg("remote") + .arg("add") + .arg("origin") + .arg(upstream_url.as_str()) + .current_dir(path) + .status()? + .success() + { + bail!("Adding git remote failed") + } + Ok(()) } - fn configure_endpoint(&self) -> Result<()> { + fn configure_endpoint(&self, location: Location) -> Result<()> { let path = self.underlying.dir.path().join("project_cache_db"); std::fs::create_dir_all(&path)?; git_helper::write_config( - &self.underlying.sparse_repo_path, + self.repo_path_for_location(location), PROJECT_CACHE_ENDPOINT_CONFIG_KEY, format!("file://{}", path.display()).as_str(), self.underlying.app.clone(), @@ -41,7 +94,7 @@ impl Fixture { for shard_index in 0..shard_count { let exit_code = project_cache::push( app.clone(), - &self.underlying.sparse_repo_path, + self.repo_path(), String::from("HEAD"), shard_index, shard_count, @@ -70,8 +123,8 @@ impl Fixture { fn generation() -> Result<()> { init_logging(); - let fixture = Fixture::new()?; - fixture.configure_endpoint()?; + let fixture = Fixture::new(Location::Sparse)?; + fixture.configure_endpoint(Location::Sparse)?; fixture.generate_content(2)?; Ok(()) @@ -81,9 +134,9 @@ fn generation() -> Result<()> { fn project_cache_falls_back_with_non_project_targets_selected() -> Result<()> { init_logging(); - let fixture = Fixture::new()?; + let fixture = Fixture::new(Location::Sparse)?; let app = fixture.underlying.app.clone(); - fixture.configure_endpoint()?; + fixture.configure_endpoint(Location::Sparse)?; // Add a project and a directory target to the selection crate::selection::add( @@ -119,9 +172,9 @@ fn project_cache_falls_back_with_non_project_targets_selected() -> Result<()> { fn project_cache_answers_with_only_projects_selected() -> Result<()> { init_logging(); - let fixture = Fixture::new()?; + let fixture = Fixture::new(Location::Sparse)?; let app = fixture.underlying.app.clone(); - fixture.configure_endpoint()?; + fixture.configure_endpoint(Location::Sparse)?; fixture.generate_content(2)?; // Add a project and a directory target to the selection @@ -146,15 +199,31 @@ fn project_cache_answers_with_only_projects_selected() -> Result<()> { Ok(()) } -#[test] -fn project_cache_generates_all_projects() -> Result<()> { +fn project_cache_generates_all_projects_internal(location: Location) -> Result<()> { init_logging(); - let fixture = Fixture::new()?; + let fixture = Fixture::new(location)?; let app = fixture.underlying.app.clone(); - fixture.configure_endpoint()?; + if location == Location::Dense { + // Also configure sparse since we will query there. + fixture.configure_endpoint(Location::Sparse)?; + } + fixture.configure_endpoint(location)?; + let repo_name = format!( + "project_cache_generates_all_projects_internal_{:?}", + location + ); + fixture + .configure_remote(&repo_name, Location::Dense) + .context("Configuring dense repo")?; + fixture + .configure_remote(&repo_name, Location::Sparse) + .context("Configuring sparse repo")?; + fixture.generate_content(10)?; + tracing::debug!(repo = ?fixture.repo_path()); + let selection_manager = fixture.underlying.sparse_repo()?.selection_manager()?; let project_names: Vec = selection_manager .project_catalog() @@ -184,3 +253,14 @@ fn project_cache_generates_all_projects() -> Result<()> { Ok(()) } + +#[test] +fn project_cache_generates_all_projects_with_sparse_repo() -> Result<()> { + project_cache_generates_all_projects_internal(Location::Sparse) +} + +// #[ignore = "Needs underlying implementation"] +#[test] +fn project_cache_generates_all_projects_with_dense_repo() -> Result<()> { + project_cache_generates_all_projects_internal(Location::Dense) +} diff --git a/focus/platform/Cargo.toml b/focus/platform/Cargo.toml index 4d88d417..5376b007 100644 --- a/focus/platform/Cargo.toml +++ b/focus/platform/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "focus-platform" -version = "0.7.0" +version = "0.7.1" edition = "2021" description = "Platform specific utilities" diff --git a/focus/repo-management/Cargo.toml b/focus/repo-management/Cargo.toml index ab02a584..d8561d94 100644 --- a/focus/repo-management/Cargo.toml +++ b/focus/repo-management/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "focus-repo-management" -version = "0.7.0" +version = "0.7.1" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/focus/testing/Cargo.toml b/focus/testing/Cargo.toml index 87bdd7b2..01db7522 100644 --- a/focus/testing/Cargo.toml +++ b/focus/testing/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "focus-testing" -version = "0.7.0" +version = "0.7.1" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/focus/tracing/Cargo.toml b/focus/tracing/Cargo.toml index 77be5dea..a80605b1 100644 --- a/focus/tracing/Cargo.toml +++ b/focus/tracing/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "focus-tracing" -version = "0.7.0" +version = "0.7.1" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/focus/util/Cargo.toml b/focus/util/Cargo.toml index ef6cbf70..0c4bf054 100644 --- a/focus/util/Cargo.toml +++ b/focus/util/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "focus-util" -version = "0.7.0" +version = "0.7.1" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/tool_insights_client/Cargo.toml b/tool_insights_client/Cargo.toml index 00375c7c..232d3ed2 100644 --- a/tool_insights_client/Cargo.toml +++ b/tool_insights_client/Cargo.toml @@ -5,7 +5,7 @@ Library to instrument tools via the tool insights service. The library, currently, is only useful on MacOS since it relies on the tool insights daemon to upload the logs to tool insights service. """ -version = "0.7.0" +version = "0.7.1" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html