From 98618d5eafa7bd91c8c807363ed802d2df2f0eef Mon Sep 17 00:00:00 2001 From: Austin Gregg-Smith Date: Wed, 19 Aug 2026 19:23:51 +0100 Subject: [PATCH 01/13] Bundle prune/reconcile placement into one ClonePlacement value MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit prune_plan and reconcile_plan took a root and a WorkspaceLocations that had to be derived from the same root, with the agreement held only in prose; a caller pairing a root with locations resolved against a different tree would mis-classify clones — a healthy unrecorded clone whose workspace placed elsewhere reads as orphaned, which is a deletion. ClonePlacement has one constructor that derives both halves from one root, so the mismatched pair has no representation; clone_root and workspace_locations drop to pub(crate) so the parts cannot be paired by hand outside the crate. PrunePlan, PurgePlan and ReconcilePlan fields go private behind accessors for the same reason: a plan is the classifier's answer, and fillable fields let one be assembled from a root and a classification that never met (or a purge count approved for one set with a delete acting on another). No rendered output changes: the accessors hand the renderers the same values the fields held. On the #251 §7 surface both plans stay binary surface; ClonePlacement carries the same note. Refs #263. --- rust/devlaunch-core/src/flows/lifecycle.rs | 186 +++++++++++++++------ rust/devlaunch-core/src/flows/migration.rs | 16 +- rust/dl/src/commands.rs | 22 +-- rust/dl/src/render.rs | 43 ++--- rust/dl/src/session.rs | 4 +- 5 files changed, 173 insertions(+), 98 deletions(-) diff --git a/rust/devlaunch-core/src/flows/lifecycle.rs b/rust/devlaunch-core/src/flows/lifecycle.rs index 9c94ec75..55ea36db 100644 --- a/rust/devlaunch-core/src/flows/lifecycle.rs +++ b/rust/devlaunch-core/src/flows/lifecycle.rs @@ -947,13 +947,27 @@ fn delete_call(workspace_id: &str, insistence: Insistence) -> Call { /// cannot disagree — [`purge_all_data`] deletes exactly `ownership.mine`. #[derive(Debug, Clone, PartialEq, Eq)] pub struct PurgePlan { + // Private like [`PrunePlan`]'s fields: a plan a caller could assemble is a + // count approved for one set and a delete acting on another. /// Everything devlaunch stores on this machine. Removed whole. - pub cache_dir: PathBuf, + cache_dir: PathBuf, /// The workspaces devlaunch made, and the ones it did not. The second half is /// *named* rather than merely excluded from the count: a user who asked for a /// clean slate and gets survivors should learn it while saying no is still an /// option, rather than from a later `dl --ls`. - pub ownership: WorkspaceOwnership, + ownership: WorkspaceOwnership, +} + +impl PurgePlan { + /// The cache directory the purge removes whole. + pub fn cache_dir(&self) -> &Path { + &self.cache_dir + } + + /// The workspaces devlaunch made, and the ones it did not. + pub fn ownership(&self) -> &WorkspaceOwnership { + &self.ownership + } } /// What a purge would do. One `devpod list`, read before anything is destroyed. @@ -1359,7 +1373,7 @@ pub fn site_of(source: &Path, root: &Path) -> SourceSite { /// candidate and stop the command; a source that lands inside a repository's clone /// tree on something with no `.git` in it means the workspace could be opening any /// of *that repository's* clones, and disputes only those. -pub fn workspace_locations(workspaces: &[Workspace], root: &Path) -> WorkspaceLocations { +pub(crate) fn workspace_locations(workspaces: &[Workspace], root: &Path) -> WorkspaceLocations { let mut located = WorkspaceLocations::default(); for workspace in workspaces { let places = match source_places(&workspace.source) { @@ -1743,14 +1757,17 @@ pub struct Kept { /// [`Reclaimable`] instead. #[derive(Debug, Clone, PartialEq, Eq)] pub struct PrunePlan { - pub root: PathBuf, + // Private, all of them: a plan is [`prune_plan`]'s answer, and fields a caller + // could fill would let one be assembled from a root and a classification that + // never met — the mismatch [`ClonePlacement`] exists to make inexpressible. + root: PathBuf, /// Biggest first: the report's job is to be acted on, and "which of these is /// worth reclaiming" is the comparative question. Path breaks ties so two runs /// over an unchanged cache read alike. - pub removing: Vec, - pub keeping: Vec, + removing: Vec, + keeping: Vec, /// Worktree records whose directory is definitively not there any more. - pub stale_records: Vec, + stale_records: Vec, } impl PrunePlan { @@ -1763,6 +1780,26 @@ impl PrunePlan { pub fn freed(&self) -> DiskUsage { disk_usage::total_usage(self.removing.iter().map(|it| it.usage.clone())) } + + /// The directory the plan's candidates were scanned under. + pub fn root(&self) -> &Path { + &self.root + } + + /// The directories this run will remove. + pub fn removing(&self) -> &[Reclaimable] { + &self.removing + } + + /// The directories this run will leave standing, and why. + pub fn keeping(&self) -> &[Kept] { + &self.keeping + } + + /// The records this run will drop for directories already gone. + pub fn stale_records(&self) -> &[WorktreeInfo] { + &self.stale_records + } } /// The directory `--prune` scans, canonicalised once. @@ -1776,11 +1813,45 @@ impl PrunePlan { /// /// Absent is not a failure — a fresh install has no repos directory yet, and /// resolving one that is not there is what says so. -pub fn clone_root(clones: &WorkspaceCloneManager<'_>) -> PathBuf { +pub(crate) fn clone_root(clones: &WorkspaceCloneManager<'_>) -> PathBuf { let repos_dir = clones.repos_dir(); canonical(&repos_dir.to_string_lossy()).unwrap_or_else(|| repos_dir.to_path_buf()) } +/// The tree a maintenance command scans and where devpod's workspaces sit in it, +/// resolved together. +/// +/// [`prune_plan`] and [`reconcile_plan`] classify a directory by joining two +/// facts: the root the candidates are scanned under, and where every live +/// workspace's source resolves *against that same root*. Taken as two parameters +/// the pair could be built from two different roots — and the join would then +/// mis-classify a clone, reading a healthy one whose workspace was placed against +/// the other root as sourced by nobody, which is an orphan, which is a deletion. +/// One constructor derives both halves from one root, so a mismatched pair has no +/// representation. +#[derive(Debug, Clone, PartialEq, Eq)] +// binary surface — not part of the frozen wf API (#251 §7) +pub struct ClonePlacement { + root: PathBuf, + locations: WorkspaceLocations, +} + +impl ClonePlacement { + /// Resolve `workspaces` against the tree `clones` manages (see + /// [`clone_root`]). The only way to build one. + pub fn resolve(clones: &WorkspaceCloneManager<'_>, workspaces: &[Workspace]) -> Self { + let root = clone_root(clones); + let locations = workspace_locations(workspaces, &root); + Self { root, locations } + } + + /// The live workspaces this command cannot place, or nothing when every one + /// of them placed itself. See [`WorkspaceLocations::unlocatable`]. + pub fn unlocatable(&self) -> Option> { + self.locations.unlocatable() + } +} + /// Why a prune could not be carried out. #[derive(Debug)] pub enum PruneError { @@ -1808,11 +1879,11 @@ pub fn prune_plan( clones: &WorkspaceCloneManager<'_>, storage: &MetadataStorage, workspaces: &[Workspace], - locations: &WorkspaceLocations, - root: &Path, + placement: &ClonePlacement, insistence: Insistence, notices: &mut dyn Notices, ) -> Result { + let ClonePlacement { root, locations } = placement; let mut removing: Vec = Vec::new(); let mut keeping: Vec = Vec::new(); let mut cache_notices = Vec::new(); @@ -2231,15 +2302,33 @@ pub struct Unadoptable { /// What `--reconcile` would change, and what it would only report. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ReconcilePlan { - pub root: PathBuf, - pub adopting: Vec, - pub reporting: Vec, + // Private for [`PrunePlan`]'s reason: an adoption list a caller could fill + // would not be the one [`reconcile_plan`]'s contested-in-both-directions + // matching produced. + root: PathBuf, + adopting: Vec, + reporting: Vec, } impl ReconcilePlan { pub fn nothing_to_do(&self) -> bool { self.adopting.is_empty() && self.reporting.is_empty() } + + /// The directory the orphans were placed under. + pub fn root(&self) -> &Path { + &self.root + } + + /// The devpod records this run will re-point. + pub fn adopting(&self) -> &[Adoptable] { + &self.adopting + } + + /// The orphans this run will only name, and why. + pub fn reporting(&self) -> &[Unadoptable] { + &self.reporting + } } /// The workspaces devpod sources inside a repository's clone tree, at no clone. @@ -2287,10 +2376,10 @@ pub fn reconcile_plan( clones: &WorkspaceCloneManager<'_>, storage: &MetadataStorage, workspaces: &[Workspace], - locations: &WorkspaceLocations, - root: &Path, + placement: &ClonePlacement, notices: &mut dyn Notices, ) -> ReconcilePlan { + let ClonePlacement { root, locations } = placement; let orphans = orphaned_workspaces(workspaces, root); let mut cache_notices = Vec::new(); @@ -3011,24 +3100,21 @@ mod tests { ) } - /// The plan `--prune` would print, and the locations it was built from. - fn plan_for(world: &World, insistence: Insistence) -> (PrunePlan, WorkspaceLocations) { + /// The plan `--prune` would print. + fn plan_for(world: &World, insistence: Insistence) -> PrunePlan { let clones = clones_for(&world.repos_dir, &world.devpod); - let root = clone_root(&clones); let mut context = CommandContext::new(&world.devpod); let workspaces = context.workspaces().expect("a listing"); - let locations = workspace_locations(&workspaces, &root); - let plan = prune_plan( + let placement = ClonePlacement::resolve(&clones, &workspaces); + prune_plan( &clones, &world.storage, &workspaces, - &locations, - &root, + &placement, insistence, &mut ignoring(), ) - .expect("a plan"); - (plan, locations) + .expect("a plan") } /// The paths the plan would remove, in the order it would report them. @@ -5475,7 +5561,7 @@ mod tests { #[test] fn the_four_arms_are_classified_from_the_disk_and_from_devpods_own_listing() { let four = four_clones(); - let (plan, _) = plan_for(&four.world, Insistence::NotInsisted); + let plan = plan_for(&four.world, Insistence::NotInsisted); assert_eq!( removing(&plan), @@ -5503,7 +5589,7 @@ mod tests { // Nothing sources it and no record names it, so every rule would call it an // orphan — and it is the copy every clone hardlinks its git objects out of. let four = four_clones(); - let (plan, _) = plan_for(&four.world, Insistence::NotInsisted); + let plan = plan_for(&four.world, Insistence::NotInsisted); let bare = canonical(&four.world.bare.to_string_lossy()).expect("the bare directory"); assert!(!removing(&plan).contains(&bare)); assert!( @@ -5519,7 +5605,7 @@ mod tests { // saying the directory is still in use or that its own records disagree, and // there is nothing for a user to mean by insisting. let four = four_clones(); - let (plan, _) = plan_for(&four.world, Insistence::Insisted); + let plan = plan_for(&four.world, Insistence::Insisted); let mut going = removing(&plan); going.sort(); @@ -5542,7 +5628,7 @@ mod tests { // uncommitted work as for an empty one, and the confirmation cannot say what // it costs. let four = four_clones(); - let (plan, _) = plan_for(&four.world, Insistence::Insisted); + let plan = plan_for(&four.world, Insistence::Insisted); let promotions: Vec<(PathBuf, Promotion)> = plan .removing @@ -5576,14 +5662,14 @@ mod tests { std::fs::create_dir_all(&broken).expect("a directory that is not a clone"); std::fs::write(broken.join("something.txt"), "x\n").expect("a file in it"); - let (kept, _) = plan_for(&world, Insistence::NotInsisted); + let kept = plan_for(&world, Insistence::NotInsisted); assert!(matches!( kept_because(&kept, &broken), KeptBecause::Objected(Objection::CouldNotTell(_)) )); assert!(removing(&kept).is_empty()); - let (forced, _) = plan_for(&world, Insistence::Insisted); + let forced = plan_for(&world, Insistence::Insisted); assert_eq!(removing(&forced), [broken]); } @@ -5597,7 +5683,7 @@ mod tests { std::fs::create_dir_all(&outside).expect("somebody else's directory"); std::os::unix::fs::symlink(&outside, world.repo_dir.join("a-link")).expect("a symlink"); - let (plan, _) = plan_for(&world, Insistence::Insisted); + let plan = plan_for(&world, Insistence::Insisted); assert!(removing(&plan).is_empty()); assert!(plan.keeping.is_empty(), "{:?}", plan.keeping); @@ -5615,19 +5701,17 @@ mod tests { Git::new(&devpod), GitLfs::NotInstalled, ); - let root = clone_root(&clones); let (storage, _) = MetadataStorage::open(dir.path().join("metadata.json")).expect("a store"); let mut context = CommandContext::new(&devpod); let workspaces = context.workspaces().expect("a listing"); - let locations = workspace_locations(&workspaces, &root); + let placement = ClonePlacement::resolve(&clones, &workspaces); let plan = prune_plan( &clones, &storage, &workspaces, - &locations, - &root, + &placement, Insistence::NotInsisted, &mut ignoring(), ) @@ -5648,7 +5732,7 @@ mod tests { std::fs::create_dir_all(&stub).expect("a config-only stub with no .git"); world.devpod.lists(&[listed("stale", &stub)]); - let (plan, _) = plan_for(&world, Insistence::Insisted); + let plan = plan_for(&world, Insistence::Insisted); assert!(removing(&plan).is_empty(), "{:?}", removing(&plan)); assert!(matches!( @@ -5672,7 +5756,7 @@ mod tests { std::fs::create_dir_all(&stub).expect("a stub in the first repository"); world.devpod.lists(&[listed("stale", &stub)]); - let (plan, _) = plan_for(&world, Insistence::Insisted); + let plan = plan_for(&world, Insistence::Insisted); assert_eq!( removing(&plan), @@ -5740,7 +5824,7 @@ mod tests { .expect("an exclude file"); std::fs::write(big.join("payload.bin"), vec![0u8; 2 * 1024 * 1024]).expect("a payload"); - let (plan, _) = plan_for(&world, Insistence::NotInsisted); + let plan = plan_for(&world, Insistence::NotInsisted); assert_eq!(removing(&plan), [big, small]); assert!(plan.freed().known_bytes() > 2 * 1024 * 1024); @@ -5752,7 +5836,7 @@ mod tests { let gone = world.repo_dir.join("r-gone-aaa"); world.record("r-gone-aaa", "gone", &gone); - let (plan, _) = plan_for(&world, Insistence::NotInsisted); + let plan = plan_for(&world, Insistence::NotInsisted); assert_eq!( plan.stale_records @@ -5779,7 +5863,7 @@ mod tests { return; }; - let (plan, _) = plan_for(&world, Insistence::NotInsisted); + let plan = plan_for(&world, Insistence::NotInsisted); assert!(plan.stale_records.is_empty(), "{:?}", plan.stale_records); } @@ -5787,7 +5871,7 @@ mod tests { #[test] fn the_acting_pass_removes_the_directories_and_forgets_their_records() { let mut four = four_clones(); - let (plan, _) = plan_for(&four.world, Insistence::NotInsisted); + let plan = plan_for(&four.world, Insistence::NotInsisted); let clones = clones_for(&four.world.repos_dir, &four.world.devpod); let mut context = CommandContext::new(&four.world.devpod); @@ -5823,7 +5907,7 @@ mod tests { fn work_written_while_the_question_was_open_is_not_destroyed() { // The report a user answered was taken before they answered it. let mut four = four_clones(); - let (plan, _) = plan_for(&four.world, Insistence::NotInsisted); + let plan = plan_for(&four.world, Insistence::NotInsisted); assert_eq!(removing(&plan), [four.orphan_clean.clone()]); std::fs::write(four.orphan_clean.join("just-typed.md"), "half a plan\n") .expect("work written while the question was open"); @@ -5861,7 +5945,7 @@ mod tests { // unsaved work" caught the other case and not this one, and the difference was // somebody's running workspace. let mut four = four_clones(); - let (plan, _) = plan_for(&four.world, Insistence::Insisted); + let plan = plan_for(&four.world, Insistence::Insisted); assert!(removing(&plan).contains(&four.orphan_dirty)); four.world.devpod.lists(&[ listed("referenced", &four.referenced), @@ -5897,7 +5981,7 @@ mod tests { let mut world = World::empty(); let stuck = world.clone_at("r-stuck-aa", "stuck"); let goes = world.clone_at("r-goes-aaa", "goes"); - let (plan, _) = plan_for(&world, Insistence::NotInsisted); + let plan = plan_for(&world, Insistence::NotInsisted); assert_eq!(plan.removing.len(), 2); let Some(_sealed) = refusing_writes(&stuck) else { return; @@ -5939,7 +6023,7 @@ mod tests { #[test] fn the_acting_pass_stops_when_a_workspace_appeared_that_it_cannot_place() { let mut four = four_clones(); - let (plan, _) = plan_for(&four.world, Insistence::NotInsisted); + let plan = plan_for(&four.world, Insistence::NotInsisted); four.world.devpod.lists(&[listed_with( "unreadable", serde_json::json!({ "localFolder": {} }), @@ -5966,7 +6050,7 @@ mod tests { #[test] fn a_second_run_finds_nothing_left_to_do() { let mut four = four_clones(); - let (plan, _) = plan_for(&four.world, Insistence::NotInsisted); + let plan = plan_for(&four.world, Insistence::NotInsisted); let clones = clones_for(&four.world.repos_dir, &four.world.devpod); { let mut context = CommandContext::new(&four.world.devpod); @@ -5981,7 +6065,7 @@ mod tests { } drop(clones); - let (again, _) = plan_for(&four.world, Insistence::NotInsisted); + let again = plan_for(&four.world, Insistence::NotInsisted); assert!(again.nothing_to_do()); } @@ -5991,7 +6075,7 @@ mod tests { // It is the one question whose answer cannot be re-derived from disk, and it is // paid only after a user has said yes to a deletion. let mut four = four_clones(); - let (plan, _) = plan_for(&four.world, Insistence::NotInsisted); + let plan = plan_for(&four.world, Insistence::NotInsisted); let before = four.world.devpod.devpod_argvs().len(); let clones = clones_for(&four.world.repos_dir, &four.world.devpod); let mut context = CommandContext::new(&four.world.devpod); @@ -6064,16 +6148,14 @@ mod tests { /// The plan `--reconcile` would print. fn reconcile_for(world: &World) -> ReconcilePlan { let clones = clones_for(&world.repos_dir, &world.devpod); - let root = clone_root(&clones); let mut context = CommandContext::new(&world.devpod); let workspaces = context.workspaces().expect("a listing"); - let locations = workspace_locations(&workspaces, &root); + let placement = ClonePlacement::resolve(&clones, &workspaces); reconcile_plan( &clones, &world.storage, &workspaces, - &locations, - &root, + &placement, &mut ignoring(), ) } diff --git a/rust/devlaunch-core/src/flows/migration.rs b/rust/devlaunch-core/src/flows/migration.rs index 697a3872..144cbb38 100644 --- a/rust/devlaunch-core/src/flows/migration.rs +++ b/rust/devlaunch-core/src/flows/migration.rs @@ -1211,9 +1211,7 @@ mod tests { fn a_workspace_the_migration_orphans_is_one_reconcile_adopts() { use crate::clients::devpod::{Workspace, WorkspaceSource}; use crate::clients::git::Git; - use crate::flows::lifecycle::{ - LifecycleNotice, canonical, clone_root, reconcile_plan, workspace_locations, - }; + use crate::flows::lifecycle::{ClonePlacement, LifecycleNotice, canonical, reconcile_plan}; use crate::flows::workspace_clone::{GitLfs, WorkspaceCloneManager}; use devlaunch_runner::ProcessRunner; use std::time::Duration; @@ -1268,23 +1266,21 @@ mod tests { context: "default".to_owned(), }]; - let root = clone_root(&clones); - let locations = workspace_locations(&workspaces, &root); + let placement = ClonePlacement::resolve(&clones, &workspaces); let plan = reconcile_plan( &clones, &storage, &workspaces, - &locations, - &root, + &placement, &mut Vec::::new(), ); assert_eq!( - plan.adopting.len(), + plan.adopting().len(), 1, "the migration's orphan is adopted: {plan:?}" ); - let adopted = &plan.adopting[0]; + let adopted = &plan.adopting()[0]; assert_eq!(adopted.workspace_id, orphaned_id); assert_eq!( adopted.clone, @@ -1296,7 +1292,7 @@ mod tests { "carrying the migrated record, whose id join reconcile writes" ); assert!( - plan.reporting.is_empty(), + plan.reporting().is_empty(), "nothing was left unadopted: {plan:?}" ); diff --git a/rust/dl/src/commands.rs b/rust/dl/src/commands.rs index 54b53a45..5e027a05 100644 --- a/rust/dl/src/commands.rs +++ b/rust/dl/src/commands.rs @@ -800,13 +800,12 @@ fn prune_clone_directories( }; report(&records); announce_lock_waits(&mut records); - let root = lifecycle::clone_root(&records.clones); let workspaces = match context.workspaces() { Err(refused) => return Cleanup::Raised(refuse_listing(&refused)), Ok(workspaces) => workspaces, }; - let locations = lifecycle::workspace_locations(&workspaces, &root); - if let Some(unlocatable) = locations.unlocatable() { + let placement = lifecycle::ClonePlacement::resolve(&records.clones, &workspaces); + if let Some(unlocatable) = placement.unlocatable() { print(&render::report_unlocatable( &unlocatable, "--prune", @@ -824,8 +823,7 @@ fn prune_clone_directories( &records.clones, &records.storage, &workspaces, - &locations, - &root, + &placement, insistence, &mut notices, ) { @@ -906,13 +904,12 @@ fn render_reconcile( }; report(&records); announce_lock_waits(&mut records); - let root = lifecycle::clone_root(&records.clones); let workspaces = match context.workspaces() { Err(refused) => return refuse_listing(&refused), Ok(workspaces) => workspaces, }; - let locations = lifecycle::workspace_locations(&workspaces, &root); - if let Some(unlocatable) = locations.unlocatable() { + let placement = lifecycle::ClonePlacement::resolve(&records.clones, &workspaces); + if let Some(unlocatable) = placement.unlocatable() { // `--prune`'s stop, for `--prune`'s reason: a clone that cannot be shown to // be free is one no orphan can be given. print(&render::report_unlocatable( @@ -927,14 +924,13 @@ fn render_reconcile( &records.clones, &records.storage, &workspaces, - &locations, - &root, + &placement, &mut notices, ); say(¬ices); notices.clear(); print(&render::reconcile_plan_lines(&plan)); - if plan.adopting.is_empty() { + if plan.adopting().is_empty() { // Nothing to consent to. A report of workspaces dl will not touch is // already complete, and asking about it would imply an action it has. return Ending::Done; @@ -957,10 +953,10 @@ fn render_reconcile( ); // Read in the plan's order rather than the report's two lists, because that is // the order these lines happened in: one adoption at a time, each either done or - // refused. The report partitions `plan.adopting` — every adoption lands in + // refused. The report partitions the plan's adoptions — every one lands in // exactly one of the lists — so "not refused" is "re-pointed", which is the same // question Python's loop asked of each one. - for adoptable in &plan.adopting { + for adoptable in plan.adopting() { let refused = applied .refused .iter() diff --git a/rust/dl/src/render.rs b/rust/dl/src/render.rs index d25be082..b813c00a 100644 --- a/rust/dl/src/render.rs +++ b/rust/dl/src/render.rs @@ -1116,22 +1116,23 @@ pub(crate) const DOCKER_BOUNDARY: &str = concat!( /// of the count: a user who asked for a clean slate and gets survivors should /// learn it while saying no is still an option. pub(crate) fn purge_plan_lines(plan: &PurgePlan) -> Vec { + let ownership = plan.ownership(); let mut lines = vec![ "This will remove all devlaunch data:".to_owned(), - format!(" - {} DevPod workspace(s)", plan.ownership.mine.len()), + format!(" - {} DevPod workspace(s)", ownership.mine.len()), format!( " - {}/ (workspace clones, repo caches, the shared pixi cache, completions)", - plan.cache_dir.display() + plan.cache_dir().display() ), ]; - if !plan.ownership.foreign.is_empty() { + if !ownership.foreign.is_empty() { lines.push(String::new()); lines.push(format!( "Leaving {} workspace(s) devlaunch did not create:", - plan.ownership.foreign.len() + ownership.foreign.len() )); lines.extend( - plan.ownership + ownership .foreign .iter() .map(|workspace| format!(" - {}", workspace.id)), @@ -1276,16 +1277,16 @@ fn shell_quoted(path: &Path) -> String { /// workspaces, and a clone with no workspace has no row there to appear in. pub(crate) fn prune_plan_lines(plan: &PrunePlan) -> Vec { let mut lines = vec![ - format!("Clone directories under {}:", plan.root.display()), + format!("Clone directories under {}:", plan.root().display()), String::new(), ]; - if !plan.removing.is_empty() { + if !plan.removing().is_empty() { lines.push(format!( "Removing {} that nothing references -- {}:", - plan.removing.len(), + plan.removing().len(), describe_usage(&plan.freed()) )); - for reclaimable in &plan.removing { + for reclaimable in plan.removing() { let mut line = format!( " - {} ({})", reclaimable.path.display(), @@ -1301,9 +1302,9 @@ pub(crate) fn prune_plan_lines(plan: &PrunePlan) -> Vec { } lines.push(String::new()); } - if !plan.keeping.is_empty() { - lines.push(format!("Leaving {}:", plan.keeping.len())); - for kept in &plan.keeping { + if !plan.keeping().is_empty() { + lines.push(format!("Leaving {}:", plan.keeping().len())); + for kept in plan.keeping() { lines.push(format!( " - {}: {}", kept.path.display(), @@ -1312,10 +1313,10 @@ pub(crate) fn prune_plan_lines(plan: &PrunePlan) -> Vec { } lines.push(String::new()); } - if !plan.stale_records.is_empty() { + if !plan.stale_records().is_empty() { lines.push(format!( "Dropping {} record(s) of directories already gone.", - plan.stale_records.len() + plan.stale_records().len() )); lines.push(String::new()); } @@ -1423,13 +1424,13 @@ pub(crate) fn reconcile_plan_lines(plan: &ReconcilePlan) -> Vec { let mut lines = vec![ format!( "devpod workspaces sourced under {} at something that is not a clone:", - plan.root.display() + plan.root().display() ), String::new(), ]; - if !plan.adopting.is_empty() { - lines.push(format!("Re-pointing {}:", plan.adopting.len())); - for adoptable in &plan.adopting { + if !plan.adopting().is_empty() { + lines.push(format!("Re-pointing {}:", plan.adopting().len())); + for adoptable in plan.adopting() { lines.push(format!( " - {}: {} -> {}", adoptable.workspace_id, @@ -1449,12 +1450,12 @@ pub(crate) fn reconcile_plan_lines(plan: &ReconcilePlan) -> Vec { ); lines.push(String::new()); } - if !plan.reporting.is_empty() { + if !plan.reporting().is_empty() { lines.push(format!( "Leaving {}, which dl will not guess at:", - plan.reporting.len() + plan.reporting().len() )); - for unadoptable in &plan.reporting { + for unadoptable in plan.reporting() { lines.push(format!( " - {} ({}): {}", unadoptable.workspace_id, diff --git a/rust/dl/src/session.rs b/rust/dl/src/session.rs index 66925c41..f7c07589 100644 --- a/rust/dl/src/session.rs +++ b/rust/dl/src/session.rs @@ -98,8 +98,8 @@ pub(crate) fn worktree_config() -> Result { /// Holds the manager and the store together because the listing reads both and /// they have to describe the same cache. There is deliberately no second copy of /// the config here: `repos_dir` is what the commands want from it, and the manager -/// is what answers for that (see [`lifecycle::clone_root`]), so a command cannot -/// scan one tree while locking against another. +/// is what answers for that (see [`lifecycle::ClonePlacement`]), so a command +/// cannot scan one tree while locking against another. pub(crate) struct Records<'r> { pub(crate) storage: MetadataStorage, /// The clone manager, which is the one thing that names a record's clone From 3ee301352ed5e5213830429fa87879852a56abb4 Mon Sep 17 00:00:00 2001 From: Austin Gregg-Smith Date: Wed, 19 Aug 2026 19:44:47 +0100 Subject: [PATCH 02/13] Type the core-authored English behind reason: String (B4/B7/B15) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plan #251 §5: no user-facing English composed in devlaunch-core — typed values in core, prose rendered only in the dl binary. Three families of `reason: String` broke that and each hid a defect the type now removes: - B4: the stale-base machinery composed its reasons in core, and two of them interpolated `{:?}` debug renderings of NamePart/String into text a person reads ("\"--evil\" is not a safe Ref name") where frozen Python prints str(ValueError) ("Invalid git ref name: '--evil'"). BranchBase:: Stale, DefaultBranch::Unknown and the fetch notices now carry a typed NotRefreshed (FetchFailed / NoBranchOnRemote / UnsafeName / NoDefaultBranchRecorded); dl renders each arm in Python's words, so the Debug leak is gone and the rendering is restored to Python's bytes. The recorded-default-branch refusal gets its own notice arm (RecordedDefaultBranchUnsafe) because Python warns a different sentence there ("Cannot fetch recorded default branch: …"), which the collapsed RefNotFetched could not say. CloneNotNamed carries the UnsafeName instead of a pre-composed string, restoring Python's wording there too. - B7: Unsaved::CouldNotTell held one string for four distinct causes, with nothing but convention keeping it non-empty. It now carries a CouldNotTell enum (CouldNotLook / GitCouldNotRead / UnpushedNotListed / DirectoryUnknown); describe() derives Python's exact phrasing — a wire payload via --ls --json's couldNotTell, Losses::describe's rule — and every arm opens with its own words, so an empty rendering has no representation. RemovalRefused and Objection carry the typed cause. - B15: RepointFailure::Unreadable collapsed "the OS refused the read" and "the bytes are not JSON" into one string (Python's one except clause); they are two arms now, rendered through the same sentence. ConfigError::Malformed collapsed "not TOML at all" and "TOML with a wrong-typed value" contra divergence row 8's granularity; NotToml and WrongType are classified by a syntax-only toml::Table pass over the same text, so the parser's message bytes are unchanged. BaseRepository's default_branch String used "" as the unrecorded sentinel (Python's falsy read); RecordedDefaultBranch names the two meanings, keeps the wire spelling ("" for unrecorded, byte-for-byte what Python writes), and from_stored is the one place that reads the sentinel. Parity: no divergence row needed — every rendered change restores frozen Python's bytes (the previous {:?} renderings were an unlicensed drift); JSON payloads, config refusal lines and metadata.json bytes are unchanged. Refs #263. --- rust/devlaunch-core/src/domain/config.rs | 68 ++++++-- rust/devlaunch-core/src/domain/model.rs | 63 ++++++- .../src/domain/workspace_state.rs | 111 ++++++++----- .../src/domain/workspace_state/tests.rs | 27 ++- rust/devlaunch-core/src/flows/lifecycle.rs | 43 +++-- rust/devlaunch-core/src/flows/listing.rs | 12 +- rust/devlaunch-core/src/flows/repo_manager.rs | 72 +++++--- .../src/flows/workspace_clone.rs | 154 +++++++++++------- rust/dl/src/render.rs | 71 ++++++-- 9 files changed, 441 insertions(+), 180 deletions(-) diff --git a/rust/devlaunch-core/src/domain/config.rs b/rust/devlaunch-core/src/domain/config.rs index 8df8f802..5016581a 100644 --- a/rust/devlaunch-core/src/domain/config.rs +++ b/rust/devlaunch-core/src/domain/config.rs @@ -50,15 +50,23 @@ pub struct WorktreeConfig { } /// Why the configuration could not be read. +/// +/// Not-TOML and wrong-typed are two arms rather than one `Malformed`, matching +/// the granularity divergence row 8 claims for the refusal: a file that is not +/// TOML at all and a TOML file whose one value has the wrong type are different +/// things to fix, and a caller holding one string could not tell which it had. #[derive(Debug)] pub enum ConfigError { /// This machine names no home directory, so no config path can be built. NoHomeDirectory, /// The file exists but could not be read. Unreadable { path: PathBuf, source: io::Error }, - /// The file is not TOML, or a value is not of the type its key must be. - /// `reason` is the parser's own words, quoted as data. - Malformed { path: PathBuf, reason: String }, + /// The file is not TOML at all. `reason` is the parser's own words, quoted + /// as data. + NotToml { path: PathBuf, reason: String }, + /// TOML, but a value is not of the type its key must be. `reason` is the + /// deserializer's own words, quoted as data. + WrongType { path: PathBuf, reason: String }, } impl From for ConfigError { @@ -116,20 +124,47 @@ pub(crate) fn worktree_config_at( }); } }; - parse_worktree_config(&text, defaults).map_err(|reason| ConfigError::Malformed { - path: path.to_path_buf(), - reason, + parse_worktree_config(&text, defaults).map_err(|refused| match refused { + Malformed::NotToml { reason } => ConfigError::NotToml { + path: path.to_path_buf(), + reason, + }, + Malformed::WrongType { reason } => ConfigError::WrongType { + path: path.to_path_buf(), + reason, + }, }) } +/// Why `text` is not a readable configuration — [`ConfigError`]'s two parse arms +/// before a path is known. +#[derive(Debug, PartialEq, Eq)] +pub(crate) enum Malformed { + /// Not TOML at all. + NotToml { reason: String }, + /// TOML with a value of the wrong type. + WrongType { reason: String }, +} + /// Read the `[worktree]` tables out of `text`, filling in from `defaults`. /// -/// The error is the parser's message; the caller adds the path. +/// The error is the parser's message; the caller adds the path. Which arm it is +/// comes from a syntax-only pass first: a text no [`toml::Table`] can be read +/// from is not TOML, and anything the typed read then refuses about a valid +/// table is a wrong-typed value — the two reads are of the same text, so the +/// message is the same one the single-pass read produced. pub(crate) fn parse_worktree_config( text: &str, defaults: &WorktreeConfig, -) -> Result { - let document: StoredConfig = toml::from_str(text).map_err(|error| error.to_string())?; +) -> Result { + if let Err(error) = text.parse::() { + return Err(Malformed::NotToml { + reason: error.to_string(), + }); + } + let document: StoredConfig = toml::from_str(text).map_err(|error| Malformed::WrongType { + reason: error.to_string(), + })?; let worktree = document.worktree.unwrap_or_default(); let cleanup = worktree.cleanup.unwrap_or_default(); Ok(WorktreeConfig { @@ -348,22 +383,27 @@ mod tests { #[test] fn text_that_is_not_toml_is_refused_with_the_parsers_reason() { - let reason = + let refused = parse_worktree_config("[worktree\nenabled = true", &defaults()).expect_err("not TOML"); - assert!(!reason.is_empty()); + match refused { + Malformed::NotToml { reason } => assert!(!reason.is_empty()), + other => panic!("not TOML at all, got {other:?}"), + } } #[test] fn a_value_of_the_wrong_type_is_refused_rather_than_carried() { // Python accepted this and failed later, in arithmetic; a parse step - // exists so the refusal happens where the value is read. + // exists so the refusal happens where the value is read — and the arm + // says the file is TOML, so the reader knows which half to fix. for text in [ "[worktree]\nfetch_interval = \"soon\"\n", "[worktree]\nenabled = \"yes\"\n", "[worktree.cleanup]\nprune_after_days = -1\n", ] { - parse_worktree_config(text, &defaults()).expect_err(text); + let refused = parse_worktree_config(text, &defaults()).expect_err(text); + assert!(matches!(refused, Malformed::WrongType { .. }), "{text}"); } } @@ -399,7 +439,7 @@ mod tests { let failed = worktree_config_at(&path, &defaults()).expect_err("not TOML"); match failed { - ConfigError::Malformed { + ConfigError::NotToml { path: named, reason, } => { diff --git a/rust/devlaunch-core/src/domain/model.rs b/rust/devlaunch-core/src/domain/model.rs index 248adb75..ee8681ee 100644 --- a/rust/devlaunch-core/src/domain/model.rs +++ b/rust/devlaunch-core/src/domain/model.rs @@ -31,6 +31,59 @@ use serde::{Deserialize, Serialize, Serializer}; /// What `default_branch` is when a stored entry does not say. const DEFAULT_BRANCH: &str = "main"; +/// The stored `default_branch`: a name, or the record not saying. +/// +/// `models.py` holds a plain string read with a falsy test, so `""` was the +/// sentinel for "no default branch recorded" — a second meaning riding in the +/// value domain, representable anywhere the string travelled. The two meanings +/// are two arms here; the wire keeps the sentinel spelling (an unrecorded branch +/// serializes back as `""`, byte for byte what Python writes), and +/// [`RecordedDefaultBranch::from_stored`] is the one place that reads it. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum RecordedDefaultBranch { + /// The recorded name. Non-empty: [`RecordedDefaultBranch::from_stored`] is + /// the constructor every producer goes through. + Named(String), + /// The record does not say. Python's falsy read of `""`. + Unrecorded, +} + +impl RecordedDefaultBranch { + /// Parse the stored spelling: empty is the sentinel and means unrecorded. + pub(crate) fn from_stored(name: String) -> Self { + if name.is_empty() { + Self::Unrecorded + } else { + Self::Named(name) + } + } + + /// The recorded name, or nothing when the record does not say. + pub(crate) fn named(&self) -> Option<&str> { + match self { + Self::Named(name) => Some(name), + Self::Unrecorded => None, + } + } + + /// The stored spelling — the name, or the `""` the wire keeps for + /// unrecorded. What a save writes, and what the one consumer whose Python + /// interpolates the field verbatim reads + /// (`workspace_clone.py`'s start-point fallback). + pub(crate) fn stored_spelling(&self) -> &str { + match self { + Self::Named(name) => name, + Self::Unrecorded => "", + } + } +} + +impl Serialize for RecordedDefaultBranch { + fn serialize(&self, serializer: S) -> Result { + serializer.serialize_str(self.stored_spelling()) + } +} + /// A naive local timestamp, in Python's `datetime.isoformat()` spelling. /// /// Two values in one because the two jobs differ: `at` is what comparisons and @@ -149,7 +202,7 @@ pub(crate) struct BaseRepository { pub(crate) remote_url: String, #[serde(serialize_with = "as_string")] pub(crate) local_path: PathBuf, - pub(crate) default_branch: String, + pub(crate) default_branch: RecordedDefaultBranch, pub(crate) last_fetched: Option, /// The branch names this repository has workspace clones for. pub(crate) worktrees: Vec, @@ -201,7 +254,7 @@ impl BaseRepository { repo: repo.to_owned(), remote_url: remote_url.to_owned(), local_path, - default_branch: DEFAULT_BRANCH.to_owned(), + default_branch: RecordedDefaultBranch::Named(DEFAULT_BRANCH.to_owned()), last_fetched: None, worktrees: Vec::new(), } @@ -216,7 +269,7 @@ impl BaseRepository { repo: stored.repo, remote_url: stored.remote_url, local_path: PathBuf::from(stored.local_path), - default_branch: stored.default_branch, + default_branch: RecordedDefaultBranch::from_stored(stored.default_branch), last_fetched: read_optional_timestamp(stored.last_fetched)?, worktrees: stored.worktrees, }, @@ -400,7 +453,7 @@ mod tests { repo: "test-repo".to_owned(), remote_url: "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/test-owner/test-repo.git".to_owned(), local_path: PathBuf::from("/tmp/repos/test-owner/test-repo"), - default_branch: "main".to_owned(), + default_branch: RecordedDefaultBranch::Named("main".to_owned()), last_fetched: Some(Timestamp::from_civil(civil::datetime( 2024, 1, 1, 12, 0, 0, 0, ))), @@ -526,7 +579,7 @@ mod tests { fn the_defaults_match_the_dataclass_defaults() { let repo = BaseRepository::new("o", "r", "u", PathBuf::from("/p")); - assert_eq!(repo.default_branch, "main"); + assert_eq!(repo.default_branch.named(), Some("main")); assert_eq!(repo.last_fetched, None); assert!(repo.worktrees.is_empty()); assert_eq!( diff --git a/rust/devlaunch-core/src/domain/workspace_state.rs b/rust/devlaunch-core/src/domain/workspace_state.rs index 12699da0..009025ce 100644 --- a/rust/devlaunch-core/src/domain/workspace_state.rs +++ b/rust/devlaunch-core/src/domain/workspace_state.rs @@ -69,7 +69,7 @@ //! //! Ported from `devlaunch/workspace_state.py`. -use std::path::Path; +use std::path::{Path, PathBuf}; use crate::clients::git::{Git, GitAnswer}; @@ -104,13 +104,13 @@ pub enum Unsaved { /// for the same reason [`Unsaved::WouldLose`] does: the files are still on /// disk, and nothing has established that they exist anywhere else. /// - /// The reason is carried rather than reconstructed at the point of printing, - /// because the cause is not guessable from the path: an interrupted delete, a + /// The cause is carried rather than reconstructed at the point of printing, + /// because it is not guessable from the path: an interrupted delete, a /// `.git` written by a container as another user, a truncated gitfile and a /// directory that was never a clone all arrive here and read differently to /// the person who has to decide what to do about it. It names the directory /// it is about, which is the specific thing the shipped bug got wrong. - CouldNotTell(Reason), + CouldNotTell(CouldNotTell), } impl Unsaved { @@ -128,7 +128,7 @@ impl Unsaved { match self { Self::NothingToLose => serde_json::json!({ "nothingToLose": true }), Self::WouldLose(losses) => serde_json::json!({ "wouldLose": losses.describe() }), - Self::CouldNotTell(reason) => serde_json::json!({ "couldNotTell": reason.as_str() }), + Self::CouldNotTell(cause) => serde_json::json!({ "couldNotTell": cause.describe() }), } } } @@ -139,33 +139,63 @@ impl Unsaved { // could not be told — so a `match` costs it nothing and a fourth arm added later // breaks it rather than being read as permission. -/// Why git could not be asked, in words meant to reach a person. +/// Why git could not be asked, one arm per cause. /// -/// Free text, because that is what it carries: git's own stderr, the OS's own -/// message, or — one layer up, in the delete guard — dl's own account of a record -/// it could not read or a directory it could not name. Unlike -/// [`Unsaved::WouldLose`]'s description there is no structural guarantee of -/// non-emptiness, and Python had none either; what there is instead is that every -/// site building one names the *directory* it is about, which is the specific -/// thing the shipped bug got wrong. +/// One `reason: String` held all four before, which collapsed causes that read +/// differently to the person deciding what to do — and nothing but convention +/// kept it non-empty or naming the directory it was about, which is the specific +/// thing the shipped bug got wrong. Each arm here carries the directory (or the +/// workspace, for the one cause with no directory to name) plus the words the +/// failure came with, and [`CouldNotTell::describe`] is derived from them. #[derive(Clone, Debug, PartialEq, Eq)] // binary surface — not part of the frozen wf API (#251 §7) -pub struct Reason(String); - -impl Reason { - /// A reason in whatever words the failure came with. - pub(crate) fn new(text: impl Into) -> Self { - Self(text.into()) - } - - pub(crate) fn as_str(&self) -> &str { - &self.0 - } +pub enum CouldNotTell { + /// dl was stopped before it could look at the directory at all; `error` is + /// the OS's words. + CouldNotLook { clone: PathBuf, error: String }, + /// git could not read the directory as a repository; `reason` is git's words. + GitCouldNotRead { clone: PathBuf, reason: String }, + /// The repository read fine, and listing unpushed commits then refused — + /// broken remote-tracking refs, usually. + UnpushedNotListed { + clone: PathBuf, + branch: String, + reason: String, + }, + /// No directory could be named for this workspace's record at all: the + /// recorded path is unusable and the derivation refused the record's own + /// triple (the `dl rm` guard's arm — see + /// [`crate::flows::listing::unsaved_work_in`]). + DirectoryUnknown { workspace_id: String }, } -impl std::fmt::Display for Reason { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(&self.0) +impl CouldNotTell { + /// Python's phrasing, exactly: this text reaches a person through + /// `dl rm`'s refusal and a tool through `--ls --json`'s `couldNotTell`, + /// so it is a wire payload and not rendering — [`Losses::describe`]'s rule. + /// Every arm opens with words of its own, so it cannot read as empty. + /// + /// binary surface — not part of the frozen wf API (#251 §7) + pub fn describe(&self) -> String { + match self { + Self::CouldNotLook { clone, error } => { + format!("could not look at {}: {error}", clone.display()) + } + Self::GitCouldNotRead { clone, reason } => { + format!("git could not read {}: {reason}", clone.display()) + } + Self::UnpushedNotListed { + clone, + branch, + reason, + } => format!( + "git could not list unpushed commits on {branch} in {}: {reason}", + clone.display() + ), + Self::DirectoryUnknown { workspace_id } => { + format!("could not work out which directory {workspace_id}'s clone is in") + } + } } } @@ -341,10 +371,10 @@ pub(crate) fn read_clone(git: &Git<'_>, clone: &Path) -> CloneState { Err(error) => { return CloneState { branch: None, - unsaved: Unsaved::CouldNotTell(Reason::new(format!( - "could not look at {}: {error}", - clone.display() - ))), + unsaved: Unsaved::CouldNotTell(CouldNotTell::CouldNotLook { + clone: clone.to_path_buf(), + error: error.to_string(), + }), }; } }; @@ -427,11 +457,10 @@ fn unsaved(git: &Git<'_>, clone: &Path, branch: Option<&str>) -> Unsaved { let status = match git.status_porcelain(clone) { GitAnswer::Said(status) => status, GitAnswer::Refused(refused) => { - return Unsaved::CouldNotTell(Reason::new(format!( - "git could not read {}: {}", - clone.display(), - refused.reason() - ))); + return Unsaved::CouldNotTell(CouldNotTell::GitCouldNotRead { + clone: clone.to_path_buf(), + reason: refused.reason().to_owned(), + }); } }; @@ -447,11 +476,11 @@ fn unsaved(git: &Git<'_>, clone: &Path, branch: Option<&str>) -> Unsaved { } } GitAnswer::Refused(refused) => { - return Unsaved::CouldNotTell(Reason::new(format!( - "git could not list unpushed commits on {branch} in {}: {}", - clone.display(), - refused.reason() - ))); + return Unsaved::CouldNotTell(CouldNotTell::UnpushedNotListed { + clone: clone.to_path_buf(), + branch: branch.to_owned(), + reason: refused.reason().to_owned(), + }); } } } diff --git a/rust/devlaunch-core/src/domain/workspace_state/tests.rs b/rust/devlaunch-core/src/domain/workspace_state/tests.rs index bfc8f07c..2fefb215 100644 --- a/rust/devlaunch-core/src/domain/workspace_state/tests.rs +++ b/rust/devlaunch-core/src/domain/workspace_state/tests.rs @@ -203,10 +203,10 @@ fn would_lose(unsaved: &Unsaved) -> String { } } -/// The reason of a `CouldNotTell`, or a failure naming the arm that came back. +/// The description of a `CouldNotTell`, or a failure naming the arm that came back. fn could_not_tell(unsaved: &Unsaved) -> String { match unsaved { - Unsaved::CouldNotTell(reason) => reason.as_str().to_owned(), + Unsaved::CouldNotTell(cause) => cause.describe(), other => panic!("expected a CouldNotTell: {other:?}"), } } @@ -783,10 +783,13 @@ fn each_arm_renders_as_one_key_that_names_it() { r#"{"wouldLose":"1 unpushed commit(s)"}"# ); assert_eq!( - Unsaved::CouldNotTell(Reason::new("git said no")) - .as_json() - .to_string(), - r#"{"couldNotTell":"git said no"}"# + Unsaved::CouldNotTell(CouldNotTell::GitCouldNotRead { + clone: PathBuf::from("/c"), + reason: "git said no".to_owned(), + }) + .as_json() + .to_string(), + r#"{"couldNotTell":"git could not read /c: git said no"}"# ); } @@ -819,7 +822,7 @@ fn every_description_says_something() { for unsaved in [held(&clone), Unsaved::NothingToLose] { match unsaved { Unsaved::WouldLose(losses) => assert!(!losses.describe().is_empty()), - Unsaved::CouldNotTell(reason) => assert!(!reason.as_str().is_empty()), + Unsaved::CouldNotTell(cause) => assert!(!cause.describe().is_empty()), Unsaved::NothingToLose => {} } } @@ -830,14 +833,20 @@ fn two_of_the_three_answers_refuse_a_delete() { // devlaunch#171 in one assertion: "could not tell" refuses exactly as "would // lose" does, and only the first arm is permission. Written as the match a // guard writes rather than against a `may_delete()` helper, which this module - // deliberately does not offer — see the note beside `Reason`. + // deliberately does not offer — see the note above `Unsaved`'s impl. for (unsaved, may_delete) in [ (Unsaved::NothingToLose, true), ( Unsaved::WouldLose(Losses::one(Loss::Unpushed(NonEmpty::one("abc".to_owned())))), false, ), - (Unsaved::CouldNotTell(Reason::new("git said no")), false), + ( + Unsaved::CouldNotTell(CouldNotTell::GitCouldNotRead { + clone: PathBuf::from("/c"), + reason: "git said no".to_owned(), + }), + false, + ), ] { let permitted = match &unsaved { Unsaved::NothingToLose => true, diff --git a/rust/devlaunch-core/src/flows/lifecycle.rs b/rust/devlaunch-core/src/flows/lifecycle.rs index 55ea36db..c6a24347 100644 --- a/rust/devlaunch-core/src/flows/lifecycle.rs +++ b/rust/devlaunch-core/src/flows/lifecycle.rs @@ -67,7 +67,7 @@ use crate::clients::git::Git; use crate::domain::locks::{self, LockError}; use crate::domain::metadata::{self, MetadataStorage, WorktreeFilter}; use crate::domain::model::WorktreeInfo; -use crate::domain::workspace_state::{self, Losses, NonEmpty, Reason, Unsaved}; +use crate::domain::workspace_state::{self, CouldNotTell, Losses, NonEmpty, Unsaved}; use crate::flows::completion_cache; use crate::flows::disk_usage::{self, DiskUsage}; use crate::flows::listing::{ @@ -732,7 +732,7 @@ pub enum RemovalRefused { /// the same refusal and the same way past it (devlaunch#171). CouldNotTell { workspace_id: String, - reason: Reason, + cause: CouldNotTell, }, } @@ -766,9 +766,9 @@ pub fn guard_removal(workspace_id: &str, unsaved: Unsaved, insistence: Insistenc workspace_id: workspace_id.to_owned(), losses, }, - Unsaved::CouldNotTell(reason) => RemovalRefused::CouldNotTell { + Unsaved::CouldNotTell(cause) => RemovalRefused::CouldNotTell { workspace_id: workspace_id.to_owned(), - reason, + cause, }, }; match insistence { @@ -1528,7 +1528,7 @@ pub enum Objection { /// report prints is derived rather than passed along as text. WouldLose(Losses), /// git could not be asked about it, and this is what it said. - CouldNotTell(Reason), + CouldNotTell(CouldNotTell), } /// What removing `unsaved`'s clone would cost, or nothing when it would cost @@ -1540,7 +1540,7 @@ pub fn objection(unsaved: &Unsaved) -> Option { match unsaved { Unsaved::NothingToLose => None, Unsaved::WouldLose(losses) => Some(Objection::WouldLose(losses.clone())), - Unsaved::CouldNotTell(reason) => Some(Objection::CouldNotTell(reason.clone())), + Unsaved::CouldNotTell(cause) => Some(Objection::CouldNotTell(cause.clone())), } } @@ -2524,10 +2524,17 @@ pub fn devpod_workspace_record(devpod_home: &Path, context: &str, workspace_id: } /// Why one devpod record could not be re-pointed. +/// +/// Unreadable and not-JSON are two arms where Python's one `except` clause caught +/// both: an `OSError` and a decode error read the same to its f-string but not to +/// a caller, and one `reason: String` could not say which had happened. Both +/// render through the same sentence, so the split changes no output. #[derive(Debug, Clone, PartialEq, Eq)] pub enum RepointFailure { - /// devpod's record could not be read, or is not JSON. + /// devpod's record's bytes could not be read; `reason` is the OS's words. Unreadable { path: PathBuf, reason: String }, + /// The bytes are not JSON; `reason` is the parser's words. + NotJson { path: PathBuf, reason: String }, /// Not the shape this repair understands. Refusing is the whole of the /// response: a source key dl cannot read is one it cannot safely replace. NotADevpodRecord { path: PathBuf }, @@ -2560,7 +2567,7 @@ pub fn repoint_devpod_source( reason: system_words(&error), })?; let mut record: serde_json::Value = - serde_json::from_str(&text).map_err(|error| RepointFailure::Unreadable { + serde_json::from_str(&text).map_err(|error| RepointFailure::NotJson { path: path.clone(), reason: error.to_string(), })?; @@ -4406,16 +4413,20 @@ mod tests { // devlaunch#171: "could not tell" refuses exactly as "would lose" does. The // files are still on disk and nothing has established that they exist // anywhere else. + let cause = CouldNotTell::GitCouldNotRead { + clone: PathBuf::from("/x"), + reason: "not a repository".to_owned(), + }; let guarded = guard_removal( "ws", - Unsaved::CouldNotTell(Reason::new("git could not read /x")), + Unsaved::CouldNotTell(cause.clone()), Insistence::NotInsisted, ); assert_eq!( guarded, Guarded::Refused(RemovalRefused::CouldNotTell { workspace_id: "ws".to_owned(), - reason: Reason::new("git could not read /x") + cause }) ); } @@ -4427,7 +4438,9 @@ mod tests { Unsaved::WouldLose(Losses::one(workspace_state::Loss::Uncommitted( NonEmpty::one("?? scratch.md".to_owned()), ))), - Unsaved::CouldNotTell(Reason::new("no idea")), + Unsaved::CouldNotTell(CouldNotTell::DirectoryUnknown { + workspace_id: "ws".to_owned(), + }), ] { assert_eq!( guard_removal("ws", unsaved, Insistence::Insisted), @@ -4523,10 +4536,14 @@ mod tests { let unsaved = guard_reads(&world, "r-evil-aaa"); - let Unsaved::CouldNotTell(reason) = &unsaved else { + let Unsaved::CouldNotTell(cause) = &unsaved else { panic!("expected a refusal, got {unsaved:?}"); }; - assert!(reason.as_str().contains("r-evil-aaa"), "{reason}"); + assert!( + cause.describe().contains("r-evil-aaa"), + "{}", + cause.describe() + ); } #[test] diff --git a/rust/devlaunch-core/src/flows/listing.rs b/rust/devlaunch-core/src/flows/listing.rs index 6c61e654..3b528f6d 100644 --- a/rust/devlaunch-core/src/flows/listing.rs +++ b/rust/devlaunch-core/src/flows/listing.rs @@ -63,7 +63,7 @@ use crate::clients::devpod::{self, ContainerState, ListingUnreadable, Workspace, use crate::clients::git::{Git, GitAnswer}; use crate::domain::metadata::MetadataStorage; use crate::domain::model::WorktreeInfo; -use crate::domain::workspace_state::{self, CloneState, NonEmpty, Reason, Unsaved}; +use crate::domain::workspace_state::{self, CloneState, CouldNotTell, NonEmpty, Unsaved}; use crate::flows::disk_usage::{self, DiskUsage}; use crate::runner::Runner; use crate::timing; @@ -799,9 +799,9 @@ pub(crate) fn unsaved_work_in(git: &Git<'_>, view: &DlView<'_>, workspace_id: &s }; match view.clones.clone_path(record) { Some(clone) => workspace_state::holds_unsaved_work(git, &clone), - None => Unsaved::CouldNotTell(Reason::new(format!( - "could not work out which directory {workspace_id}'s clone is in" - ))), + None => Unsaved::CouldNotTell(CouldNotTell::DirectoryUnknown { + workspace_id: workspace_id.to_owned(), + }), } } @@ -2565,8 +2565,8 @@ mod tests { let git = Git::new(&scene.runner); match unsaved_work_in(&git, &view, scene.id("clean")) { - Unsaved::CouldNotTell(reason) => { - assert!(reason.as_str().contains(scene.id("clean")), "{reason}"); + Unsaved::CouldNotTell(CouldNotTell::DirectoryUnknown { workspace_id }) => { + assert_eq!(workspace_id, scene.id("clean")); } other => panic!("expected a refusal, got {other:?}"), } diff --git a/rust/devlaunch-core/src/flows/repo_manager.rs b/rust/devlaunch-core/src/flows/repo_manager.rs index 6cec97a4..5bd72c47 100644 --- a/rust/devlaunch-core/src/flows/repo_manager.rs +++ b/rust/devlaunch-core/src/flows/repo_manager.rs @@ -41,7 +41,7 @@ use std::time::Duration; use crate::clients::git::{self, Failure, Git, GitRefused}; use crate::domain::locks::{self, Contention, LockError, LockGuard}; use crate::domain::metadata::{self, MetadataError, MetadataStorage}; -use crate::domain::model::{BaseRepository, Timestamp}; +use crate::domain::model::{BaseRepository, RecordedDefaultBranch, Timestamp}; use crate::domain::workspace_id::{NamePart, UnsafeName, validate_ref_name}; use crate::domain::workspace_state::NonEmpty; use crate::notices::Notices; @@ -219,14 +219,20 @@ pub enum CacheNotice { owner: String, repo: String, branch: String, - reason: String, + reason: NotRefreshed, }, + /// The *recorded* default branch is not a safe git name, so git was never + /// asked to fetch it. Its own arm rather than a [`CacheNotice::RefNotFetched`] + /// because Python warns a different sentence here: the name was read back + /// from `metadata.json` with no proof, and the line says so rather than + /// echoing a name that cannot safely be interpolated anywhere. + RecordedDefaultBranchUnsafe { refused: UnsafeName }, /// The repository's default branch could not be named, so nothing was /// fetched to cut a new branch from. DefaultBranchUnknown { owner: String, repo: String, - reason: String, + reason: NotRefreshed, }, /// The workspace was prepared from a base nothing refreshed this call. The /// one consequence-stating notice of the whole degraded family: the fetch @@ -237,7 +243,7 @@ pub enum CacheNotice { repo: String, branch: String, base: String, - reason: String, + reason: NotRefreshed, }, /// The cache's git-lfs store could not be filled. Best-effort: the workspace /// falls through to the network phase. @@ -261,12 +267,13 @@ pub enum CacheNotice { /// No clone directory can be named for a record: the recorded path is /// unusable *and* the record's own triple is not a safe one. Named by the /// triple, because that triple is what failed and is the field a hand-edited - /// `metadata.json` would have to be fixed in. + /// `metadata.json` would have to be fixed in; `refused` says which part the + /// derivation judged. CloneNotNamed { owner: String, repo: String, branch: String, - reason: String, + refused: UnsafeName, }, /// Something the metadata store reported while loading under a mutation. Metadata(metadata::Notice), @@ -326,6 +333,31 @@ pub(crate) enum FetchOutcome { Failed { reason: String }, } +/// Why a ref went unrefreshed — the reason inside +/// [`crate::flows::workspace_clone::BranchBase::Stale`] and the fetch notices. +/// +/// A `reason: String` before, and two of its producers *composed* it in core — one +/// interpolating a `{:?}` debug rendering of [`crate::domain::workspace_id::NamePart`] +/// into text a person reads. Typed arms carry the data instead, and the `dl` binary +/// owns the words (#251 §5); only [`NotRefreshed::FetchFailed`] carries text, and +/// that text is git's or the OS's, never this crate's. +#[derive(Debug, Clone, PartialEq, Eq)] +// binary surface — not part of the frozen wf API (#251 §7) +pub enum NotRefreshed { + /// The fetch ran and failed, or could not be run; `reason` is what git or the + /// OS said, carried verbatim from [`FetchOutcome::Failed`]. + FetchFailed { reason: String }, + /// The remote answered: it has no branch by this name to refresh from. + NoBranchOnRemote { branch: String }, + /// The name is not one git can safely be asked about, so nothing was fetched. + /// In practice this is the *recorded* default branch — the one ref on this + /// path that does not arrive inside a validated + /// [`crate::domain::workspace_id::WorkspaceId`]. + UnsafeName(UnsafeName), + /// No default branch is recorded, so there was nothing to name a fetch of. + NoDefaultBranchRecorded, +} + // -------------------------------------------------------------- the token /// Evidence that the per-repo lock for `(owner, repo)` is held right now. @@ -1216,7 +1248,7 @@ impl<'r> RepositoryManager<'r> { repo: repo.to_owned(), remote_url: remote_url.to_owned(), local_path: bare.clone(), - default_branch: self.default_branch_of(&bare), + default_branch: RecordedDefaultBranch::from_stored(self.default_branch_of(&bare)), last_fetched: Some(Timestamp::now()), worktrees: Vec::new(), }; @@ -1248,7 +1280,7 @@ impl<'r> RepositoryManager<'r> { // Read off the adopted clone, not defaulted: a repository whose // default branch is `master` and one this could read nothing at all // from would otherwise get the same answer. - default_branch: self.default_branch_of(bare), + default_branch: RecordedDefaultBranch::from_stored(self.default_branch_of(bare)), last_fetched: Some(Timestamp::now()), worktrees: Vec::new(), }; @@ -1618,9 +1650,9 @@ impl<'r> RepositoryManager<'r> { ) -> String { let _stage = timing::stage(timing::Stage::HostPrep); if let Some(recorded) = self.get_repo(storage, owner, repo, notices) - && !recorded.default_branch.is_empty() + && let Some(named) = recorded.default_branch.named() { - return recorded.default_branch; + return named.to_owned(); } let remote_url = format!("git@github.com:{owner}/{repo}.git"); if let Some(answered) = self.git.ls_remote_symref_head(&remote_url).said() @@ -2198,7 +2230,7 @@ pub(crate) mod tests { assert_eq!(cloned.repo, "repo"); assert_eq!(cloned.remote_url, "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/owner/repo.git"); assert_eq!(cloned.local_path, bare); - assert_eq!(cloned.default_branch, "main"); + assert_eq!(cloned.default_branch.named(), Some("main")); assert!( cloned.last_fetched.is_some(), "the sweep's clock starts here" @@ -2333,7 +2365,8 @@ pub(crate) mod tests { "cloning over it is refused by git and would delete a live cache" ); assert_eq!( - adopted.default_branch, "release/1.0", + adopted.default_branch.named(), + Some("release/1.0"), "read off the adopted clone, prefix stripped rather than split" ); assert_eq!( @@ -3117,7 +3150,7 @@ pub(crate) mod tests { let mut cache = a_cache(); cache.given_bare_clone("owner", "repo"); let mut recorded = cache.given_record("owner", "repo"); - recorded.default_branch = "develop".to_owned(); + recorded.default_branch = RecordedDefaultBranch::Named("develop".to_owned()); cache.storage.add_repository(recorded).expect("recorded"); let fake = FakeGit::new(); let manager = a_manager(&cache, Git::new(&fake)); @@ -3273,7 +3306,7 @@ pub(crate) mod tests { refs.contains(&"refs/heads/feature/test".to_owned()), "{refs:?}" ); - assert_eq!(cloned.default_branch, "main"); + assert_eq!(cloned.default_branch.named(), Some("main")); } #[test] @@ -3342,7 +3375,7 @@ pub(crate) mod tests { let recovered = manager .ensure_repo(&mut storage, "test", "repo", &remote.url, &mut ignoring()) .expect("cloned"); - assert_eq!(recovered.default_branch, "main"); + assert_eq!(recovered.default_branch.named(), Some("main")); assert!(manager.repo_exists("test", "repo")); } @@ -3390,7 +3423,8 @@ pub(crate) mod tests { assert_eq!(adopted.local_path, bare); assert_eq!(adopted.remote_url, remote.url); assert_eq!( - adopted.default_branch, "master", + adopted.default_branch.named(), + Some("master"), "the rebuilt record fell back to `main` instead of reading the clone" ); assert!(storage.get_repository("test", "repo").is_some()); @@ -3413,7 +3447,7 @@ pub(crate) mod tests { assert!(!bare.join("half-written").exists()); assert!(bare.join("HEAD").exists()); - assert_eq!(cloned.default_branch, "main"); + assert_eq!(cloned.default_branch.named(), Some("main")); assert!(refs_of(&bare).contains(&"refs/heads/main".to_owned())); } @@ -3434,7 +3468,7 @@ pub(crate) mod tests { .ensure_repo(&mut storage, "test", "headed", &remote.url, &mut ignoring()) .expect("cloned"); - assert_eq!(cloned.default_branch, branch); + assert_eq!(cloned.default_branch.named(), Some(branch)); // And it names a ref that is really there, which is the property the // equality above is a proxy for. assert!( @@ -3501,7 +3535,7 @@ pub(crate) mod tests { .ensure_repo(&mut storage, "test", "repo", &remote.url, &mut ignoring()) .expect("cloned back"); assert!(manager.repo_exists("test", "repo")); - assert_eq!(recovered.default_branch, "main"); + assert_eq!(recovered.default_branch.named(), Some("main")); } #[test] diff --git a/rust/devlaunch-core/src/flows/workspace_clone.rs b/rust/devlaunch-core/src/flows/workspace_clone.rs index d4b41825..3824ad6f 100644 --- a/rust/devlaunch-core/src/flows/workspace_clone.rs +++ b/rust/devlaunch-core/src/flows/workspace_clone.rs @@ -43,8 +43,9 @@ use super::branch_manager::{ BranchEnsured, BranchError, BranchManager, EnsureBranch, LocalBranch, RemotePush, }; use super::repo_manager::{ - CacheNotice, Cleanup, CloneError, CloneIfMissingError, FetchOutcome, RemoveTreeError, RepoLock, - RepositoryManager, TreeRemoval, WrongRepoLock, clone_dir, remove_tree, + CacheNotice, Cleanup, CloneError, CloneIfMissingError, FetchOutcome, NotRefreshed, + RemoveTreeError, RepoLock, RepositoryManager, TreeRemoval, WrongRepoLock, clone_dir, + remove_tree, }; use crate::clients::git::{self, Git}; use crate::domain::config::WorktreeConfig; @@ -87,7 +88,7 @@ pub(crate) enum BranchBase { /// cache's own `HEAD` when no default branch could even be named, or the /// requested branch itself when its fetch failed. `reason` is why nothing /// refreshed it, carried rather than reconstructed at the print site. - Stale { base: String, reason: String }, + Stale { base: String, reason: NotRefreshed }, } /// The name-or-why of a repository's default branch. @@ -107,7 +108,7 @@ enum DefaultBranch { /// nothing to fetch, and a new branch cut from the bare cache's own `HEAD`. /// The reason is what tells the two apart afterwards, and it is carried rather /// than rebuilt at the print site. - Unknown { reason: String }, + Unknown { reason: NotRefreshed }, } impl DefaultBranch { @@ -458,10 +459,7 @@ impl<'r> WorkspaceCloneManager<'r> { owner: recorded.owner.clone(), repo: recorded.repo.clone(), branch: recorded.branch.clone(), - reason: format!( - "{:?} is not a safe {:?} name", - unsafe_name.name, unsafe_name.part - ), + refused: unsafe_name, }); None } @@ -605,21 +603,12 @@ impl<'r> WorkspaceCloneManager<'r> { .map_err(EnsureBranchError::WrongRepoLock)?; let bare = self.repo_manager.bare_dir(owner, repo); - // The requested branch reaches here proven by a WorkspaceId on the cold - // path, so a refusal is only reachable for the recorded default branch - // below; an unsafe requested ref answers as a failed fetch rather than - // changing what this method returns. - let outcome = self - .repo_manager - .fetch_ref(owner, repo, branch, notices) - .unwrap_or_else(|unsafe_name| FetchOutcome::Failed { - reason: unsafe_reason(&unsafe_name), - }); + let outcome = self.repo_manager.fetch_ref(owner, repo, branch, notices); let default = self.resolve_default_branch(storage, owner, repo, notices); let base = match outcome { - FetchOutcome::Updated => BranchBase::Fresh, - FetchOutcome::RefMissingOnRemote => match &default { + Ok(FetchOutcome::Updated) => BranchBase::Fresh, + Ok(FetchOutcome::RefMissingOnRemote) => match &default { // The remote answered, so the branch really is new: base it on the // default branch, and fetch *that* so it is based on something // current. Whatever this second fetch answers, the branch creation @@ -637,16 +626,35 @@ impl<'r> WorkspaceCloneManager<'r> { // Not an error here: a cached branch still launches. Deliberately no // default-branch fetch — nothing was learned about the remote, so // nothing licenses treating this branch as new. - FetchOutcome::Failed { reason } => { + Ok(FetchOutcome::Failed { reason }) => { notices.say(CacheNotice::RefNotFetched { owner: owner.to_owned(), repo: repo.to_owned(), branch: branch.to_owned(), - reason: reason.clone(), + reason: NotRefreshed::FetchFailed { + reason: reason.clone(), + }, }); BranchBase::Stale { base: branch.to_owned(), - reason, + reason: NotRefreshed::FetchFailed { reason }, + } + } + // The requested branch reaches here proven by a WorkspaceId on the + // cold path, so this arm is not reachable from it; kept total because + // the proof is the caller's, not this method's to assume. An unsafe + // requested ref answers as a failed fetch rather than changing what + // this method returns. + Err(unsafe_name) => { + notices.say(CacheNotice::RefNotFetched { + owner: owner.to_owned(), + repo: repo.to_owned(), + branch: branch.to_owned(), + reason: NotRefreshed::UnsafeName(unsafe_name.clone()), + }); + BranchBase::Stale { + base: branch.to_owned(), + reason: NotRefreshed::UnsafeName(unsafe_name), } } }; @@ -682,13 +690,14 @@ impl<'r> WorkspaceCloneManager<'r> { // because the resolver's fallback is not this method's to assume: an // empty name has to mean "no default branch" here rather than // downstream, where it would read as a branch. - let reason = "no default branch is recorded".to_owned(); notices.say(CacheNotice::DefaultBranchUnknown { owner: owner.to_owned(), repo: repo.to_owned(), - reason: reason.clone(), + reason: NotRefreshed::NoDefaultBranchRecorded, }); - return DefaultBranch::Unknown { reason }; + return DefaultBranch::Unknown { + reason: NotRefreshed::NoDefaultBranchRecorded, + }; } DefaultBranch::Named(name) } @@ -715,16 +724,12 @@ impl<'r> WorkspaceCloneManager<'r> { { Ok(outcome) => outcome, Err(unsafe_name) => { - let reason = unsafe_reason(&unsafe_name); - notices.say(CacheNotice::RefNotFetched { - owner: owner.to_owned(), - repo: repo.to_owned(), - branch: default_branch.to_owned(), - reason: reason.clone(), + notices.say(CacheNotice::RecordedDefaultBranchUnsafe { + refused: unsafe_name.clone(), }); return BranchBase::Stale { base: default_branch.to_owned(), - reason, + reason: NotRefreshed::UnsafeName(unsafe_name), }; } }; @@ -736,7 +741,9 @@ impl<'r> WorkspaceCloneManager<'r> { // stale here: nothing fetched backs it. FetchOutcome::RefMissingOnRemote => BranchBase::Stale { base: default_branch.to_owned(), - reason: format!("the remote has no branch '{default_branch}' to refresh from"), + reason: NotRefreshed::NoBranchOnRemote { + branch: default_branch.to_owned(), + }, }, // Noticed here as well as reported, because the reason is carried // precisely so it can be printed: the new branch is about to be cut @@ -746,11 +753,13 @@ impl<'r> WorkspaceCloneManager<'r> { owner: owner.to_owned(), repo: repo.to_owned(), branch: default_branch.to_owned(), - reason: reason.clone(), + reason: NotRefreshed::FetchFailed { + reason: reason.clone(), + }, }); BranchBase::Stale { base: default_branch.to_owned(), - reason, + reason: NotRefreshed::FetchFailed { reason }, } } } @@ -897,7 +906,10 @@ impl<'r> WorkspaceCloneManager<'r> { let default_branch = self .repo_manager .get_repo(storage, owner, repo, notices) - .map(|recorded| recorded.default_branch) + // The stored spelling, as Python reads the field here — an + // unrecorded branch stays the wire's `""` rather than + // borrowing the absent-record fallback below. + .map(|recorded| recorded.default_branch.stored_spelling().to_owned()) .unwrap_or_else(|| FALLBACK_DEFAULT_BRANCH.to_owned()); if !self.remote_ref_exists(ws_path, &default_branch, ORIGIN)? { return Err(PrepareWorkspaceError::NoStartPoint { @@ -1313,15 +1325,6 @@ fn say_branch( } } -/// An unsafe name as a reason a caller can carry. -/// -/// The refusal's own data — which part, and what the name was — rendered as one -/// string because it lands in a [`BranchBase::Stale`] beside git's own words, -/// where the *shape* has to be uniform even though the sources are not. -fn unsafe_reason(refused: &UnsafeName) -> String { - format!("{:?} is not a safe {:?} name", refused.name, refused.part) -} - #[cfg(test)] mod tests { //! `test/test_workspace_clone.py`, `test/test_cold_launch_fetches.py`, the @@ -1347,7 +1350,7 @@ mod tests { use super::*; use crate::domain::locks; - use crate::domain::model::Timestamp; + use crate::domain::model::{RecordedDefaultBranch, Timestamp}; use crate::flows::repo_manager::{ Cleanup, RemoveTreeError, bare_dir, clone_dir, repo_dir, repo_lock_path, tests::{ @@ -1646,7 +1649,10 @@ mod tests { &mut cache.storage, "owner", "repo", - &named.default_branch, + named + .default_branch + .named() + .expect("a named default branch"), REMOTE_URL, &mut ignoring(), ) @@ -2135,7 +2141,9 @@ mod tests { base.expect("ensured"), BranchBase::Stale { base: "main".to_owned(), - reason: "the remote has no branch 'main' to refresh from".to_owned(), + reason: NotRefreshed::NoBranchOnRemote { + branch: "main".to_owned() + }, } ); } @@ -2196,7 +2204,9 @@ mod tests { base.expect("ensured"), BranchBase::Stale { base: "main".to_owned(), - reason: "fatal: no such host".to_owned(), + reason: NotRefreshed::FetchFailed { + reason: "fatal: no such host".to_owned() + }, } ); assert_eq!( @@ -2216,7 +2226,9 @@ mod tests { owner: "owner".to_owned(), repo: "repo".to_owned(), branch: "main".to_owned(), - reason: "fatal: no such host".to_owned(), + reason: NotRefreshed::FetchFailed { + reason: "fatal: no such host".to_owned() + }, }, CacheNotice::BranchAlreadyBothSides { branch: "newbranch".to_owned(), @@ -2249,7 +2261,11 @@ mod tests { match base.expect("ensured") { BranchBase::Stale { base, reason } => { assert_eq!(base, "newbranch"); - assert!(reason.contains("Could not read from remote repository")); + assert!(matches!( + reason, + NotRefreshed::FetchFailed { ref reason } + if reason.contains("Could not read from remote repository") + )); } other => panic!("a stale base, got {other:?}"), } @@ -2288,7 +2304,7 @@ mod tests { .get_repository("owner", "repo") .expect("a record") .clone(); - recorded.default_branch = "--upload-pack=evil".to_owned(); + recorded.default_branch = RecordedDefaultBranch::Named("--upload-pack=evil".to_owned()); cache.storage.add_repository(recorded).expect("recorded"); let fake = FakeGit::new().with_script( ["git", "fetch"], @@ -2304,7 +2320,16 @@ mod tests { match base.expect("ensured, not raised") { BranchBase::Stale { base, reason } => { assert_eq!(base, "--upload-pack=evil"); - assert!(reason.contains("not a safe"), "{reason}"); + assert!( + matches!( + reason, + NotRefreshed::UnsafeName(UnsafeName { + part: NamePart::Ref, + ref name, + }) if name == "--upload-pack=evil" + ), + "{reason:?}" + ); } other => panic!("a stale base, got {other:?}"), } @@ -2313,7 +2338,7 @@ mod tests { notices.as_slice(), [ CacheNotice::FetchingRef { .. }, - CacheNotice::RefNotFetched { .. }, + CacheNotice::RecordedDefaultBranchUnsafe { .. }, CacheNotice::BranchAlreadyBothSides { .. } ] ), @@ -2378,7 +2403,7 @@ mod tests { ); assert_eq!( DefaultBranch::Unknown { - reason: "no default branch is recorded".to_owned() + reason: NotRefreshed::NoDefaultBranchRecorded } .start_point(), "HEAD", @@ -2433,7 +2458,9 @@ mod tests { repo: "repo".to_owned(), branch: "nb4".to_owned(), base: "main".to_owned(), - reason: "fatal: no such host".to_owned(), + reason: NotRefreshed::FetchFailed { + reason: "fatal: no such host".to_owned() + }, }], "one line for the whole degraded family, not one per failed fetch" ); @@ -3171,7 +3198,10 @@ mod tests { owner: "owner".to_owned(), repo: "repo".to_owned(), branch: "--evil".to_owned(), - reason: "\"--evil\" is not a safe Ref name".to_owned(), + refused: UnsafeName { + part: NamePart::Ref, + name: "--evil".to_owned(), + }, }], "named by the triple the derivation refused, which is the field to fix" ); @@ -3716,7 +3746,13 @@ mod tests { match &prepared.base { BranchBase::Stale { base, reason } => { assert_eq!(base, "main"); - assert!(!reason.is_empty(), "git said something about why"); + assert!( + matches!( + reason, + NotRefreshed::FetchFailed { reason } if !reason.is_empty() + ), + "git said something about why: {reason:?}" + ); } other => panic!("a stale base, got {other:?}"), } diff --git a/rust/dl/src/render.rs b/rust/dl/src/render.rs index b813c00a..652c93d0 100644 --- a/rust/dl/src/render.rs +++ b/rust/dl/src/render.rs @@ -31,8 +31,8 @@ use devlaunch_core::flows::listing::{LastUsed, SizeCell, Sizes, TableRow, Worksp use devlaunch_core::flows::migration::{Listing, MigrationReport}; use devlaunch_core::flows::provision::{BundleFailed, FailureLevel, ProvisionEvent}; use devlaunch_core::flows::repo_manager::{ - CacheNotice, Cleanup, CloneError, EnsureRepoError, Refusal, RefusalReason, RemoveTreeError, - WrongRepoLock, + CacheNotice, Cleanup, CloneError, EnsureRepoError, NotRefreshed, Refusal, RefusalReason, + RemoveTreeError, WrongRepoLock, }; use devlaunch_core::flows::workspace_clone::{ EnsureBranchError, PrepareColdError, PrepareWorkspaceError, RemoveWorkspaceError, @@ -750,7 +750,10 @@ pub(crate) fn config_error(error: &config::ConfigError) -> String { config::ConfigError::Unreadable { path, source } => { format!("could not read {} ({source})", path.display()) } - config::ConfigError::Malformed { path, reason } => { + // One sentence for both parse arms: the reason already says whether the + // parser or the typed read refused, and the arms exist for callers. + config::ConfigError::NotToml { path, reason } + | config::ConfigError::WrongType { path, reason } => { format!("{} is not usable: {reason}", path.display()) } } @@ -954,9 +957,24 @@ fn cache_notice(notice: &CacheNotice) -> Option { repo, branch, reason, - } => format!("Could not fetch {branch} for {owner}/{repo}: {reason}"), + } => format!( + "Could not fetch {branch} for {owner}/{repo}: {}", + not_refreshed(reason) + ), + // Python's own sentence for this site (`workspace_clone.py`'s "Cannot + // fetch recorded default branch: {e}"): the name came out of + // `metadata.json` with no proof, and the line says so. + CacheNotice::RecordedDefaultBranchUnsafe { refused } => { + format!( + "Cannot fetch recorded default branch: {}", + unsafe_name(refused) + ) + } CacheNotice::DefaultBranchUnknown { reason, .. } => { - format!("Failed to resolve default branch: {reason}") + format!( + "Failed to resolve default branch: {}", + not_refreshed(reason) + ) } CacheNotice::PreparedFromStaleBase { owner, @@ -966,7 +984,8 @@ fn cache_notice(notice: &CacheNotice) -> Option { reason, } => format!( "Prepared '{owner}/{repo}@{branch}' from the cache's '{base}', which could not be \ - refreshed ({reason}); it may be behind the remote." + refreshed ({}); it may be behind the remote.", + not_refreshed(reason) ), CacheNotice::LfsCacheNotFilled { reason } => { format!("Could not fill the cache's git-lfs store: {reason}") @@ -996,12 +1015,33 @@ fn cache_notice(notice: &CacheNotice) -> Option { owner, repo, branch, - reason, - } => format!("cannot name the clone directory for {owner}/{repo}@{branch}: {reason}"), + refused, + } => format!( + "cannot name the clone directory for {owner}/{repo}@{branch}: {}", + unsafe_name(refused) + ), CacheNotice::Metadata(notice) => metadata_notice(notice), }) } +/// Why nothing refreshed a ref, as the clause inside the fetch warnings and the +/// stale-base report. +/// +/// Every arm renders the words Python put there: git's own for a failed fetch, +/// and `str(ValueError)` — [`unsafe_name`]'s sentence — for a name git was never +/// asked about (`workspace_clone.py`'s `_fetch_base_branch` interpolates exactly +/// that into its `StaleBase.reason`). +fn not_refreshed(reason: &NotRefreshed) -> String { + match reason { + NotRefreshed::FetchFailed { reason } => reason.clone(), + NotRefreshed::NoBranchOnRemote { branch } => { + format!("the remote has no branch '{branch}' to refresh from") + } + NotRefreshed::UnsafeName(refused) => unsafe_name(refused), + NotRefreshed::NoDefaultBranchRecorded => "no default branch is recorded".to_owned(), + } +} + /// Why a workspace clone directory is still on disk. /// /// The reason phrase Python interpolated its `Exception` into (`dl.py`:4122). The @@ -1067,10 +1107,11 @@ pub(crate) fn removal_refusal(refused: &RemovalRefused, spec: &str) -> String { ), RemovalRefused::CouldNotTell { workspace_id, - reason, + cause, } => format!( - "{workspace_id}: {reason}. devlaunch will not delete a clone it cannot check. Look at \ - it, or run: dl {spec} rm --force" + "{workspace_id}: {}. devlaunch will not delete a clone it cannot check. Look at it, \ + or run: dl {spec} rm --force", + cause.describe() ), } } @@ -1352,8 +1393,8 @@ fn kept_because(because: &KeptBecause) -> String { fn objection(objected: &Objection) -> String { match objected { Objection::WouldLose(losses) => losses.describe(), - Objection::CouldNotTell(reason) => { - format!("work git could not be asked about ({reason})") + Objection::CouldNotTell(cause) => { + format!("work git could not be asked about ({})", cause.describe()) } } } @@ -1501,7 +1542,9 @@ fn not_adopted(because: &NotAdopted) -> String { /// Why one devpod record could not be re-pointed. pub(crate) fn repoint_failure(failure: &RepointFailure) -> String { match failure { - RepointFailure::Unreadable { path, reason } => { + // One sentence for both, as Python's one `except (OSError, + // json.JSONDecodeError)` wrote it; the arms differ so a caller can. + RepointFailure::Unreadable { path, reason } | RepointFailure::NotJson { path, reason } => { format!("could not read {}: {reason}", path.display()) } RepointFailure::NotADevpodRecord { path } => format!( From fe99d520ea290416dddf06eaaa4ce1186f87eb8f Mon Sep 17 00:00:00 2001 From: Austin Gregg-Smith Date: Wed, 19 Aug 2026 19:48:41 +0100 Subject: [PATCH 03/13] One JsonKind, in the json leaf, instead of two identical enums MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit clients::devpod and domain::metadata each declared a verbatim copy of JsonKind, which is the duplicated-enum smell: two sources of truth for one vocabulary, kept aligned only by nobody touching either — and downstream it forced the dl binary to hold two identical renderers of the Python type names (json_type_name beside metadata_type_name). The enum now lives once in the json leaf module, which sits below every layer that names a kind; both boundaries use it, and dl renders it through one function. The module goes pub for this one type; the Python-spelling writers in it stay pub(crate). Rendered output is unchanged: both renderers already produced the same six words. Refs #263. --- rust/devlaunch-core/src/clients/devpod.rs | 28 +----------------- rust/devlaunch-core/src/domain/metadata.rs | 25 +--------------- rust/devlaunch-core/src/json.rs | 32 ++++++++++++++++++++ rust/devlaunch-core/src/lib.rs | 6 +++- rust/dl/src/render.rs | 34 ++++++++-------------- 5 files changed, 51 insertions(+), 74 deletions(-) diff --git a/rust/devlaunch-core/src/clients/devpod.rs b/rust/devlaunch-core/src/clients/devpod.rs index 4d7bb423..7c970173 100644 --- a/rust/devlaunch-core/src/clients/devpod.rs +++ b/rust/devlaunch-core/src/clients/devpod.rs @@ -56,6 +56,7 @@ use std::collections::{BTreeMap, BTreeSet}; use std::path::PathBuf; use std::time::Duration; +use crate::json::JsonKind; use crate::runner::{ CapturedText, EnvSpec, Exit, Invocation, OsFailure, Outcome, Runner, SpawnSpec, StdinPlan, }; @@ -540,33 +541,6 @@ pub(crate) fn parse_status(output: &str) -> Result Self { - match value { - serde_json::Value::Null => Self::Null, - serde_json::Value::Bool(_) => Self::Bool, - serde_json::Value::Number(_) => Self::Number, - serde_json::Value::String(_) => Self::String, - serde_json::Value::Array(_) => Self::Array, - serde_json::Value::Object(_) => Self::Object, - } - } -} - /// A devpod workspace, as `devpod list --output json` lists it. /// /// No container state, deliberately: real devpod answers state only to `status`, diff --git a/rust/devlaunch-core/src/domain/metadata.rs b/rust/devlaunch-core/src/domain/metadata.rs index bda3c9c5..f6064a15 100644 --- a/rust/devlaunch-core/src/domain/metadata.rs +++ b/rust/devlaunch-core/src/domain/metadata.rs @@ -42,6 +42,7 @@ use indexmap::IndexMap; use serde::Serialize; use serde_json::Value; +use crate::json::JsonKind; use crate::runner::interrupt; use super::locks::{LockError, WaitStarted, hold_lock_watching}; @@ -91,30 +92,6 @@ impl Section { } } -/// The JSON type something turned out to be, where a report names it. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum JsonKind { - Null, - Bool, - Number, - String, - Array, - Object, -} - -impl JsonKind { - fn of(value: &Value) -> Self { - match value { - Value::Null => JsonKind::Null, - Value::Bool(_) => JsonKind::Bool, - Value::Number(_) => JsonKind::Number, - Value::String(_) => JsonKind::String, - Value::Array(_) => JsonKind::Array, - Value::Object(_) => JsonKind::Object, - } - } -} - /// What the OS said when a step failed, as data. /// /// The kind is what code branches on; the message is the OS's own words, quoted diff --git a/rust/devlaunch-core/src/json.rs b/rust/devlaunch-core/src/json.rs index 2111a666..e5eb3557 100644 --- a/rust/devlaunch-core/src/json.rs +++ b/rust/devlaunch-core/src/json.rs @@ -17,6 +17,38 @@ use std::io; use serde::Serialize; +/// The JSON type a value turned out to be, where a typed refusal names it. +/// +/// Data for a report, not a sentence: Python's messages name the Python type +/// (`dict`, `str`, `NoneType`), which is a spelling the `dl` binary chooses. +/// One copy here rather than one per boundary — `devpod`'s listing and +/// `metadata`'s store both name kinds, and two identical enums forced the +/// binary to keep two identical renderers. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +// binary surface — not part of the frozen wf API (#251 §7) +pub enum JsonKind { + Null, + Bool, + Number, + String, + Array, + Object, +} + +impl JsonKind { + /// Which kind `value` is. + pub(crate) fn of(value: &serde_json::Value) -> Self { + match value { + serde_json::Value::Null => Self::Null, + serde_json::Value::Bool(_) => Self::Bool, + serde_json::Value::Number(_) => Self::Number, + serde_json::Value::String(_) => Self::String, + serde_json::Value::Array(_) => Self::Array, + serde_json::Value::Object(_) => Self::Object, + } + } +} + /// A JSON value spelled the way Python's `json.dumps` spells it. /// /// Three surfaces need it and all three are compared byte for byte against the diff --git a/rust/devlaunch-core/src/lib.rs b/rust/devlaunch-core/src/lib.rs index 7e362bf4..02a8e5c8 100644 --- a/rust/devlaunch-core/src/lib.rs +++ b/rust/devlaunch-core/src/lib.rs @@ -61,8 +61,12 @@ pub(crate) mod osext; // Leaf like `runner`: the env-gated span registry everything above may use // (even `locks` spans a contended wait), depending on nothing itself. // +// `json` is pub only for [`json::JsonKind`], which the typed refusals above +// carry and the `dl` binary renders; the Python-spelling writers stay +// `pub(crate)`. +// // binary surface — not part of the frozen wf API (#251 §7) -pub(crate) mod json; +pub mod json; pub mod timing; // Also a leaf, and for the same reason: the sink every flow reports its notices diff --git a/rust/dl/src/render.rs b/rust/dl/src/render.rs index 652c93d0..579b45e4 100644 --- a/rust/dl/src/render.rs +++ b/rust/dl/src/render.rs @@ -9,7 +9,7 @@ use std::fmt::Write as _; use std::io; use std::path::Path; -use devlaunch_core::clients::devpod::{self, ListingUnreadable, NotAListing, NotRun}; +use devlaunch_core::clients::devpod::{ListingUnreadable, NotAListing, NotRun}; use devlaunch_core::clients::gh::{GhEvent, GhUnavailable}; use devlaunch_core::clients::ssh::{NotRun as SshNotRun, UnsafeRequest}; use devlaunch_core::domain::config; @@ -37,6 +37,7 @@ use devlaunch_core::flows::repo_manager::{ use devlaunch_core::flows::workspace_clone::{ EnsureBranchError, PrepareColdError, PrepareWorkspaceError, RemoveWorkspaceError, }; +use devlaunch_core::json::JsonKind; use devlaunch_core::notices::Notices; use devlaunch_runner::{Exit, OsFailure}; use serde_json::Value; @@ -338,25 +339,14 @@ pub fn python_repr(text: &str) -> String { /// `Number` answers `int`, because that is what every number in a document dl /// reads is; a float would be reported as `int` here, which is a wording /// difference in a message only a malformed devpod can provoke. -fn json_type_name(kind: devpod::JsonKind) -> &'static str { +fn json_type_name(kind: JsonKind) -> &'static str { match kind { - devpod::JsonKind::Null => "NoneType", - devpod::JsonKind::Bool => "bool", - devpod::JsonKind::Number => "int", - devpod::JsonKind::String => "str", - devpod::JsonKind::Array => "list", - devpod::JsonKind::Object => "dict", - } -} - -fn metadata_type_name(kind: metadata::JsonKind) -> &'static str { - match kind { - metadata::JsonKind::Null => "NoneType", - metadata::JsonKind::Bool => "bool", - metadata::JsonKind::Number => "int", - metadata::JsonKind::String => "str", - metadata::JsonKind::Array => "list", - metadata::JsonKind::Object => "dict", + JsonKind::Null => "NoneType", + JsonKind::Bool => "bool", + JsonKind::Number => "int", + JsonKind::String => "str", + JsonKind::Array => "list", + JsonKind::Object => "dict", } } @@ -504,7 +494,7 @@ fn metadata_notice(notice: &metadata::Notice) -> String { metadata::FileProblem::NotAnObject { found } => format!( "metadata file {} is not a JSON object (found {})", path.display(), - metadata_type_name(*found) + json_type_name(*found) ), }; match quarantine { @@ -543,7 +533,7 @@ fn metadata_notice(notice: &metadata::Notice) -> String { "ignoring the \"{}\" section of {}: expected an object, found {}", section.key(), path.display(), - metadata_type_name(*found) + json_type_name(*found) ), metadata::Notice::EntryUnusable { path, @@ -560,7 +550,7 @@ fn metadata_notice(notice: &metadata::Notice) -> String { match problem { metadata::EntryProblem::NotAnObject { found } => format!( "{head}: expected an object, found {}", - metadata_type_name(*found) + json_type_name(*found) ), metadata::EntryProblem::NotRebuilt { reason } => { format!("{head}: {}", python_repr(reason)) From 8909636b1c2c8432862b32a307cefab5cc0f6c87 Mon Sep 17 00:00:00 2001 From: Austin Gregg-Smith Date: Wed, 19 Aug 2026 19:54:31 +0100 Subject: [PATCH 04/13] Make a spaced stage name a compile error, not a phantom NotReached MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The setup pass's outcome protocol is a marked line split on spaces, so Stage::new("set hostname") was legal and silently reported every stage as not reached — the constraint lived in a doc comment. StageName is a newtype whose const-fn constructor panics on a space or an empty name; the two definition sites (HOSTNAME_STAGE, ZELLIJ_STAGE) call it in const context, so an invalid name is E0080 at compile time (verified: the constant evaluator refuses `StageName::new("set hostname")` with the constructor's message). A #[should_panic] test pins the rule the evaluator enforces. ProvisionEvent keeps carrying &'static str — the name crosses into the binary's rendering as data via StageName::as_str — so no rendered line and no script byte changes; the setup-script goldens against Python's payload still pass unchanged. Refs #263. --- rust/devlaunch-core/src/flows/provision.rs | 106 ++++++++++++++------- 1 file changed, 74 insertions(+), 32 deletions(-) diff --git a/rust/devlaunch-core/src/flows/provision.rs b/rust/devlaunch-core/src/flows/provision.rs index 160531c2..5afda09a 100644 --- a/rust/devlaunch-core/src/flows/provision.rs +++ b/rust/devlaunch-core/src/flows/provision.rs @@ -127,13 +127,46 @@ pub(crate) const STAGE_KEY: &str = "stage"; const STAGE_OK_WORD: &str = "ok"; const STAGE_FAILED_WORD: &str = "failed"; +/// A name the stage-outcome protocol can carry on its marked line. +/// +/// The outcome line is ` stage `, split on spaces by both +/// readers, so a name with a space in it shears every line it is on: +/// `Stage::new("set hostname")` would silently report the stage — and the parse +/// of everything after it — as not reached. The constructor is a `const fn` that +/// panics on a space or an empty name, and every definition site calls it in +/// `const` context, so an invalid name is E0080 at compile time rather than a +/// run of phantom "not reached" reports. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) struct StageName(&'static str); + +impl StageName { + /// The one way to name a stage. Call it in `const` context. + pub(crate) const fn new(name: &'static str) -> Self { + assert!(!name.is_empty(), "a stage name must not be empty"); + let bytes = name.as_bytes(); + let mut at = 0; + while at < bytes.len() { + assert!( + bytes[at] != b' ', + "a stage name must not contain a space: stage_outcomes splits the marked line on them" + ); + at += 1; + } + Self(name) + } + + pub(crate) const fn as_str(self) -> &'static str { + self.0 + } +} + /// The stage that names the container. Its outcome is what "can this image set a /// hostname" detection is, and it costs nothing: the trip is the probe's. -pub(crate) const HOSTNAME_STAGE: &str = "hostname"; +pub(crate) const HOSTNAME_STAGE: StageName = StageName::new("hostname"); /// The stage that puts `zellij` in the container (see [`ZELLIJ_TOOL`]). Also free /// of round trips: it rides the pass every entry into Running already pays. -pub(crate) const ZELLIJ_STAGE: &str = "zellij"; +pub(crate) const ZELLIJ_STAGE: StageName = StageName::new("zellij"); // =========================================================================== // quoting @@ -694,13 +727,10 @@ pub enum FailureLevel { /// is an ordinary command rather than something that has to know about this /// protocol. /// -/// Two constraints on what a stage may be, both unenforced, because the protocol -/// shares the stage's stdout: +/// One constraint on what a stage may be, unenforced, because the protocol +/// shares the stage's stdout ([`StageName`] carries the other one to compile +/// time): /// -/// - `name` must contain no space. It is interpolated raw into the outcome line, -/// which [`stage_outcomes`] splits on the first space; a two-word name makes every -/// stage's outcome unreadable. It is `&'static str` here, so every name in this -/// build is one of the two written above. /// - A stage's own stdout is *not* redirected — [`probe_script`]'s output comes /// behind the stages, not in front of them — so a stage that prints a /// [`PROBE_MARK`] line of its own is read as protocol. Both readers keep the last @@ -708,14 +738,14 @@ pub enum FailureLevel { /// Keep stages silent, or redirect their output. #[derive(Clone, Debug, PartialEq, Eq)] pub(crate) struct Stage { - pub(crate) name: &'static str, + pub(crate) name: StageName, pub(crate) command: String, pub(crate) failure_level: FailureLevel, } impl Stage { /// A stage whose failure is worth a warning — the default a new stage gets. - pub(crate) fn new(name: &'static str, command: impl Into) -> Self { + pub(crate) fn new(name: StageName, command: impl Into) -> Self { Self { name, command: command.into(), @@ -790,12 +820,12 @@ fn stage_snippet(stage: &Stage) -> String { format!("if {}; then", stage.command), format!( " echo \"{PROBE_MARK} {STAGE_KEY} {} {STAGE_OK_WORD}\"", - stage.name + stage.name.as_str() ), "else".to_owned(), format!( " echo \"{PROBE_MARK} {STAGE_KEY} {} {STAGE_FAILED_WORD} $?\"", - stage.name + stage.name.as_str() ), "fi".to_owned(), ] @@ -852,7 +882,7 @@ pub(crate) enum StageResult { /// One stage's name and what became of it. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(crate) struct StageOutcome { - pub(crate) stage: &'static str, + pub(crate) stage: StageName, pub(crate) result: StageResult, } @@ -880,7 +910,7 @@ pub(crate) fn stage_outcomes(report: &str, stages: &[Stage]) -> Vec {} StageResult::Failed { status } => events.push(ProvisionEvent::StageFailed { workspace: workspace.to_owned(), - stage: outcome.stage, + stage: outcome.stage.as_str(), status, loudness, }), StageResult::NotReached => events.push(ProvisionEvent::StageNotReported { workspace: workspace.to_owned(), - stage: outcome.stage, + stage: outcome.stage.as_str(), loudness, }), } @@ -3349,7 +3379,7 @@ fi assert!( script.find(&stage.command).expect("the stage") < probe_at, "{}", - stage.name + stage.name.as_str() ); } } @@ -3373,6 +3403,17 @@ fi assert!(!script.contains("hostname myws;")); } + #[test] + #[should_panic(expected = "must not contain a space")] + fn a_stage_name_with_a_space_has_no_representation() { + // A two-word name shears every outcome line, so every stage reads as not + // reached. At the definition sites the constructor runs in `const` + // context, so there this panic is E0080 — a compile error — rather than + // anything a launch can hit; this pins the rule the constant evaluator + // enforces. + let _ = StageName::new("set hostname"); + } + /// Run the whole pass for real, and read it the way the host reads it. /// /// `sudo_exit` puts a `sudo` on PATH that exits with that status; `None` leaves @@ -3409,7 +3450,7 @@ fi } /// One stage's outcome out of a pass that runs more than one, by name. - fn outcome_of(report: &str, stage: &str) -> Option { + fn outcome_of(report: &str, stage: StageName) -> Option { stage_outcomes(report, &setup_stages("myws", ToolsSwitch::Install)) .into_iter() .find(|outcome| outcome.stage == stage) @@ -3818,13 +3859,9 @@ fi assert!( !events.iter().any(|event| matches!( event, - ProvisionEvent::StageFailed { - stage: ZELLIJ_STAGE, - .. - } | ProvisionEvent::StageNotReported { - stage: ZELLIJ_STAGE, - .. - } + ProvisionEvent::StageFailed { stage, .. } + | ProvisionEvent::StageNotReported { stage, .. } + if *stage == ZELLIJ_STAGE.as_str() )), "{events:?}" ); @@ -3861,7 +3898,9 @@ fi .into_iter() .filter(|event| match event { ProvisionEvent::StageFailed { stage, .. } - | ProvisionEvent::StageNotReported { stage, .. } => *stage == HOSTNAME_STAGE, + | ProvisionEvent::StageNotReported { stage, .. } => { + *stage == HOSTNAME_STAGE.as_str() + } _ => false, }) .collect() @@ -3878,7 +3917,7 @@ fi reported, vec![ProvisionEvent::StageFailed { workspace: "myws".to_owned(), - stage: HOSTNAME_STAGE, + stage: HOSTNAME_STAGE.as_str(), status: 1, loudness: FailureLevel::Info, }] @@ -3901,7 +3940,7 @@ fi hostname_events(REPORT_PROVISIONED, 0), vec![ProvisionEvent::StageNotReported { workspace: "myws".to_owned(), - stage: HOSTNAME_STAGE, + stage: HOSTNAME_STAGE.as_str(), loudness: FailureLevel::Info, }] ); @@ -3919,8 +3958,11 @@ fi // Warning is the default a new stage gets: naming a stage that did not work // is the whole point of the composition, and the hostname's info level is an // exception it has to declare, measured (#167), rather than the rule. - assert_eq!(Stage::new("x", "true").failure_level, FailureLevel::Warning); - let levels: Vec<(&str, FailureLevel)> = setup_stages("myws", ToolsSwitch::Install) + assert_eq!( + Stage::new(StageName::new("x"), "true").failure_level, + FailureLevel::Warning + ); + let levels: Vec<(StageName, FailureLevel)> = setup_stages("myws", ToolsSwitch::Install) .iter() .map(|stage| (stage.name, stage.failure_level)) .collect(); @@ -4412,7 +4454,7 @@ fi // The guarantee, stated where it is made: the pass every entry into Running // goes through carries a zellij stage. No dotfiles, no devcontainer.json, no // repo cooperation — the ask comes from the invocation. - let names: Vec<&str> = setup_stages("myws", ToolsSwitch::Install) + let names: Vec = setup_stages("myws", ToolsSwitch::Install) .iter() .map(|stage| stage.name) .collect(); @@ -4541,7 +4583,7 @@ fi // the hostname stage, which is not tools work and is deliberately left // outside that switch — a machine that turned tool installs off has not // thereby asked for unnamed containers. - let names: Vec<&str> = setup_stages("myws", ToolsSwitch::Skip) + let names: Vec = setup_stages("myws", ToolsSwitch::Skip) .iter() .map(|stage| stage.name) .collect(); From 05cd917f3443891fa3057c42f4acd3be171f469f Mon Sep 17 00:00:00 2001 From: Austin Gregg-Smith Date: Wed, 19 Aug 2026 20:06:06 +0100 Subject: [PATCH 05/13] Stream provisioning events through the notices sink (B9) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit provision_tools filled a &mut Vec that the dl binary rendered only after the whole flow returned, so a warning about a cold install that streams hundreds of megabytes arrived after the wait it explains. Every other long-running flow already reports through the Notices sink at the moment the notice happens. The five flow signatures now take &mut dyn Notices, and dl's ToolProvisioning hands in render::Saying — one line on stderr per event, as core says it. Parity-restoring rather than diverging: Python's tools.py makes each logging.* call inline as the flow runs, so streaming is Python's order; the lines' bytes are unchanged (render::provision_event is untouched), and the tests' Vec sinks coerce to the trait, so no test changed. #264 --- rust/devlaunch-core/src/flows/provision.rs | 30 ++++++++++++---------- rust/dl/src/launch.rs | 13 ++++------ rust/dl/src/render.rs | 13 ++++++---- 3 files changed, 30 insertions(+), 26 deletions(-) diff --git a/rust/devlaunch-core/src/flows/provision.rs b/rust/devlaunch-core/src/flows/provision.rs index 5afda09a..a3fbd7a5 100644 --- a/rust/devlaunch-core/src/flows/provision.rs +++ b/rust/devlaunch-core/src/flows/provision.rs @@ -55,8 +55,11 @@ //! devpod that has gone missing between `up` and here ([`DevpodMissing`]) — which //! dl treats as fatal everywhere else and which this does not make an exception //! of. Nothing in this module prints: every `logging.*` call Python makes is an -//! arm of [`ProvisionEvent`], carrying what that line interpolated, and the words -//! and levels are the `dl` binary's rendering (#251 §5). +//! arm of [`ProvisionEvent`], carrying what that line interpolated, said into the +//! caller's [`Notices`] sink at the moment it happens — Python logs each line as +//! it goes, and a cold install is minutes long, so an event held back to the end +//! of the flow would arrive after the wait it explains. The words and levels are +//! the `dl` binary's rendering (#251 §5). use std::collections::BTreeMap; use std::fs::{File, Metadata}; @@ -68,6 +71,7 @@ use std::path::{Path, PathBuf}; use sha2::{Digest as _, Sha256}; use crate::clients::devpod::{self, Call, NotRun}; +use crate::notices::Notices; use crate::runner::interrupt; use crate::runner::{Exit, OsFailure, Runner}; use crate::timing; @@ -1484,7 +1488,7 @@ pub fn provision_tools( workspace: &str, tools: ToolsSwitch, host: Option<&HostLayout>, - events: &mut Vec, + events: &mut dyn Notices, ) -> Result { timing::stage_result(timing::Stage::Tools, || { provision(runner, workspace, tools, host, events) @@ -1497,7 +1501,7 @@ fn provision( workspace: &str, tools: ToolsSwitch, host: Option<&HostLayout>, - events: &mut Vec, + events: &mut dyn Notices, ) -> Result { let found = match setup_pass(runner, workspace, tools, events) { Ok(found) => found, @@ -1505,7 +1509,7 @@ fn provision( }; if let ToolsSwitch::Skip = tools { - events.push(ProvisionEvent::ProvisioningDisabled { + events.say(ProvisionEvent::ProvisioningDisabled { workspace: workspace.to_owned(), }); return Ok(Provisioning::Disabled); @@ -1519,7 +1523,7 @@ fn provision( match transfer(runner, workspace, &payload) { Ok(()) => return Ok(Provisioning::Lent), Err(TransferFailed::Bundle(failure)) => { - events.push(ProvisionEvent::PayloadNotBundled { failure }); + events.say(ProvisionEvent::PayloadNotBundled { failure }); } Err(TransferFailed::Trip(refusal)) => return refused(workspace, refusal, events), // The container would not run the lent binaries (a different arch or @@ -1550,7 +1554,7 @@ fn provision( match devpod::run(runner, &call) { Ok(exit) if exit.is_success() => Ok(Provisioning::Installed), Ok(exit) => { - events.push(ProvisionEvent::NotInstalled { + events.say(ProvisionEvent::NotInstalled { workspace: workspace.to_owned(), tools: REQUIRED_TOOLS.iter().map(|tool| tool.command).collect(), exit, @@ -1568,7 +1572,7 @@ fn provision( fn refused( workspace: &str, refusal: NotRun, - events: &mut Vec, + events: &mut dyn Notices, ) -> Result { match refusal { NotRun::NotInstalled => Err(DevpodMissing), @@ -1576,7 +1580,7 @@ fn refused( // process killed is one that never answered, which is the same fact the // OS refusals carry. refusal @ (NotRun::TimedOut | NotRun::Blocked(_)) => { - events.push(ProvisionEvent::TripRefused { + events.say(ProvisionEvent::TripRefused { workspace: workspace.to_owned(), refusal, }); @@ -1606,7 +1610,7 @@ fn setup_pass( runner: &dyn Runner, workspace: &str, tools: ToolsSwitch, - events: &mut Vec, + events: &mut dyn Notices, ) -> Result { let stages = setup_stages(workspace, tools); let call = Call::new([ @@ -1637,7 +1641,7 @@ fn report_outcome( workspace: &str, stages: &[Stage], outcome: StageOutcome, - events: &mut Vec, + events: &mut dyn Notices, ) { let loudness = stages .iter() @@ -1646,13 +1650,13 @@ fn report_outcome( .unwrap_or_default(); match outcome.result { StageResult::Ok => {} - StageResult::Failed { status } => events.push(ProvisionEvent::StageFailed { + StageResult::Failed { status } => events.say(ProvisionEvent::StageFailed { workspace: workspace.to_owned(), stage: outcome.stage.as_str(), status, loudness, }), - StageResult::NotReached => events.push(ProvisionEvent::StageNotReported { + StageResult::NotReached => events.say(ProvisionEvent::StageNotReported { workspace: workspace.to_owned(), stage: outcome.stage.as_str(), loudness, diff --git a/rust/dl/src/launch.rs b/rust/dl/src/launch.rs index bff3d107..00c7b1ee 100644 --- a/rust/dl/src/launch.rs +++ b/rust/dl/src/launch.rs @@ -122,20 +122,17 @@ impl Provision for ToolProvisioning { runner: &dyn Runner, workspace_id: &str, ) -> Result<(), DevpodMissing> { - let mut events = Vec::new(); + // The events stream through the same sink as the launch's own notices — + // one line on stderr at the moment core says it, which is Python's order: + // a cold install streams hundreds of megabytes, and a warning about it is + // worth something while it is still happening. let provisioned = provision::provision_tools( runner, workspace_id, self.tools, self.host.as_ref(), - &mut events, + &mut render::Saying, ); - // Said here rather than collected for the end of the command: a cold - // install streams hundreds of megabytes, and a warning about it is worth - // something while it is still happening. - for line in render::provision_events(&events) { - eprintln!("{line}"); - } // Every way of coming up empty is an arm of `Provisioning`, and none of them // is worth a word beyond the events above: the workspace is up and the user // asked for a session, not for an install. A devpod that has gone missing is diff --git a/rust/dl/src/render.rs b/rust/dl/src/render.rs index 579b45e4..e4f0c39e 100644 --- a/rust/dl/src/render.rs +++ b/rust/dl/src/render.rs @@ -1646,6 +1646,14 @@ impl Notices for Saying { } } +impl Notices for Saying { + fn say(&mut self, event: ProvisionEvent) { + if let Some(line) = provision_event(&event) { + eprintln!("{line}"); + } + } +} + /// Why this workspace opens without a GitHub login. /// /// The `Refused` arm names the directory gh read its config from, because that is @@ -2009,11 +2017,6 @@ pub(crate) fn provision_event(event: &ProvisionEvent) -> Option { }) } -/// The lines a provisioning pass's events read as, with the debug ones dropped. -pub(crate) fn provision_events(events: &[ProvisionEvent]) -> Vec { - events.iter().filter_map(provision_event).collect() -} - #[cfg(test)] mod tests { use std::path::PathBuf; From 293b37873ab84b934026f4d2b1a7e85f63a45ff3 Mon Sep 17 00:00:00 2001 From: Austin Gregg-Smith Date: Wed, 19 Aug 2026 20:14:39 +0100 Subject: [PATCH 06/13] Report a purge failure once, and carry each adoption's ending (B5/B6) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit B5: a workspace devpod would not delete was reported through two paths — a PurgeStep::NotDeleted said by the step callback and a LifecycleNotice::WorkspaceNotDeleted collected beside it — and the dl binary carried say_except_purge_failures purely to drop the second copy again. The step is now the failure's one report (it is the streamed path, which is where Python's logging.warning happens, inline in the delete loop); the notice variant, the purge's notices parameter and the filtering helper are gone. The printed line is unchanged: "Failed to delete workspace {id}: {stderr}", once. B6: --reconcile re-derived each adoption's ending by scanning the report's refusal list for an absence — O(n^2), and an inference ("not refused") standing in for a record the flow already had, so an adoption that was neither re-pointed nor refused would have read as re-pointed. ReconcileReport now carries one Adoption per attempt, in the plan's order, each arm its own ending (private field behind accessors, the shape #263's 98618d5 gave ReconcilePlan). The rendered lines and their order are byte-identical; only how the binary reads them changed. #264 --- rust/devlaunch-core/src/flows/lifecycle.rs | 133 +++++++++++---------- rust/dl/src/commands.rs | 66 ++++------ rust/dl/src/render.rs | 9 +- 3 files changed, 96 insertions(+), 112 deletions(-) diff --git a/rust/devlaunch-core/src/flows/lifecycle.rs b/rust/devlaunch-core/src/flows/lifecycle.rs index c6a24347..8aa3ef07 100644 --- a/rust/devlaunch-core/src/flows/lifecycle.rs +++ b/rust/devlaunch-core/src/flows/lifecycle.rs @@ -105,14 +105,6 @@ pub enum LifecycleNotice { workspace_id: String, refusal: RemoveWorkspaceError, }, - /// A purge asked devpod to delete one of its own workspaces and devpod - /// refused. The purge carries on: one failed delete must not cost the rest of - /// the cache its removal. - WorkspaceNotDeleted { - workspace_id: String, - exit: Exit, - stderr: String, - }, /// A clone directory went and its `metadata.json` record could not be /// dropped. Named by the path, which is what the record described. /// @@ -995,7 +987,10 @@ pub fn purge_plan( pub enum PurgeStep { /// About to ask devpod to delete this workspace. Deleting { workspace_id: String }, - /// devpod refused, and the purge carried on. + /// devpod refused, and the purge carried on: one failed delete must not cost + /// the rest of the cache its removal. The step is the failure's one report — + /// it used to be doubled as a [`LifecycleNotice`] too, and the binary carried + /// a filter whose whole job was to drop the second copy. NotDeleted { workspace_id: String, exit: Exit, @@ -1068,7 +1063,6 @@ pub fn purge_all_data( context: &mut CommandContext<'_>, plan: &PurgePlan, on_step: &mut dyn FnMut(PurgeStep), - notices: &mut dyn Notices, ) -> Result { for workspace in &plan.ownership.mine { on_step(PurgeStep::Deleting { @@ -1076,11 +1070,6 @@ pub fn purge_all_data( }); let answer = devpod::capture(context.runner(), &purge_delete_call(&workspace.id))?; if !answer.succeeded() { - notices.say(LifecycleNotice::WorkspaceNotDeleted { - workspace_id: workspace.id.clone(), - exit: answer.exit, - stderr: answer.stderr().to_owned(), - }); on_step(PurgeStep::NotDeleted { workspace_id: workspace.id.clone(), exit: answer.exit, @@ -2607,23 +2596,56 @@ pub fn repoint_devpod_source( Ok(()) } -/// One adoption that could not be made. +/// What became of one of the plan's adoptions. +/// +/// Each carries its own ending, so a caller reads how an adoption went straight +/// off the arm. The report used to be two lists — re-pointed and refused — and the +/// binary re-derived each adoption's ending by scanning the refusal list for an +/// absence: quadratic, and an inference ("not refused") standing in for a record +/// ("re-pointed") that was already being kept. #[derive(Debug, Clone, PartialEq, Eq)] -pub struct RepointRefused { - pub workspace_id: String, - pub failure: RepointFailure, +pub enum Adoption { + /// devpod's record now points at the clone, and metadata carries the id. + /// Boxed because an [`Adoptable`] carries the whole [`WorktreeInfo`] and the + /// refusal arm is a fraction of its size (clippy::large_enum_variant). + Repointed(Box), + /// devpod's record could not be re-pointed. The plan's other adoptions went + /// on: one unrepairable record must not cost the rest their repair. + Refused { + workspace_id: String, + failure: RepointFailure, + }, } -/// What applying a reconcile plan did. +/// What applying a reconcile plan did: one [`Adoption`] per adoption the plan +/// asked for, in the order they were attempted. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ReconcileReport { - pub repointed: Vec, - pub refused: Vec, + // Private for [`ReconcilePlan`]'s reason: a report a caller could assemble is + // one whose endings need not be the attempts'. + adoptions: Vec, } impl ReconcileReport { + /// Each adoption with what became of it, in the order they were attempted. + pub fn adoptions(&self) -> &[Adoption] { + &self.adoptions + } + + /// The adoptions that landed. + pub fn repointed(&self) -> impl Iterator { + self.adoptions.iter().filter_map(|adoption| match adoption { + Adoption::Repointed(adoptable) => Some(adoptable.as_ref()), + Adoption::Refused { .. } => None, + }) + } + + /// Whether every adoption landed. The one distinction an exit code carries. pub fn finished(&self) -> bool { - self.refused.is_empty() + !self + .adoptions + .iter() + .any(|adoption| matches!(adoption, Adoption::Refused { .. })) } } @@ -2642,13 +2664,10 @@ pub fn apply_reconciliation( plan: &ReconcilePlan, notices: &mut dyn Notices, ) -> ReconcileReport { - let mut report = ReconcileReport { - repointed: Vec::new(), - refused: Vec::new(), - }; + let mut adoptions = Vec::new(); for adoptable in &plan.adopting { if let Err(failure) = repoint_devpod_source(devpod_home, adoptable) { - report.refused.push(RepointRefused { + adoptions.push(Adoption::Refused { workspace_id: adoptable.workspace_id.clone(), failure, }); @@ -2666,13 +2685,13 @@ pub fn apply_reconciliation( refusal: error, }), } - report.repointed.push(adoptable.clone()); + adoptions.push(Adoption::Repointed(Box::new(adoptable.clone()))); } // devpod's records just changed, so any listing dl is holding describes the // world before the repair. context.forget_workspaces(); refresh.ask(context.runner(), RefreshReason::Forced); - report + ReconcileReport { adoptions } } #[cfg(test)] @@ -3765,13 +3784,10 @@ mod tests { cache } - fn purge(devpod: &Devpod, cache_dir: &Path) -> (PurgeOutcome, Vec) { + fn purge(devpod: &Devpod, cache_dir: &Path) -> PurgeOutcome { let mut context = CommandContext::new(devpod); let plan = purge_plan(&mut context, cache_dir).expect("a plan"); - let mut notices = Vec::new(); - let outcome = - purge_all_data(&mut context, &plan, &mut |_| {}, &mut notices).expect("devpod ran"); - (outcome, notices) + purge_all_data(&mut context, &plan, &mut |_| {}).expect("devpod ran") } #[test] @@ -3781,7 +3797,7 @@ mod tests { let devpod = Devpod::new(); devpod.lists(&six_workspaces(&cache)); - let (outcome, _) = purge(&devpod, &cache); + let outcome = purge(&devpod, &cache); assert_eq!(devpod.deleted(), CLONED_BY_DEVLAUNCH); assert!(!cache.exists(), "the cache goes too"); @@ -3856,7 +3872,7 @@ mod tests { let devpod = Devpod::new(); devpod.lists(&six_workspaces(&real_cache)); - let (outcome, _) = purge(&devpod, &scratch); + let outcome = purge(&devpod, &scratch); assert_eq!(devpod.deleted(), Vec::::new()); assert_eq!(outcome, PurgeOutcome::NothingToPurge); @@ -3872,7 +3888,7 @@ mod tests { Path::new("/home/dev/projects/python_template"), )]); - let (outcome, _) = purge(&devpod, &dir.path().join("never-made")); + let outcome = purge(&devpod, &dir.path().join("never-made")); assert_eq!(outcome, PurgeOutcome::NothingToPurge); assert_eq!(devpod.deleted(), Vec::::new()); @@ -3895,7 +3911,7 @@ mod tests { .join("r-main-aa"), )]); - let (outcome, _) = purge(&devpod, &cache); + let outcome = purge(&devpod, &cache); assert_eq!(outcome, PurgeOutcome::NoCacheDirectory); assert_eq!(devpod.deleted(), ["r-main-aa"]); @@ -3940,7 +3956,7 @@ mod tests { let mut context = CommandContext::new(&devpod); let plan = purge_plan(&mut context, &cache).expect("a plan"); - purge_all_data(&mut context, &plan, &mut |_| {}, &mut ignoring()).expect("devpod ran"); + purge_all_data(&mut context, &plan, &mut |_| {}).expect("devpod ran"); devpod.lists(&[]); assert_eq!( context.workspaces().expect("a listing"), @@ -3969,25 +3985,18 @@ mod tests { let mut steps = Vec::new(); let mut context = CommandContext::new(&devpod); let plan = purge_plan(&mut context, &cache).expect("a plan"); - let mut notices = Vec::new(); - let outcome = purge_all_data( - &mut context, - &plan, - &mut |step| steps.push(step), - &mut notices, - ) - .expect("devpod ran"); + let outcome = + purge_all_data(&mut context, &plan, &mut |step| steps.push(step)).expect("devpod ran"); assert_eq!(outcome, PurgeOutcome::Removed { cache_dir: cache }); - assert!(matches!( - notices.as_slice(), - [LifecycleNotice::WorkspaceNotDeleted { workspace_id, stderr, .. }] - if workspace_id == "r-main-aa" && stderr.contains("container is busy") - )); + // The step is the failure's one report — no notice doubles it. assert!(matches!( steps.as_slice(), - [PurgeStep::Deleting { .. }, PurgeStep::NotDeleted { .. },] + [ + PurgeStep::Deleting { .. }, + PurgeStep::NotDeleted { workspace_id, stderr, .. }, + ] if workspace_id == "r-main-aa" && stderr.contains("container is busy") )); } @@ -4003,7 +4012,7 @@ mod tests { return; }; - let (outcome, _) = purge(&devpod, &cache.root); + let outcome = purge(&devpod, &cache.root); let PurgeOutcome::RemovedWhatItCould { refused, .. } = &outcome else { panic!("expected a partial removal, got {outcome:?}"); @@ -4037,7 +4046,7 @@ mod tests { let devpod = Devpod::new(); devpod.lists(&[]); - let (outcome, _) = purge(&devpod, &root); + let outcome = purge(&devpod, &root); assert!( matches!(outcome, PurgeOutcome::RemovedNothing { .. }), @@ -4066,7 +4075,7 @@ mod tests { return; }; - let (outcome, _) = purge(&devpod, &root); + let outcome = purge(&devpod, &root); assert!( matches!(outcome, PurgeOutcome::RemovedNothing { .. }), @@ -6239,7 +6248,7 @@ mod tests { ); assert!(report.finished()); - assert_eq!(report.repointed.len(), 1); + assert_eq!(report.repointed().count(), 1); assert_eq!( sourced_at(&record), canonical(&clone.to_string_lossy()) @@ -6447,10 +6456,12 @@ mod tests { ); assert!(!report.finished()); - assert!(report.repointed.is_empty()); assert!(matches!( - report.refused[0].failure, - RepointFailure::Unreadable { .. } + report.adoptions(), + [Adoption::Refused { + failure: RepointFailure::Unreadable { .. }, + .. + }] )); assert_eq!( recorded_devpod_workspace_id(&world.storage, OWNER, REPO, "main"), diff --git a/rust/dl/src/commands.rs b/rust/dl/src/commands.rs index 5e027a05..c0a23eb5 100644 --- a/rust/dl/src/commands.rs +++ b/rust/dl/src/commands.rs @@ -703,49 +703,31 @@ fn purge_devlaunch_data(context: &mut CommandContext<'_>, cache: &Path, yes: boo println!("Aborted."); return Cleanup::Ended(Ending::Done); } - let mut notices: Vec = Vec::new(); - let purged = lifecycle::purge_all_data( - context, - &plan, - &mut |step| match render::purge_step(&step) { + let purged = lifecycle::purge_all_data(context, &plan, &mut |step| { + match render::purge_step(&step) { // Said before the round trip that may take a while, which is why this // is a callback and not a report: "Deleting workspace X" assembled // afterwards cannot be said in time. render::Line::Out(line) => println!("{line}"), render::Line::Err(line) => eprintln!("{line}"), - }, - &mut notices, - ); - let ending = match purged { + } + }); + match purged { // Raised out of the command in Python, and it takes the report with it: // nothing after this point would work either. - Err(not_run) => return Cleanup::Raised(refuse_devpod("delete", ¬_run)), + Err(not_run) => Cleanup::Raised(refuse_devpod("delete", ¬_run)), Ok(outcome) => { print(&render::purge_outcome(&outcome)); if outcome.finished() { - Ending::Done + Cleanup::Ended(Ending::Done) } else { // Not 0: a clone the user was told would go is still on disk. Which // of the two failures happened is in the report above, where // somebody can act on it. - Ending::Refused + Cleanup::Ended(Ending::Refused) } } - }; - // Every workspace a purge could not delete has already been said by the step - // callback above; saying the notice for it too would print it twice. - say_except_purge_failures(¬ices); - Cleanup::Ended(ending) -} - -/// The notices a purge collected, minus the ones its steps already said. -fn say_except_purge_failures(notices: &[LifecycleNotice]) { - let rest: Vec = notices - .iter() - .filter(|notice| !matches!(notice, LifecycleNotice::WorkspaceNotDeleted { .. })) - .cloned() - .collect(); - say(&rest); + } } /// Print the contended-lock wait notices Python prints, so a maintenance command @@ -951,27 +933,23 @@ fn render_reconcile( &plan, &mut notices, ); - // Read in the plan's order rather than the report's two lists, because that is - // the order these lines happened in: one adoption at a time, each either done or - // refused. The report partitions the plan's adoptions — every one lands in - // exactly one of the lists — so "not refused" is "re-pointed", which is the same - // question Python's loop asked of each one. - for adoptable in plan.adopting() { - let refused = applied - .refused - .iter() - .find(|refusal| refusal.workspace_id == adoptable.workspace_id); - match refused { - Some(refusal) => eprintln!( - "Could not re-point {}: {}", - refusal.workspace_id, - render::repoint_failure(&refusal.failure) - ), - None => println!( + // The report carries one ending per adoption, in the order they were + // attempted — the plan's order, which is the order these lines happened in: + // one adoption at a time, each either done or refused. + for adoption in applied.adoptions() { + match adoption { + lifecycle::Adoption::Repointed(adoptable) => println!( "Re-pointed {} at {}", adoptable.workspace_id, adoptable.clone.display() ), + lifecycle::Adoption::Refused { + workspace_id, + failure, + } => eprintln!( + "Could not re-point {workspace_id}: {}", + render::repoint_failure(failure) + ), } } say(¬ices); diff --git a/rust/dl/src/render.rs b/rust/dl/src/render.rs index e4f0c39e..fe2c1c28 100644 --- a/rust/dl/src/render.rs +++ b/rust/dl/src/render.rs @@ -841,11 +841,6 @@ fn lifecycle_notice(notice: &LifecycleNotice) -> Option { LifecycleNotice::CloneNotRemoved { refusal, .. } => { format!("Failed to remove local clone: {}", not_removed(refusal)) } - LifecycleNotice::WorkspaceNotDeleted { - workspace_id, - stderr, - .. - } => format!("Failed to delete workspace {workspace_id}: {stderr}"), LifecycleNotice::RecordNotDropped { path, refusal } => { format!( "Could not drop the record for {}: {}", @@ -1188,8 +1183,8 @@ pub(crate) enum Line { /// /// Handed over as it happens rather than collected, which is why this renders one /// step rather than a report: "Deleting workspace X" said afterwards is not said in -/// time. A failure is the same event as [`LifecycleNotice::WorkspaceNotDeleted`] and -/// is said here, where it happens, rather than twice. +/// time. A failed delete is said here too — the step is its one report, so nothing +/// upstream can print it a second time. pub(crate) fn purge_step(step: &PurgeStep) -> Line { match step { PurgeStep::Deleting { workspace_id } => { From c58c0ab6d84b88b89991d8e71573bc96a652604b Mon Sep 17 00:00:00 2001 From: Austin Gregg-Smith Date: Wed, 19 Aug 2026 20:16:59 +0100 Subject: [PATCH 07/13] Stage each completion-cache write through its own temp name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both cache files staged through one shared `completions.tmp` (Python's `Path.with_suffix(".tmp")` replaces the extension), and two overlapping `--update-cache` children are routine — a launch's post-command refresh beside a stale-cache startup warm — so a losing writer could have the other file's bytes renamed over its own output: bash text under completions.json, read back as "no cache". The staging name now keeps the target's full file name and appends the writer's pid — distinct per target, distinct per process, deterministic within one (the discriminator tempfile-style schemes use, without the randomness that would unsettle test determinism) — and stays in the target's directory so the rename cannot cross a filesystem. No divergence row: Python stages through a temp name too (a shared one), the temp file is transient and read by nothing, and the final paths and bytes are unchanged. #264 --- .../src/flows/completion_cache.rs | 69 +++++++++++++------ 1 file changed, 49 insertions(+), 20 deletions(-) diff --git a/rust/devlaunch-core/src/flows/completion_cache.rs b/rust/devlaunch-core/src/flows/completion_cache.rs index 77554535..56b7cc79 100644 --- a/rust/devlaunch-core/src/flows/completion_cache.rs +++ b/rust/devlaunch-core/src/flows/completion_cache.rs @@ -155,23 +155,35 @@ pub(crate) fn write_bash_completion_cache(path: &Path, data: &CompletionData) -> /// *text* into *path*, via a temp file in the same directory. /// -/// **The temp file's name is Python's, and Python's is a hazard.** Python builds it -/// with `Path.with_suffix(".tmp")`, which *replaces* the extension — so -/// `completions.json` and `completions.bash` both stage through -/// `completions.tmp`. Two refreshes running at once can therefore have one's bash -/// text renamed over the other's JSON cache. It is reproduced rather than fixed -/// because a fix is a behavioural divergence and those are numbered in -/// docs/rust-rewrite-plan.md; this comment is the note asking for a row. Nothing -/// is corrupted beyond one cache file, and the next refresh rewrites it. +/// The temp name is [`staging_path`]'s — per target file, per process — where +/// Python's `Path.with_suffix(".tmp")` *replaces* the extension, so +/// `completions.json` and `completions.bash` both staged through one shared +/// `completions.tmp`. Two overlapping `--update-cache` children are routine, and +/// under the shared name a losing writer could have the other's bash text renamed +/// over its JSON cache. Not a divergence row: the temp file is transient and +/// never read by anything, and the final paths and bytes are Python's exactly. fn write_atomically(path: &Path, text: &str) -> io::Result<()> { if let Some(parent) = path.parent() { std::fs::create_dir_all(parent)?; } - let staged = path.with_extension("tmp"); + let staged = staging_path(path); std::fs::write(&staged, text)?; std::fs::rename(&staged, path) } +/// The sibling a write stages its bytes through before the rename. +/// +/// The full file name is kept and the writer's pid appended, so no two target +/// files and no two processes share a staging file (the pid is the discriminator +/// tempfile-style schemes use, and unlike a random name it is deterministic +/// within one process). Same directory as the target, so the rename cannot cross +/// a filesystem. +fn staging_path(path: &Path) -> PathBuf { + let mut name = path.file_name().map(ToOwned::to_owned).unwrap_or_default(); + name.push(format!(".{}.tmp", std::process::id())); + path.with_file_name(name) +} + /// How long ago the cache was written, or nothing when there is no cache. /// /// Never negative: a cache stamped in the future is zero seconds old, as Python's @@ -521,23 +533,29 @@ mod tests { } #[test] - fn both_caches_stage_through_the_same_temp_file() { + fn each_cache_stages_through_its_own_temp_name() { // Python builds the temp name with `Path.with_suffix(".tmp")`, which // *replaces* the extension, so `completions.json` and `completions.bash` - // both stage through `completions.tmp`. Reproduced rather than fixed — - // fixing it is a behavioural divergence and those are numbered in - // docs/rust-rewrite-plan.md — and pinned here so the row has something to - // point at. Two refreshes at once can have one's bash text renamed over the - // other's JSON cache. + // both staged through one shared `completions.tmp` — and two overlapping + // `--update-cache` children are routine, so a losing writer could have the + // other's bash text renamed over its JSON cache. The staging name keeps the + // target's full name and appends the writer's pid: distinct per file, + // distinct per process, deterministic within one, and in the target's own + // directory so the rename stays atomic. The temp file is transient and + // never read by anything, so this diverges from nothing a user can see. let root = temp_dir(); + let json = staging_path(&cache_path(root.path())); + let bash = staging_path(&bash_cache_path(root.path())); + assert_ne!(json, bash); + let pid = std::process::id(); assert_eq!( - cache_path(root.path()).with_extension("tmp"), - bash_cache_path(root.path()).with_extension("tmp") + json, + root.path().join(format!("completions.json.{pid}.tmp")) ); assert_eq!( - cache_path(root.path()).with_extension("tmp"), - root.path().join("completions.tmp") + bash, + root.path().join(format!("completions.bash.{pid}.tmp")) ); } @@ -548,7 +566,18 @@ mod tests { write_completion_cache(&cache_path(root.path()), &data()).expect("written"); write_bash_completion_cache(&bash_cache_path(root.path()), &data()).expect("written"); - assert!(!root.path().join("completions.tmp").exists()); + let mut names: Vec = std::fs::read_dir(root.path()) + .expect("the directory") + .map(|entry| { + entry + .expect("an entry") + .file_name() + .to_string_lossy() + .into_owned() + }) + .collect(); + names.sort(); + assert_eq!(names, ["completions.bash", "completions.json"]); } #[test] From 70f2172ddf4c18f936314d8f27d7558e34eae28a Mon Sep 17 00:00:00 2001 From: Austin Gregg-Smith Date: Wed, 19 Aug 2026 20:18:06 +0100 Subject: [PATCH 08/13] Fall back to the bare name when current_exe names a swapped-out binary (F5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The detached completions-refresh child was spawned by current_exe()'s answer unconditionally. After `pixi global update` replaces the binary mid-run, Linux reports the unlinked inode as `/path/dl (deleted)` — a path that exists for no one — so the spawn failed with ProgramNotFound and completions silently lost their freshness; the "dl" fallback only covered current_exe() itself erroring. Python never had the gap: `sys.executable -m devlaunch.dl` resolves the interpreter afresh. The decision is now its own function: a path that no longer exists, or that carries the kernel's ` (deleted)` mark (refused even when a file happens to sit at the marked name), falls back to the bare "dl" and lets the spawn's PATH search find the replacement. Unit tests cover all four arms; the mid-run swap is simulated by the path-exists check the decision actually turns on. #264 --- rust/dl/src/session.rs | 80 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 71 insertions(+), 9 deletions(-) diff --git a/rust/dl/src/session.rs b/rust/dl/src/session.rs index f7c07589..c55a97f5 100644 --- a/rust/dl/src/session.rs +++ b/rust/dl/src/session.rs @@ -76,16 +76,32 @@ pub(crate) fn cache_dir() -> Result { /// drives it, so the one process that knows which program it is hands the answer /// down. No leading arguments — Python's re-invocation needs `-m devlaunch.dl` and /// a compiled binary needs nothing. -/// -/// A build whose own path cannot be read falls back to the name a shell would -/// resolve, which is the only other honest guess; a spawn that then finds nothing -/// is [`lifecycle::SpawnRefused::ProgramNotFound`], and a refresh that could not be -/// spawned costs completions their freshness and nothing else. pub(crate) fn self_invocation() -> SelfInvocation { - let program = std::env::current_exe() - .map(|path| path.to_string_lossy().into_owned()) - .unwrap_or_else(|_| "dl".to_owned()); - SelfInvocation::new(program) + SelfInvocation::new(refresh_program(std::env::current_exe().ok())) +} + +/// The program the refresh child is spawned as, from what `current_exe()` said. +/// +/// The answer has to still be spawnable, which the running binary's path is not +/// guaranteed to be: after `pixi global update` swaps the binary mid-run, Linux +/// reports the unlinked inode as `/path/dl (deleted)` — a path that exists for no +/// one — so spawning it fails with `ProgramNotFound` and completions silently +/// lose their freshness. Python's `sys.executable` survives the same swap, so +/// this is where the gap is closed: a path that no longer exists, or that carries +/// the kernel's ` (deleted)` mark (checked on its own too, against a file that +/// happens to sit at the marked name), falls back to the bare program name and +/// lets the spawn's PATH search find the replacement. That name is the only +/// other honest guess; a spawn that then finds nothing is +/// [`lifecycle::SpawnRefused::ProgramNotFound`](devlaunch_core::flows::lifecycle::SpawnRefused::ProgramNotFound), +/// and a refresh that could not be spawned costs completions their freshness and +/// nothing else. +fn refresh_program(current_exe: Option) -> String { + match current_exe { + Some(path) if path.exists() && !path.to_string_lossy().ends_with(" (deleted)") => { + path.to_string_lossy().into_owned() + } + _ => "dl".to_owned(), + } } /// The worktree config, for the commands that need only `repos_dir`. @@ -154,3 +170,49 @@ pub(crate) fn open_records<'r>(runner: &'r dyn Runner) -> Result, St migration_refused, }) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn a_binary_that_still_exists_is_respawned_by_its_own_path() { + let dir = tempfile::tempdir().expect("a temporary directory"); + let binary = dir.path().join("dl"); + std::fs::write(&binary, "").expect("a file standing in for the binary"); + + assert_eq!( + refresh_program(Some(binary.clone())), + binary.to_string_lossy() + ); + } + + #[test] + fn a_binary_swapped_out_from_under_the_run_falls_back_to_the_bare_name() { + // What `current_exe()` answers after `pixi global update` replaces the + // binary mid-run: the unlinked inode's path, which exists for no one. The + // swap is simulated by the path-exists check — the path simply is not + // there — which is exactly the fact the decision turns on. + let dir = tempfile::tempdir().expect("a temporary directory"); + + assert_eq!(refresh_program(Some(dir.path().join("dl (deleted)"))), "dl"); + assert_eq!(refresh_program(Some(dir.path().join("dl"))), "dl"); + } + + #[test] + fn the_kernels_deleted_mark_is_refused_even_where_a_file_wears_it() { + // ` (deleted)` is the kernel's annotation, not part of any name dl was + // started by — so a file that happens to sit at the marked path must not + // launder the mark into an answer. + let dir = tempfile::tempdir().expect("a temporary directory"); + let marked = dir.path().join("dl (deleted)"); + std::fs::write(&marked, "").expect("a file at the marked name"); + + assert_eq!(refresh_program(Some(marked)), "dl"); + } + + #[test] + fn a_path_that_could_not_be_read_at_all_falls_back_to_the_bare_name() { + assert_eq!(refresh_program(None), "dl"); + } +} From 91f2264b2340455d4348d0869e0f6068ca215f12 Mon Sep 17 00:00:00 2001 From: Austin Gregg-Smith Date: Wed, 19 Aug 2026 20:19:50 +0100 Subject: [PATCH 09/13] Document and pin the strict metadata.json reader (divergence row 28) Python's json.loads takes extensions the Rust reader refuses: NaN and Infinity literals, numbers past f64 range (1e400 reads as inf), and lone-surrogate \uD800 escapes. A metadata.json carrying one is quarantined to .corrupt and the run starts empty, where Python loaded the records around the value. Decided as a kept divergence rather than a softened reader: serde_json cannot represent a lone surrogate and f64 cannot hold 1e400, so accepting them would admit values the store could not round-trip; no build of dl ever writes any of them, and the quarantine keeps the bytes recoverable. Row 28 in the divergence table licenses it, and pythons_json_extensions_read_as_corruption_here in domain/metadata.rs pins the behavior (the existing corruption sweep covered malformed JSON, not Python-tolerated JSON). #264 --- docs/rust-rewrite-plan.md | 1 + rust/devlaunch-core/src/domain/metadata.rs | 37 ++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/docs/rust-rewrite-plan.md b/docs/rust-rewrite-plan.md index 4f4e8672..40f6fd39 100644 --- a/docs/rust-rewrite-plan.md +++ b/docs/rust-rewrite-plan.md @@ -173,6 +173,7 @@ Nothing diverges silently. | 25 | A path spec that normalises to `/` (`dl /`, `//`, `/.`, `/..`) is refused with `'/' does not name a workspace: its path has no final component to name one after.` (exit 1) instead of handing devpod an empty `--id`. Python also exited 1, but only after `devpod up` ran with a blank id and the follow-up `devpod ssh ""` failed. | Refusing an unnameable spec up front beats creating a blank-id workspace; exit code matches, wording differs. | | 26 | `dl --version ` / `aid --version ` exit 2 (clap rejects an argument beside `--version`) where Python ignored the extra and printed the version (exit 0). | Companion to rows 2/14/15 — the generated parser owns its usage errors. | | 27 | On SIGINT dl prints no timing summary and no partial output — the handler is `_exit(130)` after an async-signal-safe cleanup (unlink the staged token / temp files, `killpg` the `devpod up` group). Python emitted a timing summary from a `finally`. | A signal handler may not allocate, lock, or format; correctness of the credential cleanup wins over the summary (row 5: timing is not a parity dimension). | +| 28 | The `metadata.json` reader takes strict JSON where Python's `json.loads` took its extensions: `NaN`/`Infinity` literals, numbers beyond f64 range (`1e400`, which Python reads as `inf`), and lone-surrogate `\uD800` escapes all read as corruption, so a file carrying one is quarantined to `metadata.json.corrupt` (bytes intact, single slot) and the run starts with an empty cache where Python loaded the records around the value. | serde_json cannot represent a lone surrogate in a Rust `String` and f64 cannot hold `1e400`, so accepting them would mean values the store could not round-trip; no build of dl (Python or Rust) ever writes any of them — only a hand-edited or third-party-written file can carry one — and the quarantine keeps the bytes recoverable. Pinned by `pythons_json_extensions_read_as_corruption_here` in `domain/metadata.rs`. | Additions require a PR that updates this table; the row number is cited by any per-binary harness branch. diff --git a/rust/devlaunch-core/src/domain/metadata.rs b/rust/devlaunch-core/src/domain/metadata.rs index f6064a15..123c9fa6 100644 --- a/rust/devlaunch-core/src/domain/metadata.rs +++ b/rust/devlaunch-core/src/domain/metadata.rs @@ -1652,6 +1652,43 @@ mod tests { } } + #[test] + fn pythons_json_extensions_read_as_corruption_here() { + // Divergence row 28 (docs/rust-rewrite-plan.md): Python's `json.loads` + // accepts all four of these — `NaN` and `Infinity` as literals, `1e400` + // as `inf`, a lone surrogate escape as an unpaired code unit — where + // serde_json refuses them, so a file carrying one is quarantined (bytes + // intact, at `.corrupt`) and the run starts empty where Python kept the + // records. No released dl writes any of them; only a hand-edited or + // third-party-written file can. + for content in [ + r#"{"x": NaN}"#, + r#"{"x": Infinity}"#, + r#"{"x": 1e400}"#, + r#"{"x": "\ud800"}"#, + ] { + let dir = temp_dir(); + let path = dir.path().join("metadata.json"); + write(&path, content); + + let (storage, notices) = open_at(&path); + + assert!(storage.repositories().is_empty(), "{content:?}"); + let corrupt = dir.path().join("metadata.json.corrupt"); + assert_eq!(read(&corrupt), content, "the bytes stay inspectable"); + assert!( + matches!( + notices.as_slice(), + [Notice::FileUnusable { + problem: FileProblem::NotJson { .. }, + .. + }] + ), + "{content:?}: {notices:?}" + ); + } + } + #[test] fn a_json_value_that_is_not_an_object_says_what_it_found() { let dir = temp_dir(); From 912b709d6f5b6138b91a292f30b877c1c879b484 Mon Sep 17 00:00:00 2001 From: Austin Gregg-Smith Date: Wed, 19 Aug 2026 20:36:33 +0100 Subject: [PATCH 10/13] Demote what the binaries never name to pub(crate) (#262) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The crate docs promise that everything the dl/aid binaries do not need stays pub(crate); the actual surface had ~50 declarations pub with no consumer. Derived mechanically: enumerate every pub item, drop the ones whose names appear nowhere in dl, aid, or devlaunch-test-support and are not re-exported from `api`, then let the compiler referee — 27 candidates came back pub because a binary reaches them through bindings or field access without ever writing the name (Section, WrittenScript, the lifecycle report types, and friends). Three items stay pub with #[doc(hidden)] instead: Prose, Document (carried by timing::Report's variants) and ClosedDoors (carried by DiskUsage::PartlyUnreadable's field) — variant payloads inherit the enum's visibility, so they cannot be pub(crate) while their carrier is binary surface, but the binaries render via Report::lines / match DiskUsage without naming them. Eight demoted items turned out to be read only by in-crate tests (not_written, with_leading_args, the KnownWorkspace accessors, repointed, tools_present); they keep the crate's existing `#[cfg_attr(not(test), allow(dead_code))]` convention with a one-line why, rather than being deleted out from under parity-pinned tests. Visibility keywords only — no behavior change, no rendered byte changes; the four now-stale "binary surface" doc notes above demoted items are removed. --- .../devlaunch-core/src/domain/workspace_id.rs | 3 +- .../src/domain/workspace_state.rs | 3 +- .../src/flows/completion_cache.rs | 9 ++- rust/devlaunch-core/src/flows/disk_usage.rs | 5 ++ rust/devlaunch-core/src/flows/launch.rs | 10 +-- rust/devlaunch-core/src/flows/lifecycle.rs | 80 ++++++++++++------- rust/devlaunch-core/src/flows/provision.rs | 6 +- rust/devlaunch-core/src/flows/repo_manager.rs | 6 +- rust/devlaunch-core/src/notices.rs | 4 +- rust/devlaunch-core/src/shell.rs | 2 +- rust/devlaunch-core/src/timing.rs | 13 ++- rust/devlaunch-runner/src/interrupt.rs | 4 +- 12 files changed, 88 insertions(+), 57 deletions(-) diff --git a/rust/devlaunch-core/src/domain/workspace_id.rs b/rust/devlaunch-core/src/domain/workspace_id.rs index 8c8e43b1..14a04ec8 100644 --- a/rust/devlaunch-core/src/domain/workspace_id.rs +++ b/rust/devlaunch-core/src/domain/workspace_id.rs @@ -154,8 +154,7 @@ fn is_word(c: char) -> bool { } /// [`is_safe_name`] as a refusal that names the part it rejected. -// binary surface — not part of the frozen wf API (#251 §7) -pub fn validate_ref_name(name: &str, part: NamePart) -> Result<(), UnsafeName> { +pub(crate) fn validate_ref_name(name: &str, part: NamePart) -> Result<(), UnsafeName> { if is_safe_name(name) { Ok(()) } else { diff --git a/rust/devlaunch-core/src/domain/workspace_state.rs b/rust/devlaunch-core/src/domain/workspace_state.rs index 009025ce..9dc23252 100644 --- a/rust/devlaunch-core/src/domain/workspace_state.rs +++ b/rust/devlaunch-core/src/domain/workspace_state.rs @@ -200,8 +200,7 @@ impl CouldNotTell { } /// What a clone holds that exists nowhere else: at least one thing, always. -// binary surface — not part of the frozen wf API (#251 §7) -pub type Losses = NonEmpty; +pub(crate) type Losses = NonEmpty; /// One kind of loss. /// diff --git a/rust/devlaunch-core/src/flows/completion_cache.rs b/rust/devlaunch-core/src/flows/completion_cache.rs index 56b7cc79..a442a201 100644 --- a/rust/devlaunch-core/src/flows/completion_cache.rs +++ b/rust/devlaunch-core/src/flows/completion_cache.rs @@ -197,7 +197,7 @@ pub(crate) fn completion_cache_age(path: &Path, now: SystemTime) -> Option bool { +pub(crate) fn completion_cache_is_fresh(path: &Path) -> bool { is_fresh_at(path, SystemTime::now()) } @@ -235,7 +235,7 @@ pub fn wants_startup_cache_refresh>(args: &[S]) -> bool { /// could not be written is not a reason to fail the command that was warming it. /// They are carried out rather than dropped so a caller *may* say so. #[derive(Clone, Debug, PartialEq, Eq)] -pub struct CacheNotWritten { +pub(crate) struct CacheNotWritten { pub path: PathBuf, pub reason: String, } @@ -259,7 +259,10 @@ pub struct Refreshed { pub listing_refused: Option, /// Workspaces whose source devlaunch could not read, so a caller can say which. pub unreadable_sources: Vec, - pub not_written: Vec, + /// Cache writes that failed. Deliberately unrendered — Python's + /// `except OSError: pass` — so only this module's tests read it. + #[cfg_attr(not(test), allow(dead_code))] + pub(crate) not_written: Vec, } /// Rebuild the completion cache and write both files. diff --git a/rust/devlaunch-core/src/flows/disk_usage.rs b/rust/devlaunch-core/src/flows/disk_usage.rs index 877a608f..df83845e 100644 --- a/rust/devlaunch-core/src/flows/disk_usage.rs +++ b/rust/devlaunch-core/src/flows/disk_usage.rs @@ -76,6 +76,11 @@ const UNITS: [&str; 5] = ["KiB", "MiB", "GiB", "TiB", "PiB"]; /// /// The paths are carried rather than counted so a caller can say *which* door, /// which is the difference between a report someone can act on and a caveat. +/// +/// `pub` only because the [`DiskUsage::PartlyUnreadable`] variant carries it +/// and variant fields inherit the enum's visibility; the binary matches on +/// `DiskUsage` without naming this type. +#[doc(hidden)] #[derive(Debug, Clone, PartialEq, Eq)] pub struct ClosedDoors { first: PathBuf, diff --git a/rust/devlaunch-core/src/flows/launch.rs b/rust/devlaunch-core/src/flows/launch.rs index 414d72d3..d4d4a892 100644 --- a/rust/devlaunch-core/src/flows/launch.rs +++ b/rust/devlaunch-core/src/flows/launch.rs @@ -580,7 +580,7 @@ impl PixiCache { /// ask and by no later one, so a launch that forwards the token twice reports one /// notice rather than two. #[derive(Debug, Default)] -pub struct HostToken { +pub(crate) struct HostToken { asked: OnceCell, } @@ -684,7 +684,7 @@ impl Rebuild { /// four combinations three are meaningful and one — an `--id` with no identity — /// is unreachable only by the callers' good behaviour. These are the three. #[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum Naming<'a> { +pub(crate) enum Naming<'a> { /// A create: `--id ` goes to devpod, and that id is also the identity. Create { workspace_id: &'a str }, /// devpod already knows the workspace: no `--id`, and the identity is the id @@ -1389,7 +1389,7 @@ pub enum SessionRefused { /// one token lookup the launch shares — so they are one parameter rather than /// three repeated at each of five signatures. #[derive(Clone, Copy)] -pub struct SessionContext<'a> { +pub(crate) struct SessionContext<'a> { pub(crate) runner: &'a dyn Runner, pub(crate) host: &'a Host, pub(crate) token: &'a HostToken, @@ -1909,7 +1909,7 @@ impl Placement { } /// How devpod is told which workspace this is. - pub fn naming(&self) -> Naming<'_> { + pub(crate) fn naming(&self) -> Naming<'_> { match self { Self::Known { workspace_id, .. } | Self::Listed { workspace_id } => { Naming::Known { workspace_id } @@ -1920,7 +1920,7 @@ impl Placement { /// Whether a launch may attach straight away — Python's /// `custom_id is None and known_state == "Running"`. - pub fn is_running(&self) -> bool { + pub(crate) fn is_running(&self) -> bool { match self { Self::Known { state, .. } => state.is_running(), // Nothing said it was running, which is not the same as saying it is diff --git a/rust/devlaunch-core/src/flows/lifecycle.rs b/rust/devlaunch-core/src/flows/lifecycle.rs index 8aa3ef07..7badd33a 100644 --- a/rust/devlaunch-core/src/flows/lifecycle.rs +++ b/rust/devlaunch-core/src/flows/lifecycle.rs @@ -180,9 +180,11 @@ impl SelfInvocation { } /// `program`, run with these arguments in front of the command — Python's - /// `-m devlaunch.dl`. + /// `-m devlaunch.dl`. Only this module's tests build that shape; the Rust + /// binary is [`SelfInvocation::new`] with no leading arguments. #[must_use] - pub fn with_leading_args(mut self, args: I) -> Self + #[cfg_attr(not(test), allow(dead_code))] + pub(crate) fn with_leading_args(mut self, args: I) -> Self where I: IntoIterator, S: Into, @@ -197,7 +199,7 @@ impl SelfInvocation { /// `--force` is passed on rather than re-decided by the child, because the /// child re-checks the TTL and would otherwise skip a refresh that follows a /// workspace change — where the cache is wrong however new it is. - pub fn refresh_child(&self, reason: RefreshReason) -> Invocation { + pub(crate) fn refresh_child(&self, reason: RefreshReason) -> Invocation { let mut invocation = Invocation::new(&self.program) .with_args(self.leading_args.iter().cloned()) .with_arg(UPDATE_CACHE_FLAG); @@ -209,10 +211,10 @@ impl SelfInvocation { } /// The flag that puts a `dl` run in refresh-child mode. -pub const UPDATE_CACHE_FLAG: &str = "--update-cache"; +pub(crate) const UPDATE_CACHE_FLAG: &str = "--update-cache"; /// The flag that tells a refresh — parent or child — to ignore the TTL. -pub const FORCE_FLAG: &str = "--force"; +pub(crate) const FORCE_FLAG: &str = "--force"; /// Why a background refresh is being asked for. /// @@ -357,7 +359,7 @@ pub fn child_work(cache_path: &Path, reason: RefreshReason) -> ChildWork { /// child with no terminal attached — so these exist to be *counted* and to be /// visible under `DEVLAUNCH_TIMING`, not to be printed. #[derive(Debug, Clone, PartialEq, Eq)] -pub enum SweptRepo { +pub(crate) enum SweptRepo { /// The interval had elapsed and the fetch worked. Fetched { owner: String, repo: String }, /// The interval had not elapsed. Nothing was asked of the remote. @@ -392,7 +394,7 @@ pub enum SweptRepo { /// Everything one sweep did. #[derive(Debug, Default)] pub struct SweepReport { - pub repos: Vec, + pub(crate) repos: Vec, pub notices: Vec, } @@ -505,7 +507,7 @@ pub fn workspace_state( /// [`KnownWorkspace::Unknown`] carrying a recorded id, and no way to read a state /// off one. #[derive(Debug, Clone, PartialEq, Eq)] -pub enum KnownWorkspace { +pub(crate) enum KnownWorkspace { /// devpod knows this workspace and reported this state. The two come from one /// round trip: asking "which id" and then "what state" separately is how a /// command ends up addressing one workspace and reporting another's state. @@ -520,7 +522,10 @@ pub enum KnownWorkspace { impl KnownWorkspace { /// The id every later step addresses, whichever arm this is. - pub fn workspace_id(&self) -> &str { + /// + /// Only this module's tests read it; the flows match the arms directly. + #[cfg_attr(not(test), allow(dead_code))] + pub(crate) fn workspace_id(&self) -> &str { match self { Self::Known { workspace_id, .. } => workspace_id, Self::Unknown { derived } => derived, @@ -528,7 +533,11 @@ impl KnownWorkspace { } /// The state devpod gave, or nothing when it knows no such workspace. - pub fn state(&self) -> Option<&ContainerState> { + /// + /// Only this module's tests (via [`Self::is_running`]) read it; the flows + /// match the arms directly. + #[cfg_attr(not(test), allow(dead_code))] + pub(crate) fn state(&self) -> Option<&ContainerState> { match self { Self::Known { state, .. } => Some(state), Self::Unknown { .. } => None, @@ -536,7 +545,10 @@ impl KnownWorkspace { } /// Whether a launch may attach straight away. - pub fn is_running(&self) -> bool { + /// + /// Only this module's tests read it; the flows match the arms directly. + #[cfg_attr(not(test), allow(dead_code))] + pub(crate) fn is_running(&self) -> bool { matches!(self.state(), Some(state) if state.is_running()) } } @@ -578,7 +590,7 @@ impl KnownWorkspace { /// path it sends a launch down fetches a branch and builds a workspace clone on a /// host that cannot open it, and leaves both behind for the exit-127 to be /// discovered after. So the error is the runner's, and it travels. -pub fn resolve_known_workspace( +pub(crate) fn resolve_known_workspace( runner: &dyn Runner, triple: (&str, &str, &str), derived: &str, @@ -639,7 +651,7 @@ pub fn resolve_known_workspace( /// holding one has already handled the failure — and the way it handles it is to /// pass a closure that answers `None`, because a lookup that failed must not be /// able to stop a command that would otherwise have worked. -pub fn recorded_devpod_workspace_id( +pub(crate) fn recorded_devpod_workspace_id( storage: &MetadataStorage, owner: &str, repo: &str, @@ -1120,7 +1132,7 @@ fn purge_delete_call(workspace_id: &str) -> Call { /// no directory on this disk, so there is nothing to compare and no clone it could /// be holding. #[derive(Debug, Clone, PartialEq, Eq)] -pub enum SourcePlaces { +pub(crate) enum SourcePlaces { Placeable(Vec), /// The source opens a folder here and devlaunch cannot say which one. Kept /// apart from `Placeable(vec![])` because reading them alike is how a live @@ -1155,7 +1167,7 @@ pub enum SourcePlaces { /// directory on this machine — but never usably: the callers resolve plain paths, /// so it only ever produced `/file:/…` garbage. Contributing nothing is /// strictly less wrong. -pub fn names_a_remote(text: &str) -> bool { +pub(crate) fn names_a_remote(text: &str) -> bool { has_url_scheme(text) || is_scp_like(text) } @@ -1198,7 +1210,7 @@ fn is_scp_like(text: &str) -> bool { /// refuses the same arm on purpose — but refusing there means declining to delete /// somebody else's *workspace*, which is the opposite direction, so its answer must /// not be reused here. -pub fn source_places(source: &WorkspaceSource) -> SourcePlaces { +pub(crate) fn source_places(source: &WorkspaceSource) -> SourcePlaces { match source { WorkspaceSource::LocalFolder(path) => SourcePlaces::Placeable(vec![path.clone()]), WorkspaceSource::GitRepository(url) => SourcePlaces::Placeable(if names_a_remote(url) { @@ -1237,14 +1249,14 @@ pub struct Unlocatable { /// two records cannot be joined by workspace id — the id is exactly what changed — /// so the join is made from the path instead. #[derive(Debug, Clone, PartialEq, Eq)] -pub struct Misplaced { +pub(crate) struct Misplaced { pub workspace_id: String, pub sourced_at: String, } /// Where devpod's workspaces are on this disk, and which ones are unknown. #[derive(Debug, Clone, Default, PartialEq, Eq)] -pub struct WorkspaceLocations { +pub(crate) struct WorkspaceLocations { /// Resolved source path to the workspace that opens it. by_path: IndexMap, /// `unlocatable` is not an empty result with a note on it — see @@ -1291,7 +1303,7 @@ impl WorkspaceLocations { } /// The workspace disputing every clone of `(owner, repo)`, if any. - pub fn misplaced_in(&self, owner: &str, repo: &str) -> Option<&Misplaced> { + pub(crate) fn misplaced_in(&self, owner: &str, repo: &str) -> Option<&Misplaced> { self.misplaced.get(&(owner.to_owned(), repo.to_owned())) } } @@ -1303,7 +1315,7 @@ impl WorkspaceLocations { /// still says `/blooop/devlaunch/`, which names the repository /// exactly even though the leaf and the workspace id match nothing any more. #[derive(Debug, Clone, PartialEq, Eq)] -pub enum SourceSite { +pub(crate) enum SourceSite { /// Not in devlaunch's clone tree, so no clone answers for it. Outside, /// At or under `clone`, a directory that holds a checkout. @@ -1319,7 +1331,7 @@ pub enum SourceSite { /// The clone is the *third* component under the root and the source may be /// deeper — `devpod up /subproject` is a live workspace whose source is /// inside a clone, and the clone is what answers for it. -pub fn site_of(source: &Path, root: &Path) -> SourceSite { +pub(crate) fn site_of(source: &Path, root: &Path) -> SourceSite { let Ok(relative) = source.strip_prefix(root) else { return SourceSite::Outside; }; @@ -1423,7 +1435,7 @@ pub(crate) fn workspace_locations(workspaces: &[Workspace], root: &Path) -> Work /// /// `.git` may be a directory or a *file* (`git clone --separate-git-dir`), so this /// asks whether anything is there rather than whether a directory is. -pub fn is_populated_clone(path: &Path) -> bool { +pub(crate) fn is_populated_clone(path: &Path) -> bool { std::fs::metadata(path.join(".git")).is_ok() } @@ -1445,7 +1457,7 @@ pub fn is_populated_clone(path: &Path) -> bool { /// and the empty string, which names no directory (Python read it as `Path(".")` /// and resolved it to the working directory — the cwd-shaped answer devlaunch#224 /// is about). -pub fn canonical(path: &str) -> Option { +pub(crate) fn canonical(path: &str) -> Option { // A NUL byte is refused *here* rather than at the first syscall, because Rust // — unlike Python, where `Path(text)` raises `ValueError` before the `lstat` — // lets one into a `PathBuf` quite happily. Without this the walk below climbs @@ -1535,7 +1547,7 @@ pub fn objection(unsaved: &Unsaved) -> Option { /// Which arm one clone directory is. #[derive(Debug, Clone, PartialEq, Eq)] -pub enum CloneStatus { +pub(crate) enum CloneStatus { /// A live devpod workspace opens this exact clone directory. Referenced { workspace_id: String }, /// Nothing opens this directory and no record ties it to a live workspace. @@ -1579,7 +1591,7 @@ pub enum CloneStatus { /// removed. Together they are the expensive half of a scan (593 ms of git over 37 /// clones on the reference host, plus a walk with no ceiling), and asking them /// about a directory no answer could affect is time spent to learn nothing. -pub fn clone_status( +pub(crate) fn clone_status( git: &Git<'_>, clone: &Path, owner: &str, @@ -1658,7 +1670,7 @@ impl Promotion { /// What `--prune` does about one clone directory. #[derive(Debug, Clone, PartialEq, Eq)] -pub enum Decision { +pub(crate) enum Decision { /// This directory goes, this is what it gives back, and this is what was /// insisted past to get here. /// @@ -1684,7 +1696,7 @@ pub enum Decision { /// be insisted past", they are devlaunch saying the directory is still in use or /// that its own records disagree, and there is nothing for a user to mean by /// insisting. -pub fn decide(status: CloneStatus, insistence: Insistence) -> Decision { +pub(crate) fn decide(status: CloneStatus, insistence: Insistence) -> Decision { match status { CloneStatus::Referenced { workspace_id } => { Decision::Keep(KeptBecause::StillOpened { workspace_id }) @@ -2503,7 +2515,11 @@ pub fn devpod_home() -> Option { } /// devpod's own record for one workspace. -pub fn devpod_workspace_record(devpod_home: &Path, context: &str, workspace_id: &str) -> PathBuf { +pub(crate) fn devpod_workspace_record( + devpod_home: &Path, + context: &str, + workspace_id: &str, +) -> PathBuf { devpod_home .join("contexts") .join(context) @@ -2546,7 +2562,7 @@ pub enum RepointFailure { /// provider options, timestamps — survives untouched. Written through a temporary /// file in the same directory and renamed over the original, so a failure partway /// leaves devpod's record whole rather than truncated. -pub fn repoint_devpod_source( +pub(crate) fn repoint_devpod_source( devpod_home: &Path, adoptable: &Adoptable, ) -> Result<(), RepointFailure> { @@ -2632,8 +2648,10 @@ impl ReconcileReport { &self.adoptions } - /// The adoptions that landed. - pub fn repointed(&self) -> impl Iterator { + /// The adoptions that landed. Only this module's tests read it; the binary + /// renders each [`Adoption`] from [`ReconcileReport::adoptions`]. + #[cfg_attr(not(test), allow(dead_code))] + pub(crate) fn repointed(&self) -> impl Iterator { self.adoptions.iter().filter_map(|adoption| match adoption { Adoption::Repointed(adoptable) => Some(adoptable.as_ref()), Adoption::Refused { .. } => None, diff --git a/rust/devlaunch-core/src/flows/provision.rs b/rust/devlaunch-core/src/flows/provision.rs index a3fbd7a5..002d295b 100644 --- a/rust/devlaunch-core/src/flows/provision.rs +++ b/rust/devlaunch-core/src/flows/provision.rs @@ -1424,8 +1424,10 @@ pub enum Provisioning { } impl Provisioning { - /// Whether the tools are now there — the bool Python returns. - pub fn tools_present(&self) -> bool { + /// Whether the tools are now there — the bool Python returns. Only this + /// module's tests read it; the binary matches the arms directly. + #[cfg_attr(not(test), allow(dead_code))] + pub(crate) fn tools_present(&self) -> bool { match self { Self::AlreadyProvisioned | Self::Lent | Self::ShimKept | Self::Installed => true, Self::InstallRefused { .. } | Self::Disabled | Self::TripRefused { .. } => false, diff --git a/rust/devlaunch-core/src/flows/repo_manager.rs b/rust/devlaunch-core/src/flows/repo_manager.rs index 5bd72c47..6416ffec 100644 --- a/rust/devlaunch-core/src/flows/repo_manager.rs +++ b/rust/devlaunch-core/src/flows/repo_manager.rs @@ -905,8 +905,7 @@ pub enum CloneError { /// Why a repository's whole ref set could not be swept. #[derive(Debug, Clone, PartialEq, Eq)] -// binary surface — not part of the frozen wf API (#251 §7) -pub enum FetchRepoError { +pub(crate) enum FetchRepoError { /// There is no clone to fetch into. NoLocalClone { owner: String, @@ -932,8 +931,7 @@ pub enum FetchRepoError { /// Why a conditional sweep could not run. #[derive(Debug, Clone, PartialEq, Eq)] -// binary surface — not part of the frozen wf API (#251 §7) -pub enum LazyFetchError { +pub(crate) enum LazyFetchError { /// The repository is not in `metadata.json`, so there is no clock to compare /// against. NotInMetadata { diff --git a/rust/devlaunch-core/src/notices.rs b/rust/devlaunch-core/src/notices.rs index 24164a64..a4275964 100644 --- a/rust/devlaunch-core/src/notices.rs +++ b/rust/devlaunch-core/src/notices.rs @@ -39,7 +39,7 @@ pub trait Notices { /// so could not be called through the `&mut dyn` the flows are handed. There is /// nothing for a sink to get wrong here: saying several is saying each. impl dyn Notices + '_ { - pub fn say_all(&mut self, notices: impl IntoIterator) { + pub(crate) fn say_all(&mut self, notices: impl IntoIterator) { for notice in notices { self.say(notice); } @@ -63,7 +63,7 @@ impl Notices for Vec { /// pointer and not a closure because every use of it *is* a plain function — an /// enum variant's constructor, or a `match` over the arms — and a function pointer /// keeps the type free of the lifetime a closure would carry. -pub struct Wrapped<'a, T, U> { +pub(crate) struct Wrapped<'a, T, U> { inner: &'a mut dyn Notices, wrap: fn(T) -> U, } diff --git a/rust/devlaunch-core/src/shell.rs b/rust/devlaunch-core/src/shell.rs index 8ada98eb..91965cae 100644 --- a/rust/devlaunch-core/src/shell.rs +++ b/rust/devlaunch-core/src/shell.rs @@ -55,7 +55,7 @@ pub fn join<'a>(words: impl IntoIterator) -> String { /// [`quote`], because who has to care differs: a `-- ` from a command line is /// refused with it (row 19), and a generated script's interpolated workspace id /// cannot contain one to begin with. -pub fn holds_nul(word: &str) -> bool { +pub(crate) fn holds_nul(word: &str) -> bool { word.contains('\0') } diff --git a/rust/devlaunch-core/src/timing.rs b/rust/devlaunch-core/src/timing.rs index 73ed089f..5f1d9a01 100644 --- a/rust/devlaunch-core/src/timing.rs +++ b/rust/devlaunch-core/src/timing.rs @@ -277,7 +277,7 @@ fn now_epoch() -> f64 { /// One timed round trip: what it was, and how long it took. #[derive(Debug, Clone, PartialEq, Eq)] -pub struct SpanRecord { +pub(crate) struct SpanRecord { pub(crate) label: String, pub seconds: Duration, } @@ -578,10 +578,14 @@ impl Report { } /// The human summary: one line per round trip, then the total. +/// +/// `pub` only because [`Report::Prose`] carries it and `Report` is binary +/// surface; the binary renders [`Report::lines`] and never names this type. +#[doc(hidden)] #[derive(Debug, Clone, PartialEq, Eq)] pub struct Prose { - pub spans: Vec, - pub total: Duration, + pub(crate) spans: Vec, + pub(crate) total: Duration, } impl Prose { @@ -611,6 +615,9 @@ impl Prose { /// and parses what follows, which no amount of surrounding output can break. /// Field order is the order Python's dict was built in, so the two binaries' /// documents are byte-comparable. +/// `pub` only because [`Report::Document`] carries it and `Report` is binary +/// surface; the binary renders [`Report::lines`] and never names this type. +#[doc(hidden)] #[derive(Debug, Clone, PartialEq, Serialize)] pub struct Document { pub(crate) total: f64, diff --git a/rust/devlaunch-runner/src/interrupt.rs b/rust/devlaunch-runner/src/interrupt.rs index abc3e2f7..d3fe9ef9 100644 --- a/rust/devlaunch-runner/src/interrupt.rs +++ b/rust/devlaunch-runner/src/interrupt.rs @@ -128,13 +128,13 @@ fn register_in(slots: &'static [AtomicPtr], path: &Path) -> Option /// Record the process group of the foreground child now being waited on, so the /// interrupt handler can tear it down. Paired with [`clear_foreground_child`] /// once the child is reaped. -pub fn note_foreground_child(pgid: i32) { +pub(crate) fn note_foreground_child(pgid: i32) { FOREGROUND_PGID.store(pgid, Ordering::SeqCst); } /// Forget the foreground child: it has been reaped, so the handler must not /// signal its (possibly recycled) process group id. -pub fn clear_foreground_child() { +pub(crate) fn clear_foreground_child() { FOREGROUND_PGID.store(0, Ordering::SeqCst); } From f368147bceff81955bd0ca118dc2d07b9f18f9dc Mon Sep 17 00:00:00 2001 From: Austin Gregg-Smith Date: Wed, 19 Aug 2026 20:36:47 +0100 Subject: [PATCH 11/13] Keep the completion payload inside the package root (#262) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DL_COMPLETION_BASH was include_str! of ../../../../devlaunch/completions/ dl.bash — a path escaping devlaunch-core's package root, harmless for the path/git dependency wf uses but fatal for cargo publish. The crate now embeds its own completions/dl.bash, byte-identical to the copy the frozen Python package ships (Python reads its copy via importlib.resources, so it cannot be pointed at the crate's), and a new test — the_embedded_payload_matches_the_python_packages_copy — pins the two copies together: whichever twin is edited alone turns the devlaunch-core suite red in CI. The embedded bytes are unchanged, so every golden-pinned completion cache and rc-block test passes untouched, and `cargo package -p devlaunch-core --list` now succeeds with completions/dl.bash inside the crate. --- rust/devlaunch-core/completions/dl.bash | 173 ++++++++++++++++++++ rust/devlaunch-core/src/flows/completion.rs | 55 +++++-- 2 files changed, 212 insertions(+), 16 deletions(-) create mode 100644 rust/devlaunch-core/completions/dl.bash diff --git a/rust/devlaunch-core/completions/dl.bash b/rust/devlaunch-core/completions/dl.bash new file mode 100644 index 00000000..056444c8 --- /dev/null +++ b/rust/devlaunch-core/completions/dl.bash @@ -0,0 +1,173 @@ +# dl completion +# Note: This completion function does not support quoted arguments or escaped spaces. +# All arguments are treated as literal strings separated by whitespace. +# This is acceptable because GitHub usernames, repo names, and workspace names +# do not contain spaces or special characters that would require quoting. +# +# Implementation note: We parse COMP_LINE directly instead of adjusting COMP_WORDBREAKS +# because temporary COMP_WORDBREAKS modification can have side effects with bash's +# internal completion state and doesn't reliably prevent word splitting in all +# bash versions. Direct parsing gives us full control over word boundaries. +# +# The same function serves `aid`, whose first argument is a dl workspace spec +# too. Only the flag list and what follows the spec differ, so the two places +# that care branch on $cmd rather than the script being copied for aid. +_dl_completion() { + local cur prev opts + COMPREPLY=() + + # Extract current line from COMP_LINE instead of COMP_WORDS + # This avoids issues with COMP_WORDBREAKS treating dashes as word boundaries + local line="${COMP_LINE:0:COMP_POINT}" + + # Parse the line into an array of words (pure bash, no external processes) + local words + read -r -a words <<< "$line" + local word_count=${#words[@]} + + # Extract current and previous words from the parsed array + if (( word_count > 0 )); then + cur="${words[word_count-1]}" + else + cur="" + fi + + if (( word_count > 1 )); then + prev="${words[word_count-2]}" + else + prev="" + fi + + # If line ends with whitespace, we're starting a new word + if [[ "$line" =~ [[:space:]]$ ]]; then + ((word_count++)) + cur="" + # Update prev when starting a new word + if (( ${#words[@]} > 0 )); then + prev="${words[-1]}" + fi + fi + + # The command being completed: dl or aid. + local cmd="" + if (( ${#words[@]} > 0 )); then + cmd="${words[0]##*/}" + fi + + # Global command options (only valid as first arg) + local global_opts="--ls --install --help -h --version --devcontainer" + if [[ "$cmd" == aid ]]; then + global_opts="--claude --codex --gemini --devcontainer --help -h --version" + fi + + # Workspace subcommands + local ws_cmds="up stop rm code restart recreate reset dotfiles --" + + # Options that take a value; a variant name or a path follows them. + local value_opts="--devcontainer" + + # After --devcontainer, offer the repo's variant directories (and paths). + if [[ " ${value_opts} " == *" ${prev} "* ]]; then + local variants="" + if [[ -d .devcontainer ]]; then + local d + for d in .devcontainer/*/devcontainer.json; do + [[ -f "$d" ]] || continue + d="${d#.devcontainer/}" + variants+=" ${d%/devcontainer.json}" + done + fi + COMPREPLY=( $(compgen -W "${variants}" -- ${cur}) ) + compopt -o default 2>/dev/null + return 0 + fi + + # Cache file location (honors XDG_CACHE_HOME) + local cache_dir="${XDG_CACHE_HOME:-$HOME/.cache}/devlaunch" + local cache_file="$cache_dir/completions.bash" + + # Initialize completion variables + local DL_WORKSPACES="" + local DL_REPOS="" + local DL_OWNERS="" + local DL_BRANCHES="" + + # Source the bash cache file (fast, no jq needed) + if [[ -f "$cache_file" ]]; then + source "$cache_file" + fi + + # First argument: global flags, workspaces, repos, owners, or paths + if [[ ${word_count} -eq 2 ]]; then + # Global flags + if [[ ${cur} == -* ]]; then + COMPREPLY=( $(compgen -W "${global_opts}" -- ${cur}) ) + return 0 + fi + + # If typing a path, complete files/directories + if [[ "$cur" == ./* || "$cur" == /* || "$cur" == ~/* ]]; then + COMPREPLY=( $(compgen -d -- ${cur}) ) + return 0 + fi + + # Check if completing branch (contains @) + if [[ "$cur" == *@* ]]; then + # Use cached branches (format: owner/repo@branch) + if [[ -n "$DL_BRANCHES" ]]; then + COMPREPLY=( $(compgen -W "${DL_BRANCHES}" -- ${cur}) ) + fi + return 0 + fi + + # Check if completing owner/repo format (contains /) + if [[ "$cur" == */* ]]; then + # Don't add space - allow @branch suffix + compopt -o nospace + # Complete from known repos + if [[ -n "$DL_REPOS" ]]; then + COMPREPLY=( $(compgen -W "${DL_REPOS}" -- ${cur}) ) + fi + return 0 + fi + + # Default: complete workspace names and offer owner/ completion + compopt -o nospace # For owner/ completions + local completions="$DL_WORKSPACES" + + # Add owners with trailing slash + for owner in $DL_OWNERS; do + completions="$completions ${owner}/" + done + + if [[ -n "$completions" ]]; then + COMPREPLY=( $(compgen -W "${completions}" -- ${cur}) ) + fi + return 0 + fi + + # Second argument (after workspace): subcommands. Everything after an aid + # workspace is the prompt, so there is nothing to offer there. + if [[ ${word_count} -eq 3 && "$cmd" != aid ]]; then + # Don't complete after global flags + # Extract the first argument (word after "dl") from the words array + local first="" + if (( ${#words[@]} > 1 )); then + first="${words[1]}" + fi + if [[ "$first" == --* ]]; then + return 0 + fi + + COMPREPLY=( $(compgen -W "${ws_cmds}" -- ${cur}) ) + return 0 + fi + + # After "--": no completion (user types shell command) + return 0 +} + +# Use -o default for better completion behavior +complete -o default -F _dl_completion dl +complete -o default -F _dl_completion aid +# end dl completion diff --git a/rust/devlaunch-core/src/flows/completion.rs b/rust/devlaunch-core/src/flows/completion.rs index 19c2c688..09dc4c18 100644 --- a/rust/devlaunch-core/src/flows/completion.rs +++ b/rust/devlaunch-core/src/flows/completion.rs @@ -24,11 +24,13 @@ //! //! # Three seams worth naming //! -//! - **The payload is shared, not copied.** [`DL_COMPLETION_BASH`] is -//! `include_str!` of `devlaunch/completions/dl.bash` — the same file the Python -//! package ships — so while both binaries exist there is one copy of the bash -//! and they cannot drift. At cutover the file moves under `rust/` and only this -//! one path changes; nothing else here knows where it came from. +//! - **The payload lives in this crate, and its twin is guarded.** +//! [`DL_COMPLETION_BASH`] is `include_str!` of this package's own +//! `completions/dl.bash`. The frozen Python package ships its own copy at +//! `devlaunch/completions/dl.bash` (read at runtime via +//! `importlib.resources`, so it cannot be pointed here), and a test asserts +//! the two are byte-identical — one edited without the other is a red test, +//! not a silent drift. //! - **What the payload reads is not this module's business.** The script sources //! `${XDG_CACHE_HOME:-$HOME/.cache}/devlaunch/completions.bash` at completion //! time, and *writing* that cache is the listing side's job (Python's @@ -47,18 +49,18 @@ use crate::domain::xdg::NoHomeDirectory; /// The shipped bash completion payload, embedded at compile time. /// -/// One source of truth: the file the Python package ships. Python resolved it as -/// package data at runtime (`importlib.resources`), which could fail; embedding -/// it removes that failure entirely — a build that compiled has the script. +/// Python resolved its copy as package data at runtime (`importlib.resources`), +/// which could fail; embedding removes that failure entirely — a build that +/// compiled has the script. /// -/// The path escapes this crate's package root — it reaches up into the sibling -/// `devlaunch/` Python package — so `devlaunch-core` is **not** `cargo publish`- -/// able and is consumed only as a path or git dependency, which is exactly how -/// `wf` links it. That is a deliberate constraint of the single-source rule -/// above, not an oversight: the day the Python package is gone this include -/// moves in-tree; until then it must point at the file that actually ships. -pub(crate) const DL_COMPLETION_BASH: &str = - include_str!("../../../../devlaunch/completions/dl.bash"); +/// The path stays inside this crate's package root on purpose: an earlier shape +/// reached up into the sibling `devlaunch/` Python package, which made +/// `devlaunch-core` un-`cargo publish`-able (a path escaping the root is fatal +/// there, harmless everywhere else). The frozen Python package still ships its +/// own copy at `devlaunch/completions/dl.bash`; the two are kept byte-identical +/// by [`tests::the_embedded_payload_matches_the_python_packages_copy`], so this +/// is one source of truth with a guarded twin, not a fork. +pub(crate) const DL_COMPLETION_BASH: &str = include_str!("../../completions/dl.bash"); /// The override for where the completion script is written. pub(crate) const COMPLETION_FILE_VAR: &str = "DEVLAUNCH_COMPLETION_FILE"; @@ -567,6 +569,27 @@ mod tests { ); } + #[test] + fn the_embedded_payload_matches_the_python_packages_copy() { + // The twin guard. This crate embeds its own `completions/dl.bash` so the + // include never escapes the package root (which would make the crate + // un-publishable); the frozen Python package ships its copy at + // `devlaunch/completions/dl.bash` and reads it via `importlib.resources`, + // so it cannot be pointed here. Byte-identical or this test is red — + // whichever copy was edited, edit the other the same way. (The Python + // copy only exists in this repository, so a missing twin means the + // layout changed and this guard needs re-aiming, not deleting.) + let twin = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../devlaunch/completions/dl.bash"); + let python_copy = fs::read_to_string(&twin) + .unwrap_or_else(|error| panic!("cannot read the twin at {twin:?}: {error}")); + assert_eq!( + DL_COMPLETION_BASH, python_copy, + "rust/devlaunch-core/completions/dl.bash and devlaunch/completions/dl.bash \ + have drifted; make them byte-identical again" + ); + } + #[test] fn the_payload_reads_the_cache_the_listing_side_writes() { // The seam to M5b, pinned from this side: whatever writes From 1265a2be7e28ad5e3e5809624ad8ca1beba4ec76 Mon Sep 17 00:00:00 2001 From: Austin Gregg-Smith Date: Wed, 19 Aug 2026 20:45:59 +0100 Subject: [PATCH 12/13] Unlink the doc references the demotions made private (#262) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thirteen intra-doc links in public docs pointed at items the previous commit demoted to pub(crate), which rustdoc flags as private_intra_doc_links. Plain code spans now — the prose is unchanged. The ~98 warnings of the same kind that remain all predate #262: they link items that were already pub(crate) before this ticket. --- rust/devlaunch-core/src/domain/workspace_state.rs | 4 ++-- rust/devlaunch-core/src/flows/launch.rs | 6 +++--- rust/devlaunch-core/src/flows/lifecycle.rs | 12 ++++++------ rust/devlaunch-core/src/notices.rs | 2 +- rust/devlaunch-core/src/shell.rs | 2 +- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/rust/devlaunch-core/src/domain/workspace_state.rs b/rust/devlaunch-core/src/domain/workspace_state.rs index 9dc23252..b86dfe0a 100644 --- a/rust/devlaunch-core/src/domain/workspace_state.rs +++ b/rust/devlaunch-core/src/domain/workspace_state.rs @@ -63,7 +63,7 @@ //! being read as permission to delete — which is what the guard was for. //! - `WouldLose.__post_init__` raised on an empty description, because a //! description was a string a caller could get wrong. Here a `WouldLose` -//! carries [`Losses`], which cannot be empty by construction, and the +//! carries `Losses`, which cannot be empty by construction, and the //! description is *derived* from it — so the illegal value has no //! representation rather than a check. //! @@ -91,7 +91,7 @@ pub enum Unsaved { NothingToLose, /// Deleting the clone would destroy this. /// - /// [`Losses`] rather than a flag, and that is load-bearing: what it + /// `Losses` rather than a flag, and that is load-bearing: what it /// [`describes`](Losses::describe) is printed to a person deciding whether to /// force the delete, and "3 uncommitted change(s) (pixi.lock, notes.md, …) /// and 2 unpushed commit(s)" is the thing that answers them. See diff --git a/rust/devlaunch-core/src/flows/launch.rs b/rust/devlaunch-core/src/flows/launch.rs index d4d4a892..8447be69 100644 --- a/rust/devlaunch-core/src/flows/launch.rs +++ b/rust/devlaunch-core/src/flows/launch.rs @@ -45,7 +45,7 @@ //! one value. //! //! Same for the `up` request. Python takes `workspace_id`, `workspace_identity`, -//! `recreate: bool` and `reset: bool`; [`Naming`] is the three shapes the first +//! `recreate: bool` and `reset: bool`; `Naming` is the three shapes the first //! two are ever in (and makes "an `--id` with no identity" unrepresentable), and //! [`Rebuild`] is a sum so `--recreate --reset` cannot be asked for at once. //! @@ -279,7 +279,7 @@ pub enum LaunchNotice { // --- the token (gh_auth.py, memoized here) /// The host has no GitHub token to forward, and this is why. One per launch - /// however often the token is asked for — see [`HostToken`]. + /// however often the token is asked for — see `HostToken`. NoGitHubToken(GhEvent), /// The token could not be written to the private file `devpod up` reads it /// from, so this workspace opens without a GitHub login. Python distinguishes @@ -1950,7 +1950,7 @@ pub enum Resolution { /// all. /// /// Charged to the `devpod-up` stage with a `devpod status` span, for the reason -/// [`is_running`] gives: the span belongs inside the stage the lifecycle helper +/// `is_running` gives: the span belongs inside the stage the lifecycle helper /// opens, and a guard around the call would drop after that stage had closed. /// A devpod that could not be run at all travels out as [`NotRun`] rather than /// becoming a cold launch: see [`lifecycle::resolve_known_workspace`] for what a diff --git a/rust/devlaunch-core/src/flows/lifecycle.rs b/rust/devlaunch-core/src/flows/lifecycle.rs index 7badd33a..34442d71 100644 --- a/rust/devlaunch-core/src/flows/lifecycle.rs +++ b/rust/devlaunch-core/src/flows/lifecycle.rs @@ -420,7 +420,7 @@ pub struct SweepReport { /// the network's — [`BACKGROUND_FETCH_TIMEOUT`], without which a remote that /// accepts a connection and then goes quiet holds the repository for as long as /// the kernel keeps the socket. -/// - **It never complains.** Every failure is an arm of [`SweptRepo`] and the loop +/// - **It never complains.** Every failure is an arm of `SweptRepo` and the loop /// carries on. /// /// The interval itself is unchanged and still recorded in the one shared place @@ -1519,7 +1519,7 @@ fn subdirectories(path: &Path) -> Vec { /// What removing a clone would destroy or risk — the two answers `--prune` acts /// on. /// -/// The one place devlaunch#171's three answers become the two [`decide`] acts on: +/// The one place devlaunch#171's three answers become the two `decide` acts on: /// something to say, or nothing. "Could not tell" arrives here as an *objection* /// rather than as an absence, so the clone is kept for the same reason unpushed /// work keeps one. @@ -1748,11 +1748,11 @@ pub struct Kept { /// Everything one `dl --prune` will do, settled before anything is asked. /// -/// The two lists are built by one pass over one [`decide`] call each, so a +/// The two lists are built by one pass over one `decide` call each, so a /// directory cannot be in both and cannot be in neither. /// /// There is deliberately no `force` field. It was one, and a plan-wide boolean is -/// exactly the shape [`decide`] refuses to have beside a status: the pass that acts +/// exactly the shape `decide` refuses to have beside a status: the pass that acts /// read it and skipped its safety re-check for every directory, including the ones /// `--force` had promoted nothing about. What `--force` answered rides on each /// [`Reclaimable`] instead. @@ -1847,7 +1847,7 @@ impl ClonePlacement { } /// The live workspaces this command cannot place, or nothing when every one - /// of them placed itself. See [`WorkspaceLocations::unlocatable`]. + /// of them placed itself. See `WorkspaceLocations::unlocatable`. pub fn unlocatable(&self) -> Option> { self.locations.unlocatable() } @@ -2370,7 +2370,7 @@ fn orphaned_workspaces(workspaces: &[Workspace], root: &Path) -> Vec<(Workspace, /// /// Two candidates are refused rather than resolved, in both directions. A clone a /// live workspace already opens — at it *or under it*, which is what -/// [`WorkspaceLocations::holder`] is for — is not a candidate at all: adopting it +/// `WorkspaceLocations::holder` is for — is not a candidate at all: adopting it /// would point two workspaces at one directory and leave the working one sharing its /// checkout with a dead one. The rest is [`NotAdopted`]'s three arms. pub fn reconcile_plan( diff --git a/rust/devlaunch-core/src/notices.rs b/rust/devlaunch-core/src/notices.rs index a4275964..ee893317 100644 --- a/rust/devlaunch-core/src/notices.rs +++ b/rust/devlaunch-core/src/notices.rs @@ -16,7 +16,7 @@ //! what a command that prints its notices under a report still wants. //! - anything the caller writes — the `dl` binary's printer — says it now. //! -//! [`Wrapped`] is the third piece, and it is what keeps one vocabulary per layer: +//! `Wrapped` is the third piece, and it is what keeps one vocabulary per layer: //! a storage flow reports [`CacheNotice`](crate::flows::repo_manager::CacheNotice) //! whether it was called by a launch (whose vocabulary is `LaunchNotice`) or by a //! lifecycle command, and the wrapping happens in one place per boundary rather diff --git a/rust/devlaunch-core/src/shell.rs b/rust/devlaunch-core/src/shell.rs index 91965cae..c6b49b8c 100644 --- a/rust/devlaunch-core/src/shell.rs +++ b/rust/devlaunch-core/src/shell.rs @@ -26,7 +26,7 @@ use std::borrow::Cow; /// `word` as one shell word, as Python's `shlex.quote` writes it. /// /// Total, as Python's is: a NUL is quoted like any other character here. The -/// callers that must *refuse* a NUL ask [`holds_nul`] first, because refusing is +/// callers that must *refuse* a NUL ask `holds_nul` first, because refusing is /// their contract (docs/rust-rewrite-plan.md row 19) rather than the quoting's — and /// a caller whose input cannot hold one (a workspace id read out of argv) needs no /// refusal at all and would otherwise be handed an error it cannot explain. From 917a43d2b49ddaef62e1ca78525d8d639933037e Mon Sep 17 00:00:00 2001 From: Austin Gregg-Smith Date: Wed, 19 Aug 2026 20:45:59 +0100 Subject: [PATCH 13/13] Lock the public surface with a cargo-public-api snapshot in CI (#262) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit rust/devlaunch-core/public-api.txt is the crate's whole public surface as `cargo public-api -p devlaunch-core -ss` renders it (blanket and auto-trait impls omitted — those rows move when rustdoc moves, not when the crate does; derived impls kept, losing one is a real break). A new `public-api` job regenerates it on every pull request and fails on any difference, naming the regenerate-and-commit command, and feeds `gate` like every other job. The crate docs' "until that lands" caveat about this snapshot is retired. Generated and verified locally with cargo-public-api 0.52.0 on rustc 1.100.0-nightly (e71c0f1e3 2026-08-18); two consecutive runs are byte-identical, and the CI diff step passes against this snapshot. --- .github/workflows/ci.yml | 46 +- rust/devlaunch-core/public-api.txt | 2231 ++++++++++++++++++++++++++++ rust/devlaunch-core/src/lib.rs | 6 +- 3 files changed, 2280 insertions(+), 3 deletions(-) create mode 100644 rust/devlaunch-core/public-api.txt diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 951ede38..c30df08a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -265,6 +265,50 @@ jobs: - name: Parity compare (checked-in cases) run: python3 rust/tools/compare.py --cases rust/tools/parity_cases.txt --release --fingerprint + # The API-freeze tripwire (#251 §7, #262). devlaunch-core's crate docs promise + # two tiers of `pub` — the frozen wf API behind `api`, and binary surface that + # is reachable but not promised — and this job is what makes that promise + # checkable rather than prose: rust/devlaunch-core/public-api.txt is the whole + # public surface as `cargo public-api` renders it, and any change to it fails + # here until the new snapshot is committed. That turns every surface change + # into a reviewed diff: an addition to `api` is a deliberate PR, a removal a + # visible breaking change, and an accidental `pub` a red tick instead of a + # silent promise. + # + # Nightly because cargo-public-api's rustdoc-JSON backend needs it; the crate + # itself still builds on the stable pin everywhere else. `-ss` (omit blanket + # and auto-trait impls) rather than the full listing, because auto-trait rows + # move when *rustdoc* moves — `UnsafeUnpin` appeared with a nightly, not with + # a crate change — and a tripwire that fires on toolchain drift teaches people + # to update the snapshot unread. Derived impls (Clone, Debug, serde) stay in: + # losing one is a real API break. cargo-public-api is pinned for the same + # reason; bump it together with a freshly generated snapshot. + public-api: + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - name: Checkout + uses: actions/checkout@v6 + - uses: dtolnay/rust-toolchain@nightly + - uses: Swatinem/rust-cache@v2 + with: + workspaces: rust + - name: Install cargo-public-api (pinned) + run: cargo install cargo-public-api --locked --version 0.52.0 + - name: The public surface is the snapshot the repo carries + working-directory: rust + run: | + set -euo pipefail + cargo public-api -p devlaunch-core -ss > /tmp/public-api.txt + if ! diff -u devlaunch-core/public-api.txt /tmp/public-api.txt; then + echo "::error::devlaunch-core's public API changed. If the change is" \ + "deliberate, regenerate the snapshot and commit it:" \ + "cd rust && cargo +nightly public-api -p devlaunch-core -ss" \ + "> devlaunch-core/public-api.txt (needs cargo install" \ + "cargo-public-api --locked --version 0.52.0 on a nightly toolchain)." + exit 1 + fi + # The release mechanics, exercised on every pull request instead of on release # day. Both published artifacts are compiled now (docs/rust-rewrite-plan.md M10): # a maturin bin-wheel for PyPI and a `cargo install` conda package for @@ -387,7 +431,7 @@ jobs: # whole workflow: a check nobody ran reads exactly like a check that passed, # and a gate that covers nothing is the purest form of that. gate: - needs: [ci, e2e, rust-parity, packaging] + needs: [ci, e2e, rust-parity, public-api, packaging] if: always() runs-on: ubuntu-latest steps: diff --git a/rust/devlaunch-core/public-api.txt b/rust/devlaunch-core/public-api.txt new file mode 100644 index 00000000..7ff5c5de --- /dev/null +++ b/rust/devlaunch-core/public-api.txt @@ -0,0 +1,2231 @@ +pub mod devlaunch_core +pub mod devlaunch_core::api +pub enum devlaunch_core::api::LaunchVerb +pub devlaunch_core::api::LaunchVerb::Attach +pub devlaunch_core::api::LaunchVerb::Attach::command: core::option::Option +pub devlaunch_core::api::LaunchVerb::Code +pub devlaunch_core::api::LaunchVerb::Dotfiles +pub devlaunch_core::api::LaunchVerb::Recreate +pub devlaunch_core::api::LaunchVerb::Reset +pub devlaunch_core::api::LaunchVerb::Restart +pub devlaunch_core::api::LaunchVerb::Up +impl core::clone::Clone for devlaunch_core::flows::launch::LaunchVerb +pub fn devlaunch_core::flows::launch::LaunchVerb::clone(&self) -> devlaunch_core::flows::launch::LaunchVerb +impl core::cmp::Eq for devlaunch_core::flows::launch::LaunchVerb +impl core::cmp::PartialEq for devlaunch_core::flows::launch::LaunchVerb +pub fn devlaunch_core::flows::launch::LaunchVerb::eq(&self, &devlaunch_core::flows::launch::LaunchVerb) -> bool +impl core::fmt::Debug for devlaunch_core::flows::launch::LaunchVerb +pub fn devlaunch_core::flows::launch::LaunchVerb::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::launch::LaunchVerb +pub enum devlaunch_core::api::SpecIdentity<'a> +pub devlaunch_core::api::SpecIdentity::ExistingName(&'a str) +pub devlaunch_core::api::SpecIdentity::PathLeaf(&'a str) +pub devlaunch_core::api::SpecIdentity::RepoLabel(alloc::string::String) +pub devlaunch_core::api::SpecIdentity::Workspace(alloc::string::String) +impl<'a> core::clone::Clone for devlaunch_core::domain::spec::SpecIdentity<'a> +pub fn devlaunch_core::domain::spec::SpecIdentity<'a>::clone(&self) -> devlaunch_core::domain::spec::SpecIdentity<'a> +impl<'a> core::cmp::Eq for devlaunch_core::domain::spec::SpecIdentity<'a> +impl<'a> core::cmp::PartialEq for devlaunch_core::domain::spec::SpecIdentity<'a> +pub fn devlaunch_core::domain::spec::SpecIdentity<'a>::eq(&self, &devlaunch_core::domain::spec::SpecIdentity<'a>) -> bool +impl<'a> core::fmt::Debug for devlaunch_core::domain::spec::SpecIdentity<'a> +pub fn devlaunch_core::domain::spec::SpecIdentity<'a>::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl<'a> core::marker::StructuralPartialEq for devlaunch_core::domain::spec::SpecIdentity<'a> +pub enum devlaunch_core::api::WorkspaceSpec<'a> +pub devlaunch_core::api::WorkspaceSpec::ExistingIdOrName(&'a str) +pub devlaunch_core::api::WorkspaceSpec::HostPath(&'a str) +pub devlaunch_core::api::WorkspaceSpec::OwnerRepo +pub devlaunch_core::api::WorkspaceSpec::OwnerRepo::branch: core::option::Option<&'a str> +pub devlaunch_core::api::WorkspaceSpec::OwnerRepo::owner: &'a str +pub devlaunch_core::api::WorkspaceSpec::OwnerRepo::repo: &'a str +pub devlaunch_core::api::WorkspaceSpec::Path(&'a str) +pub devlaunch_core::api::WorkspaceSpec::SshUrl(&'a str) +pub devlaunch_core::api::WorkspaceSpec::Url(&'a str) +impl<'a> core::clone::Clone for devlaunch_core::domain::spec::WorkspaceSpec<'a> +pub fn devlaunch_core::domain::spec::WorkspaceSpec<'a>::clone(&self) -> devlaunch_core::domain::spec::WorkspaceSpec<'a> +impl<'a> core::cmp::Eq for devlaunch_core::domain::spec::WorkspaceSpec<'a> +impl<'a> core::cmp::PartialEq for devlaunch_core::domain::spec::WorkspaceSpec<'a> +pub fn devlaunch_core::domain::spec::WorkspaceSpec<'a>::eq(&self, &devlaunch_core::domain::spec::WorkspaceSpec<'a>) -> bool +impl<'a> core::fmt::Debug for devlaunch_core::domain::spec::WorkspaceSpec<'a> +pub fn devlaunch_core::domain::spec::WorkspaceSpec<'a>::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl<'a> core::marker::Copy for devlaunch_core::domain::spec::WorkspaceSpec<'a> +impl<'a> core::marker::StructuralPartialEq for devlaunch_core::domain::spec::WorkspaceSpec<'a> +pub struct devlaunch_core::api::CommandContext<'r> +impl<'r> devlaunch_core::flows::listing::CommandContext<'r> +pub fn devlaunch_core::flows::listing::CommandContext<'r>::git(&self) -> devlaunch_core::clients::git::Git<'r> +pub fn devlaunch_core::flows::listing::CommandContext<'r>::new(&'r dyn devlaunch_runner::Runner) -> Self +pub fn devlaunch_core::flows::listing::CommandContext<'r>::workspaces(&mut self) -> core::result::Result, devlaunch_core::clients::devpod::ListingUnreadable> +pub struct devlaunch_core::api::DevcontainerPath(_) +impl devlaunch_core::domain::spec::DevcontainerPath +pub fn devlaunch_core::domain::spec::DevcontainerPath::as_str(&self) -> &str +impl core::clone::Clone for devlaunch_core::domain::spec::DevcontainerPath +pub fn devlaunch_core::domain::spec::DevcontainerPath::clone(&self) -> devlaunch_core::domain::spec::DevcontainerPath +impl core::cmp::Eq for devlaunch_core::domain::spec::DevcontainerPath +impl core::cmp::PartialEq for devlaunch_core::domain::spec::DevcontainerPath +pub fn devlaunch_core::domain::spec::DevcontainerPath::eq(&self, &devlaunch_core::domain::spec::DevcontainerPath) -> bool +impl core::fmt::Debug for devlaunch_core::domain::spec::DevcontainerPath +pub fn devlaunch_core::domain::spec::DevcontainerPath::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::domain::spec::DevcontainerPath +pub struct devlaunch_core::api::Launch<'a, 'r, 'l> +impl<'a, 'r, 'l> devlaunch_core::flows::launch::Launch<'a, 'r, 'l> +pub fn devlaunch_core::flows::launch::Launch<'a, 'r, 'l>::new(&'a mut devlaunch_core::flows::listing::CommandContext<'r>, &'a mut devlaunch_core::flows::lifecycle::Refresh<'l>, &'a mut dyn devlaunch_core::flows::launch::ColdMachinery<'r>, &'a dyn devlaunch_core::flows::launch::Provision, &'a devlaunch_core::flows::launch::Host, &'a mut dyn core::ops::function::FnMut(&str), &'a mut dyn devlaunch_core::notices::Notices) -> Self +pub fn devlaunch_core::flows::launch::Launch<'a, 'r, 'l>::run(&mut self, &str, &devlaunch_core::flows::launch::LaunchVerb, core::option::Option<&devlaunch_core::domain::spec::DevcontainerPath>) -> core::result::Result +pub const devlaunch_core::api::HANDOFF_VAR: &str +pub const devlaunch_core::api::PREWARM_VAR: &str +pub fn devlaunch_core::api::enriched_listing(&mut devlaunch_core::flows::listing::CommandContext<'_>, &devlaunch_core::flows::listing::DlView<'_>, devlaunch_core::flows::listing::Sizes) -> core::result::Result, devlaunch_core::clients::devpod::ListingUnreadable> +pub fn devlaunch_core::api::identity(&str) -> core::result::Result, devlaunch_core::domain::workspace_id::UnsafeName> +pub fn devlaunch_core::api::json_document(&[devlaunch_core::flows::listing::ListedWorkspace]) -> serde_json::value::Value +pub fn devlaunch_core::api::parse(&str) -> devlaunch_core::domain::spec::WorkspaceSpec<'_> +pub fn devlaunch_core::api::resolve_devcontainer_ref(&str) -> core::result::Result +pub fn devlaunch_core::api::workspace_delete(&mut devlaunch_core::flows::listing::CommandContext<'_>, &mut devlaunch_core::flows::lifecycle::Refresh<'_>, &devlaunch_core::flows::workspace_clone::WorkspaceCloneManager<'_>, &mut devlaunch_core::domain::metadata::MetadataStorage, &str, devlaunch_core::flows::lifecycle::Insistence, &mut dyn devlaunch_core::notices::Notices) -> core::result::Result +pub fn devlaunch_core::api::workspace_stop(&mut devlaunch_core::flows::listing::CommandContext<'_>, &mut devlaunch_core::flows::lifecycle::Refresh<'_>, &str) -> core::result::Result +pub mod devlaunch_core::clients +pub mod devlaunch_core::clients::devpod +pub enum devlaunch_core::clients::devpod::ContainerState +pub devlaunch_core::clients::devpod::ContainerState::Busy +pub devlaunch_core::clients::devpod::ContainerState::NotFound +pub devlaunch_core::clients::devpod::ContainerState::Running +pub devlaunch_core::clients::devpod::ContainerState::Stopped +pub devlaunch_core::clients::devpod::ContainerState::Unknown(alloc::string::String) +impl core::clone::Clone for devlaunch_core::clients::devpod::ContainerState +pub fn devlaunch_core::clients::devpod::ContainerState::clone(&self) -> devlaunch_core::clients::devpod::ContainerState +impl core::cmp::Eq for devlaunch_core::clients::devpod::ContainerState +impl core::cmp::PartialEq for devlaunch_core::clients::devpod::ContainerState +pub fn devlaunch_core::clients::devpod::ContainerState::eq(&self, &devlaunch_core::clients::devpod::ContainerState) -> bool +impl core::fmt::Debug for devlaunch_core::clients::devpod::ContainerState +pub fn devlaunch_core::clients::devpod::ContainerState::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::clients::devpod::ContainerState +pub enum devlaunch_core::clients::devpod::ListingUnreadable +pub devlaunch_core::clients::devpod::ListingUnreadable::Failed +pub devlaunch_core::clients::devpod::ListingUnreadable::Failed::exit: devlaunch_runner::Exit +pub devlaunch_core::clients::devpod::ListingUnreadable::Failed::stderr: alloc::string::String +pub devlaunch_core::clients::devpod::ListingUnreadable::NotRun(devlaunch_core::clients::devpod::NotRun) +pub devlaunch_core::clients::devpod::ListingUnreadable::Unreadable(devlaunch_core::clients::devpod::NotAListing) +impl core::clone::Clone for devlaunch_core::clients::devpod::ListingUnreadable +pub fn devlaunch_core::clients::devpod::ListingUnreadable::clone(&self) -> devlaunch_core::clients::devpod::ListingUnreadable +impl core::cmp::Eq for devlaunch_core::clients::devpod::ListingUnreadable +impl core::cmp::PartialEq for devlaunch_core::clients::devpod::ListingUnreadable +pub fn devlaunch_core::clients::devpod::ListingUnreadable::eq(&self, &devlaunch_core::clients::devpod::ListingUnreadable) -> bool +impl core::fmt::Debug for devlaunch_core::clients::devpod::ListingUnreadable +pub fn devlaunch_core::clients::devpod::ListingUnreadable::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::clients::devpod::ListingUnreadable +pub enum devlaunch_core::clients::devpod::NotAListing +pub devlaunch_core::clients::devpod::NotAListing::EntryNotAnObject +pub devlaunch_core::clients::devpod::NotAListing::EntryNotAnObject::kind: devlaunch_core::json::JsonKind +pub devlaunch_core::clients::devpod::NotAListing::NotAnArray +pub devlaunch_core::clients::devpod::NotAListing::NotAnArray::kind: devlaunch_core::json::JsonKind +pub devlaunch_core::clients::devpod::NotAListing::NotJson +pub devlaunch_core::clients::devpod::NotAListing::NotJson::output: alloc::string::String +pub devlaunch_core::clients::devpod::NotAListing::NotJson::reason: alloc::string::String +pub devlaunch_core::clients::devpod::NotAListing::Silence +pub devlaunch_core::clients::devpod::NotAListing::SourceNotAnObject +pub devlaunch_core::clients::devpod::NotAListing::SourceNotAnObject::kind: devlaunch_core::json::JsonKind +pub devlaunch_core::clients::devpod::NotAListing::SourceNotAnObject::workspace_id: alloc::string::String +impl core::clone::Clone for devlaunch_core::clients::devpod::NotAListing +pub fn devlaunch_core::clients::devpod::NotAListing::clone(&self) -> devlaunch_core::clients::devpod::NotAListing +impl core::cmp::Eq for devlaunch_core::clients::devpod::NotAListing +impl core::cmp::PartialEq for devlaunch_core::clients::devpod::NotAListing +pub fn devlaunch_core::clients::devpod::NotAListing::eq(&self, &devlaunch_core::clients::devpod::NotAListing) -> bool +impl core::fmt::Debug for devlaunch_core::clients::devpod::NotAListing +pub fn devlaunch_core::clients::devpod::NotAListing::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::clients::devpod::NotAListing +pub enum devlaunch_core::clients::devpod::NotRun +pub devlaunch_core::clients::devpod::NotRun::Blocked(devlaunch_runner::OsFailure) +pub devlaunch_core::clients::devpod::NotRun::NotInstalled +pub devlaunch_core::clients::devpod::NotRun::TimedOut +impl core::clone::Clone for devlaunch_core::clients::devpod::NotRun +pub fn devlaunch_core::clients::devpod::NotRun::clone(&self) -> devlaunch_core::clients::devpod::NotRun +impl core::cmp::Eq for devlaunch_core::clients::devpod::NotRun +impl core::cmp::PartialEq for devlaunch_core::clients::devpod::NotRun +pub fn devlaunch_core::clients::devpod::NotRun::eq(&self, &devlaunch_core::clients::devpod::NotRun) -> bool +impl core::fmt::Debug for devlaunch_core::clients::devpod::NotRun +pub fn devlaunch_core::clients::devpod::NotRun::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::Copy for devlaunch_core::clients::devpod::NotRun +impl core::marker::StructuralPartialEq for devlaunch_core::clients::devpod::NotRun +pub enum devlaunch_core::clients::devpod::StatusUnreadable +pub devlaunch_core::clients::devpod::StatusUnreadable::Failed +pub devlaunch_core::clients::devpod::StatusUnreadable::Failed::exit: devlaunch_runner::Exit +pub devlaunch_core::clients::devpod::StatusUnreadable::Failed::stderr: alloc::string::String +pub devlaunch_core::clients::devpod::StatusUnreadable::NoState +pub devlaunch_core::clients::devpod::StatusUnreadable::NoState::output: alloc::string::String +pub devlaunch_core::clients::devpod::StatusUnreadable::NotJson +pub devlaunch_core::clients::devpod::StatusUnreadable::NotJson::output: alloc::string::String +pub devlaunch_core::clients::devpod::StatusUnreadable::NotJson::reason: alloc::string::String +pub devlaunch_core::clients::devpod::StatusUnreadable::NotRun(devlaunch_core::clients::devpod::NotRun) +impl core::clone::Clone for devlaunch_core::clients::devpod::StatusUnreadable +pub fn devlaunch_core::clients::devpod::StatusUnreadable::clone(&self) -> devlaunch_core::clients::devpod::StatusUnreadable +impl core::cmp::Eq for devlaunch_core::clients::devpod::StatusUnreadable +impl core::cmp::PartialEq for devlaunch_core::clients::devpod::StatusUnreadable +pub fn devlaunch_core::clients::devpod::StatusUnreadable::eq(&self, &devlaunch_core::clients::devpod::StatusUnreadable) -> bool +impl core::fmt::Debug for devlaunch_core::clients::devpod::StatusUnreadable +pub fn devlaunch_core::clients::devpod::StatusUnreadable::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::clients::devpod::StatusUnreadable +pub enum devlaunch_core::clients::devpod::WorkspaceSource +pub devlaunch_core::clients::devpod::WorkspaceSource::GitRepository(alloc::string::String) +pub devlaunch_core::clients::devpod::WorkspaceSource::LocalFolder(alloc::string::String) +pub devlaunch_core::clients::devpod::WorkspaceSource::UnreadableLocalFolder(serde_json::map::Map) +pub devlaunch_core::clients::devpod::WorkspaceSource::Unrecognised(serde_json::map::Map) +impl core::clone::Clone for devlaunch_core::clients::devpod::WorkspaceSource +pub fn devlaunch_core::clients::devpod::WorkspaceSource::clone(&self) -> devlaunch_core::clients::devpod::WorkspaceSource +impl core::cmp::Eq for devlaunch_core::clients::devpod::WorkspaceSource +impl core::cmp::PartialEq for devlaunch_core::clients::devpod::WorkspaceSource +pub fn devlaunch_core::clients::devpod::WorkspaceSource::eq(&self, &devlaunch_core::clients::devpod::WorkspaceSource) -> bool +impl core::fmt::Debug for devlaunch_core::clients::devpod::WorkspaceSource +pub fn devlaunch_core::clients::devpod::WorkspaceSource::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::clients::devpod::WorkspaceSource +pub struct devlaunch_core::clients::devpod::Workspace +pub devlaunch_core::clients::devpod::Workspace::id: alloc::string::String +impl devlaunch_core::clients::devpod::Workspace +pub fn devlaunch_core::clients::devpod::Workspace::source(&self) -> &devlaunch_core::clients::devpod::WorkspaceSource +impl core::clone::Clone for devlaunch_core::clients::devpod::Workspace +pub fn devlaunch_core::clients::devpod::Workspace::clone(&self) -> devlaunch_core::clients::devpod::Workspace +impl core::cmp::Eq for devlaunch_core::clients::devpod::Workspace +impl core::cmp::PartialEq for devlaunch_core::clients::devpod::Workspace +pub fn devlaunch_core::clients::devpod::Workspace::eq(&self, &devlaunch_core::clients::devpod::Workspace) -> bool +impl core::fmt::Debug for devlaunch_core::clients::devpod::Workspace +pub fn devlaunch_core::clients::devpod::Workspace::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::clients::devpod::Workspace +pub mod devlaunch_core::clients::gh +pub enum devlaunch_core::clients::gh::GhEvent +pub devlaunch_core::clients::gh::GhEvent::CouldNotRun(devlaunch_core::clients::gh::GhUnavailable) +pub devlaunch_core::clients::gh::GhEvent::NotAToken +pub devlaunch_core::clients::gh::GhEvent::Refused +pub devlaunch_core::clients::gh::GhEvent::Refused::exit: devlaunch_runner::Exit +impl core::clone::Clone for devlaunch_core::clients::gh::GhEvent +pub fn devlaunch_core::clients::gh::GhEvent::clone(&self) -> devlaunch_core::clients::gh::GhEvent +impl core::cmp::Eq for devlaunch_core::clients::gh::GhEvent +impl core::cmp::PartialEq for devlaunch_core::clients::gh::GhEvent +pub fn devlaunch_core::clients::gh::GhEvent::eq(&self, &devlaunch_core::clients::gh::GhEvent) -> bool +impl core::fmt::Debug for devlaunch_core::clients::gh::GhEvent +pub fn devlaunch_core::clients::gh::GhEvent::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::Copy for devlaunch_core::clients::gh::GhEvent +impl core::marker::StructuralPartialEq for devlaunch_core::clients::gh::GhEvent +pub enum devlaunch_core::clients::gh::GhUnavailable +pub devlaunch_core::clients::gh::GhUnavailable::Blocked(devlaunch_runner::OsFailure) +pub devlaunch_core::clients::gh::GhUnavailable::TimedOut +impl core::clone::Clone for devlaunch_core::clients::gh::GhUnavailable +pub fn devlaunch_core::clients::gh::GhUnavailable::clone(&self) -> devlaunch_core::clients::gh::GhUnavailable +impl core::cmp::Eq for devlaunch_core::clients::gh::GhUnavailable +impl core::cmp::PartialEq for devlaunch_core::clients::gh::GhUnavailable +pub fn devlaunch_core::clients::gh::GhUnavailable::eq(&self, &devlaunch_core::clients::gh::GhUnavailable) -> bool +impl core::fmt::Debug for devlaunch_core::clients::gh::GhUnavailable +pub fn devlaunch_core::clients::gh::GhUnavailable::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::Copy for devlaunch_core::clients::gh::GhUnavailable +impl core::marker::StructuralPartialEq for devlaunch_core::clients::gh::GhUnavailable +pub mod devlaunch_core::clients::git +pub struct devlaunch_core::clients::git::Git<'r> +impl<'r> devlaunch_core::clients::git::Git<'r> +pub fn devlaunch_core::clients::git::Git<'r>::new(&'r dyn devlaunch_runner::Runner) -> Self +impl<'r> core::clone::Clone for devlaunch_core::clients::git::Git<'r> +pub fn devlaunch_core::clients::git::Git<'r>::clone(&self) -> devlaunch_core::clients::git::Git<'r> +impl<'r> core::marker::Copy for devlaunch_core::clients::git::Git<'r> +pub struct devlaunch_core::clients::git::GitRefused +impl devlaunch_core::clients::git::GitRefused +pub fn devlaunch_core::clients::git::GitRefused::reason(&self) -> &str +impl core::clone::Clone for devlaunch_core::clients::git::GitRefused +pub fn devlaunch_core::clients::git::GitRefused::clone(&self) -> devlaunch_core::clients::git::GitRefused +impl core::cmp::Eq for devlaunch_core::clients::git::GitRefused +impl core::cmp::PartialEq for devlaunch_core::clients::git::GitRefused +pub fn devlaunch_core::clients::git::GitRefused::eq(&self, &devlaunch_core::clients::git::GitRefused) -> bool +impl core::fmt::Debug for devlaunch_core::clients::git::GitRefused +pub fn devlaunch_core::clients::git::GitRefused::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::clients::git::GitRefused +pub mod devlaunch_core::clients::ssh +pub enum devlaunch_core::clients::ssh::NotRun +pub devlaunch_core::clients::ssh::NotRun::Blocked(devlaunch_runner::OsFailure) +pub devlaunch_core::clients::ssh::NotRun::NotInstalled +pub devlaunch_core::clients::ssh::NotRun::TimedOut +impl core::clone::Clone for devlaunch_core::clients::ssh::NotRun +pub fn devlaunch_core::clients::ssh::NotRun::clone(&self) -> devlaunch_core::clients::ssh::NotRun +impl core::cmp::Eq for devlaunch_core::clients::ssh::NotRun +impl core::cmp::PartialEq for devlaunch_core::clients::ssh::NotRun +pub fn devlaunch_core::clients::ssh::NotRun::eq(&self, &devlaunch_core::clients::ssh::NotRun) -> bool +impl core::fmt::Debug for devlaunch_core::clients::ssh::NotRun +pub fn devlaunch_core::clients::ssh::NotRun::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::Copy for devlaunch_core::clients::ssh::NotRun +impl core::marker::StructuralPartialEq for devlaunch_core::clients::ssh::NotRun +pub enum devlaunch_core::clients::ssh::UnsafeRequest +pub devlaunch_core::clients::ssh::UnsafeRequest::OptionLikeWorkspaceId +pub devlaunch_core::clients::ssh::UnsafeRequest::OptionLikeWorkspaceId::workspace_id: alloc::string::String +pub devlaunch_core::clients::ssh::UnsafeRequest::UnquotableWorkdir +pub devlaunch_core::clients::ssh::UnsafeRequest::UnquotableWorkdir::workdir: alloc::string::String +impl core::clone::Clone for devlaunch_core::clients::ssh::UnsafeRequest +pub fn devlaunch_core::clients::ssh::UnsafeRequest::clone(&self) -> devlaunch_core::clients::ssh::UnsafeRequest +impl core::cmp::Eq for devlaunch_core::clients::ssh::UnsafeRequest +impl core::cmp::PartialEq for devlaunch_core::clients::ssh::UnsafeRequest +pub fn devlaunch_core::clients::ssh::UnsafeRequest::eq(&self, &devlaunch_core::clients::ssh::UnsafeRequest) -> bool +impl core::fmt::Debug for devlaunch_core::clients::ssh::UnsafeRequest +pub fn devlaunch_core::clients::ssh::UnsafeRequest::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::clients::ssh::UnsafeRequest +pub mod devlaunch_core::domain +pub mod devlaunch_core::domain::config +pub enum devlaunch_core::domain::config::ConfigError +pub devlaunch_core::domain::config::ConfigError::NoHomeDirectory +pub devlaunch_core::domain::config::ConfigError::NotToml +pub devlaunch_core::domain::config::ConfigError::NotToml::path: std::path::PathBuf +pub devlaunch_core::domain::config::ConfigError::NotToml::reason: alloc::string::String +pub devlaunch_core::domain::config::ConfigError::Unreadable +pub devlaunch_core::domain::config::ConfigError::Unreadable::path: std::path::PathBuf +pub devlaunch_core::domain::config::ConfigError::Unreadable::source: core::io::error::Error +pub devlaunch_core::domain::config::ConfigError::WrongType +pub devlaunch_core::domain::config::ConfigError::WrongType::path: std::path::PathBuf +pub devlaunch_core::domain::config::ConfigError::WrongType::reason: alloc::string::String +impl core::convert::From for devlaunch_core::domain::config::ConfigError +pub fn devlaunch_core::domain::config::ConfigError::from(devlaunch_core::domain::xdg::NoHomeDirectory) -> Self +impl core::fmt::Debug for devlaunch_core::domain::config::ConfigError +pub fn devlaunch_core::domain::config::ConfigError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +pub struct devlaunch_core::domain::config::WorktreeConfig +pub devlaunch_core::domain::config::WorktreeConfig::auto_prune: bool +pub devlaunch_core::domain::config::WorktreeConfig::enabled: bool +pub devlaunch_core::domain::config::WorktreeConfig::fallback_image: core::option::Option +pub devlaunch_core::domain::config::WorktreeConfig::fetch_interval: u64 +pub devlaunch_core::domain::config::WorktreeConfig::prune_after_days: u64 +pub devlaunch_core::domain::config::WorktreeConfig::repos_dir: std::path::PathBuf +impl core::clone::Clone for devlaunch_core::domain::config::WorktreeConfig +pub fn devlaunch_core::domain::config::WorktreeConfig::clone(&self) -> devlaunch_core::domain::config::WorktreeConfig +impl core::cmp::Eq for devlaunch_core::domain::config::WorktreeConfig +impl core::cmp::PartialEq for devlaunch_core::domain::config::WorktreeConfig +pub fn devlaunch_core::domain::config::WorktreeConfig::eq(&self, &devlaunch_core::domain::config::WorktreeConfig) -> bool +impl core::fmt::Debug for devlaunch_core::domain::config::WorktreeConfig +pub fn devlaunch_core::domain::config::WorktreeConfig::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::domain::config::WorktreeConfig +pub fn devlaunch_core::domain::config::worktree_config() -> core::result::Result +pub mod devlaunch_core::domain::locks +pub enum devlaunch_core::domain::locks::LockError +pub devlaunch_core::domain::locks::LockError::Acquire +pub devlaunch_core::domain::locks::LockError::Acquire::failure: devlaunch_core::domain::metadata::OsFailure +pub devlaunch_core::domain::locks::LockError::Acquire::path: std::path::PathBuf +pub devlaunch_core::domain::locks::LockError::CreateParent +pub devlaunch_core::domain::locks::LockError::CreateParent::failure: devlaunch_core::domain::metadata::OsFailure +pub devlaunch_core::domain::locks::LockError::CreateParent::path: std::path::PathBuf +pub devlaunch_core::domain::locks::LockError::Open +pub devlaunch_core::domain::locks::LockError::Open::failure: devlaunch_core::domain::metadata::OsFailure +pub devlaunch_core::domain::locks::LockError::Open::path: std::path::PathBuf +impl core::clone::Clone for devlaunch_core::domain::locks::LockError +pub fn devlaunch_core::domain::locks::LockError::clone(&self) -> devlaunch_core::domain::locks::LockError +impl core::cmp::Eq for devlaunch_core::domain::locks::LockError +impl core::cmp::PartialEq for devlaunch_core::domain::locks::LockError +pub fn devlaunch_core::domain::locks::LockError::eq(&self, &devlaunch_core::domain::locks::LockError) -> bool +impl core::fmt::Debug for devlaunch_core::domain::locks::LockError +pub fn devlaunch_core::domain::locks::LockError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::domain::locks::LockError +pub mod devlaunch_core::domain::metadata +pub enum devlaunch_core::domain::metadata::Backup +pub devlaunch_core::domain::metadata::Backup::Copied +pub devlaunch_core::domain::metadata::Backup::Copied::path: std::path::PathBuf +pub devlaunch_core::domain::metadata::Backup::CouldNotCopy +pub devlaunch_core::domain::metadata::Backup::CouldNotCopy::failure: devlaunch_core::domain::metadata::OsFailure +pub devlaunch_core::domain::metadata::Backup::CouldNotCopy::path: std::path::PathBuf +impl core::clone::Clone for devlaunch_core::domain::metadata::Backup +pub fn devlaunch_core::domain::metadata::Backup::clone(&self) -> devlaunch_core::domain::metadata::Backup +impl core::cmp::Eq for devlaunch_core::domain::metadata::Backup +impl core::cmp::PartialEq for devlaunch_core::domain::metadata::Backup +pub fn devlaunch_core::domain::metadata::Backup::eq(&self, &devlaunch_core::domain::metadata::Backup) -> bool +impl core::fmt::Debug for devlaunch_core::domain::metadata::Backup +pub fn devlaunch_core::domain::metadata::Backup::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::domain::metadata::Backup +pub enum devlaunch_core::domain::metadata::EntryProblem +pub devlaunch_core::domain::metadata::EntryProblem::NotAnObject +pub devlaunch_core::domain::metadata::EntryProblem::NotAnObject::found: devlaunch_core::json::JsonKind +pub devlaunch_core::domain::metadata::EntryProblem::NotRebuilt +pub devlaunch_core::domain::metadata::EntryProblem::NotRebuilt::reason: alloc::string::String +impl core::clone::Clone for devlaunch_core::domain::metadata::EntryProblem +pub fn devlaunch_core::domain::metadata::EntryProblem::clone(&self) -> devlaunch_core::domain::metadata::EntryProblem +impl core::cmp::Eq for devlaunch_core::domain::metadata::EntryProblem +impl core::cmp::PartialEq for devlaunch_core::domain::metadata::EntryProblem +pub fn devlaunch_core::domain::metadata::EntryProblem::eq(&self, &devlaunch_core::domain::metadata::EntryProblem) -> bool +impl core::fmt::Debug for devlaunch_core::domain::metadata::EntryProblem +pub fn devlaunch_core::domain::metadata::EntryProblem::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::domain::metadata::EntryProblem +pub enum devlaunch_core::domain::metadata::FileProblem +pub devlaunch_core::domain::metadata::FileProblem::NotAnObject +pub devlaunch_core::domain::metadata::FileProblem::NotAnObject::found: devlaunch_core::json::JsonKind +pub devlaunch_core::domain::metadata::FileProblem::NotJson +pub devlaunch_core::domain::metadata::FileProblem::NotJson::reason: alloc::string::String +pub devlaunch_core::domain::metadata::FileProblem::Unreadable(devlaunch_core::domain::metadata::OsFailure) +impl core::clone::Clone for devlaunch_core::domain::metadata::FileProblem +pub fn devlaunch_core::domain::metadata::FileProblem::clone(&self) -> devlaunch_core::domain::metadata::FileProblem +impl core::cmp::Eq for devlaunch_core::domain::metadata::FileProblem +impl core::cmp::PartialEq for devlaunch_core::domain::metadata::FileProblem +pub fn devlaunch_core::domain::metadata::FileProblem::eq(&self, &devlaunch_core::domain::metadata::FileProblem) -> bool +impl core::fmt::Debug for devlaunch_core::domain::metadata::FileProblem +pub fn devlaunch_core::domain::metadata::FileProblem::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::domain::metadata::FileProblem +pub enum devlaunch_core::domain::metadata::MetadataError +pub devlaunch_core::domain::metadata::MetadataError::CreateDir +pub devlaunch_core::domain::metadata::MetadataError::CreateDir::failure: devlaunch_core::domain::metadata::OsFailure +pub devlaunch_core::domain::metadata::MetadataError::CreateDir::path: std::path::PathBuf +pub devlaunch_core::domain::metadata::MetadataError::CreateTemp +pub devlaunch_core::domain::metadata::MetadataError::CreateTemp::directory: std::path::PathBuf +pub devlaunch_core::domain::metadata::MetadataError::CreateTemp::failure: devlaunch_core::domain::metadata::OsFailure +pub devlaunch_core::domain::metadata::MetadataError::Encode +pub devlaunch_core::domain::metadata::MetadataError::Encode::reason: alloc::string::String +pub devlaunch_core::domain::metadata::MetadataError::Lock(devlaunch_core::domain::locks::LockError) +pub devlaunch_core::domain::metadata::MetadataError::Replace +pub devlaunch_core::domain::metadata::MetadataError::Replace::failure: devlaunch_core::domain::metadata::OsFailure +pub devlaunch_core::domain::metadata::MetadataError::Replace::from: std::path::PathBuf +pub devlaunch_core::domain::metadata::MetadataError::Replace::to: std::path::PathBuf +pub devlaunch_core::domain::metadata::MetadataError::SetMode +pub devlaunch_core::domain::metadata::MetadataError::SetMode::failure: devlaunch_core::domain::metadata::OsFailure +pub devlaunch_core::domain::metadata::MetadataError::SetMode::mode: u32 +pub devlaunch_core::domain::metadata::MetadataError::SetMode::path: std::path::PathBuf +pub devlaunch_core::domain::metadata::MetadataError::Write +pub devlaunch_core::domain::metadata::MetadataError::Write::failure: devlaunch_core::domain::metadata::OsFailure +pub devlaunch_core::domain::metadata::MetadataError::Write::path: std::path::PathBuf +impl core::clone::Clone for devlaunch_core::domain::metadata::MetadataError +pub fn devlaunch_core::domain::metadata::MetadataError::clone(&self) -> devlaunch_core::domain::metadata::MetadataError +impl core::cmp::Eq for devlaunch_core::domain::metadata::MetadataError +impl core::cmp::PartialEq for devlaunch_core::domain::metadata::MetadataError +pub fn devlaunch_core::domain::metadata::MetadataError::eq(&self, &devlaunch_core::domain::metadata::MetadataError) -> bool +impl core::fmt::Debug for devlaunch_core::domain::metadata::MetadataError +pub fn devlaunch_core::domain::metadata::MetadataError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::domain::metadata::MetadataError +pub enum devlaunch_core::domain::metadata::Notice +pub devlaunch_core::domain::metadata::Notice::EntryHasUnknownFields +pub devlaunch_core::domain::metadata::Notice::EntryHasUnknownFields::fields: alloc::vec::Vec +pub devlaunch_core::domain::metadata::Notice::EntryHasUnknownFields::key: alloc::string::String +pub devlaunch_core::domain::metadata::Notice::EntryHasUnknownFields::path: std::path::PathBuf +pub devlaunch_core::domain::metadata::Notice::EntryHasUnknownFields::section: devlaunch_core::domain::metadata::Section +pub devlaunch_core::domain::metadata::Notice::EntryUnusable +pub devlaunch_core::domain::metadata::Notice::EntryUnusable::key: alloc::string::String +pub devlaunch_core::domain::metadata::Notice::EntryUnusable::path: std::path::PathBuf +pub devlaunch_core::domain::metadata::Notice::EntryUnusable::problem: devlaunch_core::domain::metadata::EntryProblem +pub devlaunch_core::domain::metadata::Notice::EntryUnusable::section: devlaunch_core::domain::metadata::Section +pub devlaunch_core::domain::metadata::Notice::FileUnusable +pub devlaunch_core::domain::metadata::Notice::FileUnusable::path: std::path::PathBuf +pub devlaunch_core::domain::metadata::Notice::FileUnusable::problem: devlaunch_core::domain::metadata::FileProblem +pub devlaunch_core::domain::metadata::Notice::FileUnusable::quarantine: devlaunch_core::domain::metadata::Quarantine +pub devlaunch_core::domain::metadata::Notice::OriginalPreserved +pub devlaunch_core::domain::metadata::Notice::OriginalPreserved::backup: devlaunch_core::domain::metadata::Backup +pub devlaunch_core::domain::metadata::Notice::OriginalPreserved::path: std::path::PathBuf +pub devlaunch_core::domain::metadata::Notice::SectionUnusable +pub devlaunch_core::domain::metadata::Notice::SectionUnusable::found: devlaunch_core::json::JsonKind +pub devlaunch_core::domain::metadata::Notice::SectionUnusable::path: std::path::PathBuf +pub devlaunch_core::domain::metadata::Notice::SectionUnusable::section: devlaunch_core::domain::metadata::Section +pub devlaunch_core::domain::metadata::Notice::UnknownTopLevelKeys +pub devlaunch_core::domain::metadata::Notice::UnknownTopLevelKeys::keys: alloc::vec::Vec +pub devlaunch_core::domain::metadata::Notice::UnknownTopLevelKeys::path: std::path::PathBuf +pub devlaunch_core::domain::metadata::Notice::VersionFromNewerBuild +pub devlaunch_core::domain::metadata::Notice::VersionFromNewerBuild::found: i64 +pub devlaunch_core::domain::metadata::Notice::VersionFromNewerBuild::path: std::path::PathBuf +pub devlaunch_core::domain::metadata::Notice::VersionFromNewerBuild::understood: i64 +pub devlaunch_core::domain::metadata::Notice::VersionHeaderUnusable +pub devlaunch_core::domain::metadata::Notice::VersionHeaderUnusable::found: serde_json::value::Value +pub devlaunch_core::domain::metadata::Notice::VersionHeaderUnusable::path: std::path::PathBuf +impl core::clone::Clone for devlaunch_core::domain::metadata::Notice +pub fn devlaunch_core::domain::metadata::Notice::clone(&self) -> devlaunch_core::domain::metadata::Notice +impl core::cmp::Eq for devlaunch_core::domain::metadata::Notice +impl core::cmp::PartialEq for devlaunch_core::domain::metadata::Notice +pub fn devlaunch_core::domain::metadata::Notice::eq(&self, &devlaunch_core::domain::metadata::Notice) -> bool +impl core::fmt::Debug for devlaunch_core::domain::metadata::Notice +pub fn devlaunch_core::domain::metadata::Notice::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::domain::metadata::Notice +pub enum devlaunch_core::domain::metadata::Quarantine +pub devlaunch_core::domain::metadata::Quarantine::CouldNotMove +pub devlaunch_core::domain::metadata::Quarantine::CouldNotMove::failure: devlaunch_core::domain::metadata::OsFailure +pub devlaunch_core::domain::metadata::Quarantine::CouldNotMove::path: std::path::PathBuf +pub devlaunch_core::domain::metadata::Quarantine::MovedAside +pub devlaunch_core::domain::metadata::Quarantine::MovedAside::path: std::path::PathBuf +impl core::clone::Clone for devlaunch_core::domain::metadata::Quarantine +pub fn devlaunch_core::domain::metadata::Quarantine::clone(&self) -> devlaunch_core::domain::metadata::Quarantine +impl core::cmp::Eq for devlaunch_core::domain::metadata::Quarantine +impl core::cmp::PartialEq for devlaunch_core::domain::metadata::Quarantine +pub fn devlaunch_core::domain::metadata::Quarantine::eq(&self, &devlaunch_core::domain::metadata::Quarantine) -> bool +impl core::fmt::Debug for devlaunch_core::domain::metadata::Quarantine +pub fn devlaunch_core::domain::metadata::Quarantine::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::domain::metadata::Quarantine +pub enum devlaunch_core::domain::metadata::Section +pub devlaunch_core::domain::metadata::Section::Repositories +pub devlaunch_core::domain::metadata::Section::Worktrees +impl devlaunch_core::domain::metadata::Section +pub fn devlaunch_core::domain::metadata::Section::key(self) -> &'static str +impl core::clone::Clone for devlaunch_core::domain::metadata::Section +pub fn devlaunch_core::domain::metadata::Section::clone(&self) -> devlaunch_core::domain::metadata::Section +impl core::cmp::Eq for devlaunch_core::domain::metadata::Section +impl core::cmp::PartialEq for devlaunch_core::domain::metadata::Section +pub fn devlaunch_core::domain::metadata::Section::eq(&self, &devlaunch_core::domain::metadata::Section) -> bool +impl core::fmt::Debug for devlaunch_core::domain::metadata::Section +pub fn devlaunch_core::domain::metadata::Section::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::Copy for devlaunch_core::domain::metadata::Section +impl core::marker::StructuralPartialEq for devlaunch_core::domain::metadata::Section +pub struct devlaunch_core::domain::metadata::MetadataStorage +impl devlaunch_core::domain::metadata::MetadataStorage +pub fn devlaunch_core::domain::metadata::MetadataStorage::default_path() -> core::result::Result +pub fn devlaunch_core::domain::metadata::MetadataStorage::on_metadata_lock_wait(&mut self, impl core::ops::function::Fn() + 'static) +pub fn devlaunch_core::domain::metadata::MetadataStorage::open(impl core::convert::Into) -> core::result::Result<(Self, alloc::vec::Vec), devlaunch_core::domain::metadata::MetadataError> +impl core::fmt::Debug for devlaunch_core::domain::metadata::MetadataStorage +pub fn devlaunch_core::domain::metadata::MetadataStorage::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +pub struct devlaunch_core::domain::metadata::OsFailure +pub devlaunch_core::domain::metadata::OsFailure::kind: core::io::error::ErrorKind +pub devlaunch_core::domain::metadata::OsFailure::message: alloc::string::String +impl core::clone::Clone for devlaunch_core::domain::metadata::OsFailure +pub fn devlaunch_core::domain::metadata::OsFailure::clone(&self) -> devlaunch_core::domain::metadata::OsFailure +impl core::cmp::Eq for devlaunch_core::domain::metadata::OsFailure +impl core::cmp::PartialEq for devlaunch_core::domain::metadata::OsFailure +pub fn devlaunch_core::domain::metadata::OsFailure::eq(&self, &devlaunch_core::domain::metadata::OsFailure) -> bool +impl core::convert::From for devlaunch_core::domain::metadata::OsFailure +pub fn devlaunch_core::domain::metadata::OsFailure::from(core::io::error::Error) -> Self +impl core::fmt::Debug for devlaunch_core::domain::metadata::OsFailure +pub fn devlaunch_core::domain::metadata::OsFailure::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::domain::metadata::OsFailure +pub mod devlaunch_core::domain::model +pub struct devlaunch_core::domain::model::WorktreeInfo +impl core::clone::Clone for devlaunch_core::domain::model::WorktreeInfo +pub fn devlaunch_core::domain::model::WorktreeInfo::clone(&self) -> devlaunch_core::domain::model::WorktreeInfo +impl core::cmp::Eq for devlaunch_core::domain::model::WorktreeInfo +impl core::cmp::PartialEq for devlaunch_core::domain::model::WorktreeInfo +pub fn devlaunch_core::domain::model::WorktreeInfo::eq(&self, &devlaunch_core::domain::model::WorktreeInfo) -> bool +impl core::fmt::Debug for devlaunch_core::domain::model::WorktreeInfo +pub fn devlaunch_core::domain::model::WorktreeInfo::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::domain::model::WorktreeInfo +impl serde_core::ser::Serialize for devlaunch_core::domain::model::WorktreeInfo +pub fn devlaunch_core::domain::model::WorktreeInfo::serialize<__S>(&self, __S) -> core::result::Result<<__S as serde_core::ser::Serializer>::Ok, <__S as serde_core::ser::Serializer>::Error> where __S: serde_core::ser::Serializer +pub mod devlaunch_core::domain::spec +pub enum devlaunch_core::domain::spec::DevcontainerRefError +pub devlaunch_core::domain::spec::DevcontainerRefError::FlagLike +pub devlaunch_core::domain::spec::DevcontainerRefError::Missing +impl core::clone::Clone for devlaunch_core::domain::spec::DevcontainerRefError +pub fn devlaunch_core::domain::spec::DevcontainerRefError::clone(&self) -> devlaunch_core::domain::spec::DevcontainerRefError +impl core::cmp::Eq for devlaunch_core::domain::spec::DevcontainerRefError +impl core::cmp::PartialEq for devlaunch_core::domain::spec::DevcontainerRefError +pub fn devlaunch_core::domain::spec::DevcontainerRefError::eq(&self, &devlaunch_core::domain::spec::DevcontainerRefError) -> bool +impl core::fmt::Debug for devlaunch_core::domain::spec::DevcontainerRefError +pub fn devlaunch_core::domain::spec::DevcontainerRefError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::Copy for devlaunch_core::domain::spec::DevcontainerRefError +impl core::marker::StructuralPartialEq for devlaunch_core::domain::spec::DevcontainerRefError +pub enum devlaunch_core::domain::spec::SpecIdentity<'a> +pub devlaunch_core::domain::spec::SpecIdentity::ExistingName(&'a str) +pub devlaunch_core::domain::spec::SpecIdentity::PathLeaf(&'a str) +pub devlaunch_core::domain::spec::SpecIdentity::RepoLabel(alloc::string::String) +pub devlaunch_core::domain::spec::SpecIdentity::Workspace(alloc::string::String) +impl<'a> core::clone::Clone for devlaunch_core::domain::spec::SpecIdentity<'a> +pub fn devlaunch_core::domain::spec::SpecIdentity<'a>::clone(&self) -> devlaunch_core::domain::spec::SpecIdentity<'a> +impl<'a> core::cmp::Eq for devlaunch_core::domain::spec::SpecIdentity<'a> +impl<'a> core::cmp::PartialEq for devlaunch_core::domain::spec::SpecIdentity<'a> +pub fn devlaunch_core::domain::spec::SpecIdentity<'a>::eq(&self, &devlaunch_core::domain::spec::SpecIdentity<'a>) -> bool +impl<'a> core::fmt::Debug for devlaunch_core::domain::spec::SpecIdentity<'a> +pub fn devlaunch_core::domain::spec::SpecIdentity<'a>::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl<'a> core::marker::StructuralPartialEq for devlaunch_core::domain::spec::SpecIdentity<'a> +pub enum devlaunch_core::domain::spec::WorkspaceSpec<'a> +pub devlaunch_core::domain::spec::WorkspaceSpec::ExistingIdOrName(&'a str) +pub devlaunch_core::domain::spec::WorkspaceSpec::HostPath(&'a str) +pub devlaunch_core::domain::spec::WorkspaceSpec::OwnerRepo +pub devlaunch_core::domain::spec::WorkspaceSpec::OwnerRepo::branch: core::option::Option<&'a str> +pub devlaunch_core::domain::spec::WorkspaceSpec::OwnerRepo::owner: &'a str +pub devlaunch_core::domain::spec::WorkspaceSpec::OwnerRepo::repo: &'a str +pub devlaunch_core::domain::spec::WorkspaceSpec::Path(&'a str) +pub devlaunch_core::domain::spec::WorkspaceSpec::SshUrl(&'a str) +pub devlaunch_core::domain::spec::WorkspaceSpec::Url(&'a str) +impl<'a> core::clone::Clone for devlaunch_core::domain::spec::WorkspaceSpec<'a> +pub fn devlaunch_core::domain::spec::WorkspaceSpec<'a>::clone(&self) -> devlaunch_core::domain::spec::WorkspaceSpec<'a> +impl<'a> core::cmp::Eq for devlaunch_core::domain::spec::WorkspaceSpec<'a> +impl<'a> core::cmp::PartialEq for devlaunch_core::domain::spec::WorkspaceSpec<'a> +pub fn devlaunch_core::domain::spec::WorkspaceSpec<'a>::eq(&self, &devlaunch_core::domain::spec::WorkspaceSpec<'a>) -> bool +impl<'a> core::fmt::Debug for devlaunch_core::domain::spec::WorkspaceSpec<'a> +pub fn devlaunch_core::domain::spec::WorkspaceSpec<'a>::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl<'a> core::marker::Copy for devlaunch_core::domain::spec::WorkspaceSpec<'a> +impl<'a> core::marker::StructuralPartialEq for devlaunch_core::domain::spec::WorkspaceSpec<'a> +pub struct devlaunch_core::domain::spec::DevcontainerPath(_) +impl devlaunch_core::domain::spec::DevcontainerPath +pub fn devlaunch_core::domain::spec::DevcontainerPath::as_str(&self) -> &str +impl core::clone::Clone for devlaunch_core::domain::spec::DevcontainerPath +pub fn devlaunch_core::domain::spec::DevcontainerPath::clone(&self) -> devlaunch_core::domain::spec::DevcontainerPath +impl core::cmp::Eq for devlaunch_core::domain::spec::DevcontainerPath +impl core::cmp::PartialEq for devlaunch_core::domain::spec::DevcontainerPath +pub fn devlaunch_core::domain::spec::DevcontainerPath::eq(&self, &devlaunch_core::domain::spec::DevcontainerPath) -> bool +impl core::fmt::Debug for devlaunch_core::domain::spec::DevcontainerPath +pub fn devlaunch_core::domain::spec::DevcontainerPath::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::domain::spec::DevcontainerPath +pub fn devlaunch_core::domain::spec::identity(&str) -> core::result::Result, devlaunch_core::domain::workspace_id::UnsafeName> +pub fn devlaunch_core::domain::spec::parse(&str) -> devlaunch_core::domain::spec::WorkspaceSpec<'_> +pub fn devlaunch_core::domain::spec::resolve_devcontainer_ref(&str) -> core::result::Result +pub mod devlaunch_core::domain::workspace_id +pub enum devlaunch_core::domain::workspace_id::NamePart +pub devlaunch_core::domain::workspace_id::NamePart::Owner +pub devlaunch_core::domain::workspace_id::NamePart::Ref +pub devlaunch_core::domain::workspace_id::NamePart::Repo +impl core::clone::Clone for devlaunch_core::domain::workspace_id::NamePart +pub fn devlaunch_core::domain::workspace_id::NamePart::clone(&self) -> devlaunch_core::domain::workspace_id::NamePart +impl core::cmp::Eq for devlaunch_core::domain::workspace_id::NamePart +impl core::cmp::PartialEq for devlaunch_core::domain::workspace_id::NamePart +pub fn devlaunch_core::domain::workspace_id::NamePart::eq(&self, &devlaunch_core::domain::workspace_id::NamePart) -> bool +impl core::fmt::Debug for devlaunch_core::domain::workspace_id::NamePart +pub fn devlaunch_core::domain::workspace_id::NamePart::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::hash::Hash for devlaunch_core::domain::workspace_id::NamePart +pub fn devlaunch_core::domain::workspace_id::NamePart::hash<__H: core::hash::Hasher>(&self, &mut __H) +impl core::marker::Copy for devlaunch_core::domain::workspace_id::NamePart +impl core::marker::StructuralPartialEq for devlaunch_core::domain::workspace_id::NamePart +pub struct devlaunch_core::domain::workspace_id::UnsafeName +pub devlaunch_core::domain::workspace_id::UnsafeName::name: alloc::string::String +pub devlaunch_core::domain::workspace_id::UnsafeName::part: devlaunch_core::domain::workspace_id::NamePart +impl core::clone::Clone for devlaunch_core::domain::workspace_id::UnsafeName +pub fn devlaunch_core::domain::workspace_id::UnsafeName::clone(&self) -> devlaunch_core::domain::workspace_id::UnsafeName +impl core::cmp::Eq for devlaunch_core::domain::workspace_id::UnsafeName +impl core::cmp::PartialEq for devlaunch_core::domain::workspace_id::UnsafeName +pub fn devlaunch_core::domain::workspace_id::UnsafeName::eq(&self, &devlaunch_core::domain::workspace_id::UnsafeName) -> bool +impl core::fmt::Debug for devlaunch_core::domain::workspace_id::UnsafeName +pub fn devlaunch_core::domain::workspace_id::UnsafeName::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::hash::Hash for devlaunch_core::domain::workspace_id::UnsafeName +pub fn devlaunch_core::domain::workspace_id::UnsafeName::hash<__H: core::hash::Hasher>(&self, &mut __H) +impl core::marker::StructuralPartialEq for devlaunch_core::domain::workspace_id::UnsafeName +pub struct devlaunch_core::domain::workspace_id::WorkspaceId +impl devlaunch_core::domain::workspace_id::WorkspaceId +pub fn devlaunch_core::domain::workspace_id::WorkspaceId::new(&str, &str, &str) -> core::result::Result +pub fn devlaunch_core::domain::workspace_id::WorkspaceId::value(&self) -> alloc::string::String +impl core::clone::Clone for devlaunch_core::domain::workspace_id::WorkspaceId +pub fn devlaunch_core::domain::workspace_id::WorkspaceId::clone(&self) -> devlaunch_core::domain::workspace_id::WorkspaceId +impl core::cmp::Eq for devlaunch_core::domain::workspace_id::WorkspaceId +impl core::cmp::PartialEq for devlaunch_core::domain::workspace_id::WorkspaceId +pub fn devlaunch_core::domain::workspace_id::WorkspaceId::eq(&self, &devlaunch_core::domain::workspace_id::WorkspaceId) -> bool +impl core::fmt::Debug for devlaunch_core::domain::workspace_id::WorkspaceId +pub fn devlaunch_core::domain::workspace_id::WorkspaceId::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::fmt::Display for devlaunch_core::domain::workspace_id::WorkspaceId +pub fn devlaunch_core::domain::workspace_id::WorkspaceId::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::hash::Hash for devlaunch_core::domain::workspace_id::WorkspaceId +pub fn devlaunch_core::domain::workspace_id::WorkspaceId::hash<__H: core::hash::Hasher>(&self, &mut __H) +impl core::marker::StructuralPartialEq for devlaunch_core::domain::workspace_id::WorkspaceId +pub mod devlaunch_core::domain::workspace_state +pub enum devlaunch_core::domain::workspace_state::CouldNotTell +pub devlaunch_core::domain::workspace_state::CouldNotTell::CouldNotLook +pub devlaunch_core::domain::workspace_state::CouldNotTell::CouldNotLook::clone: std::path::PathBuf +pub devlaunch_core::domain::workspace_state::CouldNotTell::CouldNotLook::error: alloc::string::String +pub devlaunch_core::domain::workspace_state::CouldNotTell::DirectoryUnknown +pub devlaunch_core::domain::workspace_state::CouldNotTell::DirectoryUnknown::workspace_id: alloc::string::String +pub devlaunch_core::domain::workspace_state::CouldNotTell::GitCouldNotRead +pub devlaunch_core::domain::workspace_state::CouldNotTell::GitCouldNotRead::clone: std::path::PathBuf +pub devlaunch_core::domain::workspace_state::CouldNotTell::GitCouldNotRead::reason: alloc::string::String +pub devlaunch_core::domain::workspace_state::CouldNotTell::UnpushedNotListed +pub devlaunch_core::domain::workspace_state::CouldNotTell::UnpushedNotListed::branch: alloc::string::String +pub devlaunch_core::domain::workspace_state::CouldNotTell::UnpushedNotListed::clone: std::path::PathBuf +pub devlaunch_core::domain::workspace_state::CouldNotTell::UnpushedNotListed::reason: alloc::string::String +impl devlaunch_core::domain::workspace_state::CouldNotTell +pub fn devlaunch_core::domain::workspace_state::CouldNotTell::describe(&self) -> alloc::string::String +impl core::clone::Clone for devlaunch_core::domain::workspace_state::CouldNotTell +pub fn devlaunch_core::domain::workspace_state::CouldNotTell::clone(&self) -> devlaunch_core::domain::workspace_state::CouldNotTell +impl core::cmp::Eq for devlaunch_core::domain::workspace_state::CouldNotTell +impl core::cmp::PartialEq for devlaunch_core::domain::workspace_state::CouldNotTell +pub fn devlaunch_core::domain::workspace_state::CouldNotTell::eq(&self, &devlaunch_core::domain::workspace_state::CouldNotTell) -> bool +impl core::fmt::Debug for devlaunch_core::domain::workspace_state::CouldNotTell +pub fn devlaunch_core::domain::workspace_state::CouldNotTell::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::domain::workspace_state::CouldNotTell +pub enum devlaunch_core::domain::workspace_state::Loss +pub devlaunch_core::domain::workspace_state::Loss::Uncommitted(devlaunch_core::domain::workspace_state::NonEmpty) +pub devlaunch_core::domain::workspace_state::Loss::Unpushed(devlaunch_core::domain::workspace_state::NonEmpty) +impl core::clone::Clone for devlaunch_core::domain::workspace_state::Loss +pub fn devlaunch_core::domain::workspace_state::Loss::clone(&self) -> devlaunch_core::domain::workspace_state::Loss +impl core::cmp::Eq for devlaunch_core::domain::workspace_state::Loss +impl core::cmp::PartialEq for devlaunch_core::domain::workspace_state::Loss +pub fn devlaunch_core::domain::workspace_state::Loss::eq(&self, &devlaunch_core::domain::workspace_state::Loss) -> bool +impl core::fmt::Debug for devlaunch_core::domain::workspace_state::Loss +pub fn devlaunch_core::domain::workspace_state::Loss::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::domain::workspace_state::Loss +pub enum devlaunch_core::domain::workspace_state::Unsaved +pub devlaunch_core::domain::workspace_state::Unsaved::CouldNotTell(devlaunch_core::domain::workspace_state::CouldNotTell) +pub devlaunch_core::domain::workspace_state::Unsaved::NothingToLose +pub devlaunch_core::domain::workspace_state::Unsaved::WouldLose(devlaunch_core::domain::workspace_state::NonEmpty) +impl core::clone::Clone for devlaunch_core::domain::workspace_state::Unsaved +pub fn devlaunch_core::domain::workspace_state::Unsaved::clone(&self) -> devlaunch_core::domain::workspace_state::Unsaved +impl core::cmp::Eq for devlaunch_core::domain::workspace_state::Unsaved +impl core::cmp::PartialEq for devlaunch_core::domain::workspace_state::Unsaved +pub fn devlaunch_core::domain::workspace_state::Unsaved::eq(&self, &devlaunch_core::domain::workspace_state::Unsaved) -> bool +impl core::fmt::Debug for devlaunch_core::domain::workspace_state::Unsaved +pub fn devlaunch_core::domain::workspace_state::Unsaved::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::domain::workspace_state::Unsaved +pub struct devlaunch_core::domain::workspace_state::NonEmpty +impl devlaunch_core::domain::workspace_state::NonEmpty +pub fn devlaunch_core::domain::workspace_state::NonEmpty::describe(&self) -> alloc::string::String +impl devlaunch_core::domain::workspace_state::NonEmpty +pub fn devlaunch_core::domain::workspace_state::NonEmpty::iter(&self) -> impl core::iter::traits::iterator::Iterator +pub fn devlaunch_core::domain::workspace_state::NonEmpty::of(impl core::iter::traits::collect::IntoIterator) -> core::option::Option +impl core::clone::Clone for devlaunch_core::domain::workspace_state::NonEmpty +pub fn devlaunch_core::domain::workspace_state::NonEmpty::clone(&self) -> devlaunch_core::domain::workspace_state::NonEmpty +impl core::cmp::Eq for devlaunch_core::domain::workspace_state::NonEmpty +impl core::cmp::PartialEq for devlaunch_core::domain::workspace_state::NonEmpty +pub fn devlaunch_core::domain::workspace_state::NonEmpty::eq(&self, &devlaunch_core::domain::workspace_state::NonEmpty) -> bool +impl core::marker::StructuralPartialEq for devlaunch_core::domain::workspace_state::NonEmpty +impl core::fmt::Debug for devlaunch_core::domain::workspace_state::NonEmpty +pub fn devlaunch_core::domain::workspace_state::NonEmpty::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +pub mod devlaunch_core::domain::xdg +pub struct devlaunch_core::domain::xdg::NoHomeDirectory +impl core::clone::Clone for devlaunch_core::domain::xdg::NoHomeDirectory +pub fn devlaunch_core::domain::xdg::NoHomeDirectory::clone(&self) -> devlaunch_core::domain::xdg::NoHomeDirectory +impl core::cmp::Eq for devlaunch_core::domain::xdg::NoHomeDirectory +impl core::cmp::PartialEq for devlaunch_core::domain::xdg::NoHomeDirectory +pub fn devlaunch_core::domain::xdg::NoHomeDirectory::eq(&self, &devlaunch_core::domain::xdg::NoHomeDirectory) -> bool +impl core::convert::From for devlaunch_core::domain::config::ConfigError +pub fn devlaunch_core::domain::config::ConfigError::from(devlaunch_core::domain::xdg::NoHomeDirectory) -> Self +impl core::convert::From for devlaunch_core::flows::completion::InstallError +pub fn devlaunch_core::flows::completion::InstallError::from(devlaunch_core::domain::xdg::NoHomeDirectory) -> Self +impl core::fmt::Debug for devlaunch_core::domain::xdg::NoHomeDirectory +pub fn devlaunch_core::domain::xdg::NoHomeDirectory::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::Copy for devlaunch_core::domain::xdg::NoHomeDirectory +impl core::marker::StructuralPartialEq for devlaunch_core::domain::xdg::NoHomeDirectory +pub fn devlaunch_core::domain::xdg::config_home() -> core::result::Result +pub fn devlaunch_core::domain::xdg::devlaunch_cache() -> core::result::Result +pub mod devlaunch_core::flows +pub mod devlaunch_core::flows::branch_manager +pub enum devlaunch_core::flows::branch_manager::BranchError +pub devlaunch_core::flows::branch_manager::BranchError::NotCreated +pub devlaunch_core::flows::branch_manager::BranchError::NotCreated::branch: alloc::string::String +pub devlaunch_core::flows::branch_manager::BranchError::NotCreated::reason: alloc::string::String +pub devlaunch_core::flows::branch_manager::BranchError::NotPushed +pub devlaunch_core::flows::branch_manager::BranchError::NotPushed::branch: alloc::string::String +pub devlaunch_core::flows::branch_manager::BranchError::NotPushed::reason: alloc::string::String +impl core::clone::Clone for devlaunch_core::flows::branch_manager::BranchError +pub fn devlaunch_core::flows::branch_manager::BranchError::clone(&self) -> devlaunch_core::flows::branch_manager::BranchError +impl core::cmp::Eq for devlaunch_core::flows::branch_manager::BranchError +impl core::cmp::PartialEq for devlaunch_core::flows::branch_manager::BranchError +pub fn devlaunch_core::flows::branch_manager::BranchError::eq(&self, &devlaunch_core::flows::branch_manager::BranchError) -> bool +impl core::fmt::Debug for devlaunch_core::flows::branch_manager::BranchError +pub fn devlaunch_core::flows::branch_manager::BranchError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::branch_manager::BranchError +pub mod devlaunch_core::flows::completion +pub enum devlaunch_core::flows::completion::FileState +pub devlaunch_core::flows::completion::FileState::AlreadyCurrent +pub devlaunch_core::flows::completion::FileState::Written +impl core::clone::Clone for devlaunch_core::flows::completion::FileState +pub fn devlaunch_core::flows::completion::FileState::clone(&self) -> devlaunch_core::flows::completion::FileState +impl core::cmp::Eq for devlaunch_core::flows::completion::FileState +impl core::cmp::PartialEq for devlaunch_core::flows::completion::FileState +pub fn devlaunch_core::flows::completion::FileState::eq(&self, &devlaunch_core::flows::completion::FileState) -> bool +impl core::fmt::Debug for devlaunch_core::flows::completion::FileState +pub fn devlaunch_core::flows::completion::FileState::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::Copy for devlaunch_core::flows::completion::FileState +impl core::marker::StructuralPartialEq for devlaunch_core::flows::completion::FileState +pub enum devlaunch_core::flows::completion::InstallError +pub devlaunch_core::flows::completion::InstallError::CreateRcDirectory +pub devlaunch_core::flows::completion::InstallError::CreateRcDirectory::path: std::path::PathBuf +pub devlaunch_core::flows::completion::InstallError::CreateRcDirectory::source: core::io::error::Error +pub devlaunch_core::flows::completion::InstallError::CreateRcDirectory::written: devlaunch_core::flows::completion::WrittenScript +pub devlaunch_core::flows::completion::InstallError::CreateScriptDirectory +pub devlaunch_core::flows::completion::InstallError::CreateScriptDirectory::path: std::path::PathBuf +pub devlaunch_core::flows::completion::InstallError::CreateScriptDirectory::source: core::io::error::Error +pub devlaunch_core::flows::completion::InstallError::NoHomeDirectory +pub devlaunch_core::flows::completion::InstallError::ReadRc +pub devlaunch_core::flows::completion::InstallError::ReadRc::path: std::path::PathBuf +pub devlaunch_core::flows::completion::InstallError::ReadRc::source: core::io::error::Error +pub devlaunch_core::flows::completion::InstallError::ReadRc::written: devlaunch_core::flows::completion::WrittenScript +pub devlaunch_core::flows::completion::InstallError::WriteRc +pub devlaunch_core::flows::completion::InstallError::WriteRc::path: std::path::PathBuf +pub devlaunch_core::flows::completion::InstallError::WriteRc::source: core::io::error::Error +pub devlaunch_core::flows::completion::InstallError::WriteRc::written: devlaunch_core::flows::completion::WrittenScript +pub devlaunch_core::flows::completion::InstallError::WriteScript +pub devlaunch_core::flows::completion::InstallError::WriteScript::path: std::path::PathBuf +pub devlaunch_core::flows::completion::InstallError::WriteScript::source: core::io::error::Error +impl devlaunch_core::flows::completion::InstallError +pub fn devlaunch_core::flows::completion::InstallError::written_script(&self) -> core::option::Option<&devlaunch_core::flows::completion::WrittenScript> +impl core::convert::From for devlaunch_core::flows::completion::InstallError +pub fn devlaunch_core::flows::completion::InstallError::from(devlaunch_core::domain::xdg::NoHomeDirectory) -> Self +impl core::fmt::Debug for devlaunch_core::flows::completion::InstallError +pub fn devlaunch_core::flows::completion::InstallError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +pub enum devlaunch_core::flows::completion::RcChange +pub devlaunch_core::flows::completion::RcChange::Added +pub devlaunch_core::flows::completion::RcChange::AlreadyInstalled +pub devlaunch_core::flows::completion::RcChange::Refreshed +impl core::clone::Clone for devlaunch_core::flows::completion::RcChange +pub fn devlaunch_core::flows::completion::RcChange::clone(&self) -> devlaunch_core::flows::completion::RcChange +impl core::cmp::Eq for devlaunch_core::flows::completion::RcChange +impl core::cmp::PartialEq for devlaunch_core::flows::completion::RcChange +pub fn devlaunch_core::flows::completion::RcChange::eq(&self, &devlaunch_core::flows::completion::RcChange) -> bool +impl core::fmt::Debug for devlaunch_core::flows::completion::RcChange +pub fn devlaunch_core::flows::completion::RcChange::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::Copy for devlaunch_core::flows::completion::RcChange +impl core::marker::StructuralPartialEq for devlaunch_core::flows::completion::RcChange +pub struct devlaunch_core::flows::completion::Installed +pub devlaunch_core::flows::completion::Installed::rc: std::path::PathBuf +pub devlaunch_core::flows::completion::Installed::rc_change: devlaunch_core::flows::completion::RcChange +pub devlaunch_core::flows::completion::Installed::script: std::path::PathBuf +pub devlaunch_core::flows::completion::Installed::script_state: devlaunch_core::flows::completion::FileState +impl core::clone::Clone for devlaunch_core::flows::completion::Installed +pub fn devlaunch_core::flows::completion::Installed::clone(&self) -> devlaunch_core::flows::completion::Installed +impl core::cmp::Eq for devlaunch_core::flows::completion::Installed +impl core::cmp::PartialEq for devlaunch_core::flows::completion::Installed +pub fn devlaunch_core::flows::completion::Installed::eq(&self, &devlaunch_core::flows::completion::Installed) -> bool +impl core::fmt::Debug for devlaunch_core::flows::completion::Installed +pub fn devlaunch_core::flows::completion::Installed::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::completion::Installed +pub struct devlaunch_core::flows::completion::WrittenScript +pub devlaunch_core::flows::completion::WrittenScript::path: std::path::PathBuf +pub devlaunch_core::flows::completion::WrittenScript::state: devlaunch_core::flows::completion::FileState +impl core::clone::Clone for devlaunch_core::flows::completion::WrittenScript +pub fn devlaunch_core::flows::completion::WrittenScript::clone(&self) -> devlaunch_core::flows::completion::WrittenScript +impl core::fmt::Debug for devlaunch_core::flows::completion::WrittenScript +pub fn devlaunch_core::flows::completion::WrittenScript::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +pub fn devlaunch_core::flows::completion::install(core::option::Option<&std::path::Path>) -> core::result::Result +pub mod devlaunch_core::flows::completion_cache +pub struct devlaunch_core::flows::completion_cache::CompletionData +pub devlaunch_core::flows::completion_cache::CompletionData::branches: alloc::vec::Vec +pub devlaunch_core::flows::completion_cache::CompletionData::owners: alloc::vec::Vec +pub devlaunch_core::flows::completion_cache::CompletionData::repos: alloc::vec::Vec +pub devlaunch_core::flows::completion_cache::CompletionData::workspaces: alloc::vec::Vec +impl devlaunch_core::flows::completion_cache::CompletionData +pub fn devlaunch_core::flows::completion_cache::CompletionData::as_json_line(&self) -> alloc::string::String +impl core::clone::Clone for devlaunch_core::flows::completion_cache::CompletionData +pub fn devlaunch_core::flows::completion_cache::CompletionData::clone(&self) -> devlaunch_core::flows::completion_cache::CompletionData +impl core::cmp::Eq for devlaunch_core::flows::completion_cache::CompletionData +impl core::cmp::PartialEq for devlaunch_core::flows::completion_cache::CompletionData +pub fn devlaunch_core::flows::completion_cache::CompletionData::eq(&self, &devlaunch_core::flows::completion_cache::CompletionData) -> bool +impl core::default::Default for devlaunch_core::flows::completion_cache::CompletionData +pub fn devlaunch_core::flows::completion_cache::CompletionData::default() -> devlaunch_core::flows::completion_cache::CompletionData +impl core::fmt::Debug for devlaunch_core::flows::completion_cache::CompletionData +pub fn devlaunch_core::flows::completion_cache::CompletionData::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::completion_cache::CompletionData +impl serde_core::ser::Serialize for devlaunch_core::flows::completion_cache::CompletionData +pub fn devlaunch_core::flows::completion_cache::CompletionData::serialize<__S>(&self, __S) -> core::result::Result<<__S as serde_core::ser::Serializer>::Ok, <__S as serde_core::ser::Serializer>::Error> where __S: serde_core::ser::Serializer +impl<'de> serde_core::de::Deserialize<'de> for devlaunch_core::flows::completion_cache::CompletionData +pub fn devlaunch_core::flows::completion_cache::CompletionData::deserialize<__D>(__D) -> core::result::Result::Error> where __D: serde_core::de::Deserializer<'de> +pub struct devlaunch_core::flows::completion_cache::Refreshed +pub devlaunch_core::flows::completion_cache::Refreshed::data: devlaunch_core::flows::completion_cache::CompletionData +pub devlaunch_core::flows::completion_cache::Refreshed::listing_refused: core::option::Option +pub devlaunch_core::flows::completion_cache::Refreshed::unreadable_sources: alloc::vec::Vec +pub fn devlaunch_core::flows::completion_cache::cache_path(&std::path::Path) -> std::path::PathBuf +pub fn devlaunch_core::flows::completion_cache::read_completion_cache(&std::path::Path) -> core::option::Option +pub fn devlaunch_core::flows::completion_cache::update_completion_cache(&mut devlaunch_core::flows::listing::CommandContext<'_>, &std::path::Path, &std::path::Path) -> devlaunch_core::flows::completion_cache::Refreshed +pub fn devlaunch_core::flows::completion_cache::wants_startup_cache_refresh>(&[S]) -> bool +pub mod devlaunch_core::flows::disk_usage +pub enum devlaunch_core::flows::disk_usage::DiskUsage +pub devlaunch_core::flows::disk_usage::DiskUsage::Measured +pub devlaunch_core::flows::disk_usage::DiskUsage::Measured::exclusive_bytes: u64 +pub devlaunch_core::flows::disk_usage::DiskUsage::PartlyUnreadable +pub devlaunch_core::flows::disk_usage::DiskUsage::PartlyUnreadable::at_least_bytes: u64 +pub devlaunch_core::flows::disk_usage::DiskUsage::PartlyUnreadable::unreadable: ClosedDoors +impl devlaunch_core::flows::disk_usage::DiskUsage +pub fn devlaunch_core::flows::disk_usage::DiskUsage::measured(u64) -> Self +impl core::clone::Clone for devlaunch_core::flows::disk_usage::DiskUsage +pub fn devlaunch_core::flows::disk_usage::DiskUsage::clone(&self) -> devlaunch_core::flows::disk_usage::DiskUsage +impl core::cmp::Eq for devlaunch_core::flows::disk_usage::DiskUsage +impl core::cmp::PartialEq for devlaunch_core::flows::disk_usage::DiskUsage +pub fn devlaunch_core::flows::disk_usage::DiskUsage::eq(&self, &devlaunch_core::flows::disk_usage::DiskUsage) -> bool +impl core::fmt::Debug for devlaunch_core::flows::disk_usage::DiskUsage +pub fn devlaunch_core::flows::disk_usage::DiskUsage::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::disk_usage::DiskUsage +pub fn devlaunch_core::flows::disk_usage::describe_usage(&devlaunch_core::flows::disk_usage::DiskUsage) -> alloc::string::String +pub mod devlaunch_core::flows::launch +pub enum devlaunch_core::flows::launch::BranchNotNamed +pub devlaunch_core::flows::launch::BranchNotNamed::Cold(devlaunch_core::flows::launch::ColdRefused) +pub devlaunch_core::flows::launch::BranchNotNamed::Repository(devlaunch_core::flows::repo_manager::EnsureRepoError) +impl core::clone::Clone for devlaunch_core::flows::launch::BranchNotNamed +pub fn devlaunch_core::flows::launch::BranchNotNamed::clone(&self) -> devlaunch_core::flows::launch::BranchNotNamed +impl core::cmp::Eq for devlaunch_core::flows::launch::BranchNotNamed +impl core::cmp::PartialEq for devlaunch_core::flows::launch::BranchNotNamed +pub fn devlaunch_core::flows::launch::BranchNotNamed::eq(&self, &devlaunch_core::flows::launch::BranchNotNamed) -> bool +impl core::fmt::Debug for devlaunch_core::flows::launch::BranchNotNamed +pub fn devlaunch_core::flows::launch::BranchNotNamed::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::launch::BranchNotNamed +pub enum devlaunch_core::flows::launch::LaunchAborted +pub devlaunch_core::flows::launch::LaunchAborted::DevpodNotRun(devlaunch_core::clients::devpod::NotRun) +pub devlaunch_core::flows::launch::LaunchAborted::ListingUnreadable(devlaunch_core::clients::devpod::ListingUnreadable) +pub devlaunch_core::flows::launch::LaunchAborted::SshNotRun(devlaunch_core::clients::ssh::NotRun) +impl core::clone::Clone for devlaunch_core::flows::launch::LaunchAborted +pub fn devlaunch_core::flows::launch::LaunchAborted::clone(&self) -> devlaunch_core::flows::launch::LaunchAborted +impl core::cmp::Eq for devlaunch_core::flows::launch::LaunchAborted +impl core::cmp::PartialEq for devlaunch_core::flows::launch::LaunchAborted +pub fn devlaunch_core::flows::launch::LaunchAborted::eq(&self, &devlaunch_core::flows::launch::LaunchAborted) -> bool +impl core::fmt::Debug for devlaunch_core::flows::launch::LaunchAborted +pub fn devlaunch_core::flows::launch::LaunchAborted::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::launch::LaunchAborted +pub enum devlaunch_core::flows::launch::LaunchNotice +pub devlaunch_core::flows::launch::LaunchNotice::AlreadyRunning +pub devlaunch_core::flows::launch::LaunchNotice::AlreadyRunning::workspace_id: alloc::string::String +pub devlaunch_core::flows::launch::LaunchNotice::AlreadyRunningAttaching +pub devlaunch_core::flows::launch::LaunchNotice::AlreadyRunningAttaching::workspace_id: alloc::string::String +pub devlaunch_core::flows::launch::LaunchNotice::BroughtUpBySibling +pub devlaunch_core::flows::launch::LaunchNotice::BroughtUpBySibling::workspace_id: alloc::string::String +pub devlaunch_core::flows::launch::LaunchNotice::Cache(devlaunch_core::flows::repo_manager::CacheNotice) +pub devlaunch_core::flows::launch::LaunchNotice::DevcontainerIgnoredRunning +pub devlaunch_core::flows::launch::LaunchNotice::DevcontainerIgnoredRunning::spec: alloc::string::String +pub devlaunch_core::flows::launch::LaunchNotice::DevcontainerIgnoredRunning::workspace_id: alloc::string::String +pub devlaunch_core::flows::launch::LaunchNotice::DevpodSessionFailed +pub devlaunch_core::flows::launch::LaunchNotice::DevpodSessionFailed::exit: devlaunch_runner::Exit +pub devlaunch_core::flows::launch::LaunchNotice::LaunchLockUnavailable +pub devlaunch_core::flows::launch::LaunchNotice::LaunchLockUnavailable::reason: alloc::string::String +pub devlaunch_core::flows::launch::LaunchNotice::LaunchLockUnavailable::workspace_id: alloc::string::String +pub devlaunch_core::flows::launch::LaunchNotice::Lifecycle(devlaunch_core::flows::lifecycle::LifecycleNotice) +pub devlaunch_core::flows::launch::LaunchNotice::NoGitHubToken(devlaunch_core::clients::gh::GhEvent) +pub devlaunch_core::flows::launch::LaunchNotice::NoTerminalAlias +pub devlaunch_core::flows::launch::LaunchNotice::NoTerminalAlias::workspace_id: alloc::string::String +pub devlaunch_core::flows::launch::LaunchNotice::PixiCacheNotADirectory +pub devlaunch_core::flows::launch::LaunchNotice::PixiCacheNotADirectory::source: std::path::PathBuf +pub devlaunch_core::flows::launch::LaunchNotice::PixiCacheNotCreated +pub devlaunch_core::flows::launch::LaunchNotice::PixiCacheNotCreated::reason: alloc::string::String +pub devlaunch_core::flows::launch::LaunchNotice::PixiCacheNotCreated::source: std::path::PathBuf +pub devlaunch_core::flows::launch::LaunchNotice::SshCommand +pub devlaunch_core::flows::launch::LaunchNotice::SshCommand::argv: alloc::vec::Vec +pub devlaunch_core::flows::launch::LaunchNotice::StartingForDotfiles +pub devlaunch_core::flows::launch::LaunchNotice::StartingForDotfiles::workspace_id: alloc::string::String +pub devlaunch_core::flows::launch::LaunchNotice::TokenNotStaged +pub devlaunch_core::flows::launch::LaunchNotice::TokenNotStaged::reason: alloc::string::String +pub devlaunch_core::flows::launch::LaunchNotice::WaitingForSiblingLaunch +pub devlaunch_core::flows::launch::LaunchNotice::WaitingForSiblingLaunch::workspace_id: alloc::string::String +impl core::clone::Clone for devlaunch_core::flows::launch::LaunchNotice +pub fn devlaunch_core::flows::launch::LaunchNotice::clone(&self) -> devlaunch_core::flows::launch::LaunchNotice +impl core::cmp::Eq for devlaunch_core::flows::launch::LaunchNotice +impl core::cmp::PartialEq for devlaunch_core::flows::launch::LaunchNotice +pub fn devlaunch_core::flows::launch::LaunchNotice::eq(&self, &devlaunch_core::flows::launch::LaunchNotice) -> bool +impl core::fmt::Debug for devlaunch_core::flows::launch::LaunchNotice +pub fn devlaunch_core::flows::launch::LaunchNotice::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::launch::LaunchNotice +pub enum devlaunch_core::flows::launch::LaunchRefusal +pub devlaunch_core::flows::launch::LaunchRefusal::BranchNotNamed +pub devlaunch_core::flows::launch::LaunchRefusal::BranchNotNamed::error: devlaunch_core::flows::launch::BranchNotNamed +pub devlaunch_core::flows::launch::LaunchRefusal::BranchNotNamed::owner: alloc::string::String +pub devlaunch_core::flows::launch::LaunchRefusal::BranchNotNamed::repo: alloc::string::String +pub devlaunch_core::flows::launch::LaunchRefusal::NoSession(devlaunch_core::flows::launch::SessionRefused) +pub devlaunch_core::flows::launch::LaunchRefusal::NotPrepared +pub devlaunch_core::flows::launch::LaunchRefusal::NotPrepared::branch: alloc::string::String +pub devlaunch_core::flows::launch::LaunchRefusal::NotPrepared::error: devlaunch_core::flows::launch::NotPrepared +pub devlaunch_core::flows::launch::LaunchRefusal::NotPrepared::owner: alloc::string::String +pub devlaunch_core::flows::launch::LaunchRefusal::NotPrepared::repo: alloc::string::String +pub devlaunch_core::flows::launch::LaunchRefusal::StopRefused +pub devlaunch_core::flows::launch::LaunchRefusal::StopRefused::exit: devlaunch_runner::Exit +pub devlaunch_core::flows::launch::LaunchRefusal::UnknownWorkspace +pub devlaunch_core::flows::launch::LaunchRefusal::UnknownWorkspace::name: alloc::string::String +pub devlaunch_core::flows::launch::LaunchRefusal::UnsafeSpec(devlaunch_core::domain::workspace_id::UnsafeName) +pub devlaunch_core::flows::launch::LaunchRefusal::UpRefused +pub devlaunch_core::flows::launch::LaunchRefusal::UpRefused::exit: devlaunch_runner::Exit +impl core::clone::Clone for devlaunch_core::flows::launch::LaunchRefusal +pub fn devlaunch_core::flows::launch::LaunchRefusal::clone(&self) -> devlaunch_core::flows::launch::LaunchRefusal +impl core::cmp::Eq for devlaunch_core::flows::launch::LaunchRefusal +impl core::cmp::PartialEq for devlaunch_core::flows::launch::LaunchRefusal +pub fn devlaunch_core::flows::launch::LaunchRefusal::eq(&self, &devlaunch_core::flows::launch::LaunchRefusal) -> bool +impl core::fmt::Debug for devlaunch_core::flows::launch::LaunchRefusal +pub fn devlaunch_core::flows::launch::LaunchRefusal::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::launch::LaunchRefusal +pub enum devlaunch_core::flows::launch::LaunchVerb +pub devlaunch_core::flows::launch::LaunchVerb::Attach +pub devlaunch_core::flows::launch::LaunchVerb::Attach::command: core::option::Option +pub devlaunch_core::flows::launch::LaunchVerb::Code +pub devlaunch_core::flows::launch::LaunchVerb::Dotfiles +pub devlaunch_core::flows::launch::LaunchVerb::Recreate +pub devlaunch_core::flows::launch::LaunchVerb::Reset +pub devlaunch_core::flows::launch::LaunchVerb::Restart +pub devlaunch_core::flows::launch::LaunchVerb::Up +impl core::clone::Clone for devlaunch_core::flows::launch::LaunchVerb +pub fn devlaunch_core::flows::launch::LaunchVerb::clone(&self) -> devlaunch_core::flows::launch::LaunchVerb +impl core::cmp::Eq for devlaunch_core::flows::launch::LaunchVerb +impl core::cmp::PartialEq for devlaunch_core::flows::launch::LaunchVerb +pub fn devlaunch_core::flows::launch::LaunchVerb::eq(&self, &devlaunch_core::flows::launch::LaunchVerb) -> bool +impl core::fmt::Debug for devlaunch_core::flows::launch::LaunchVerb +pub fn devlaunch_core::flows::launch::LaunchVerb::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::launch::LaunchVerb +pub enum devlaunch_core::flows::launch::Launched +pub devlaunch_core::flows::launch::Launched::AlreadyRunning +pub devlaunch_core::flows::launch::Launched::Ready +pub devlaunch_core::flows::launch::Launched::Refused(devlaunch_core::flows::launch::LaunchRefusal) +pub devlaunch_core::flows::launch::Launched::Session(devlaunch_core::flows::launch::Session) +impl core::clone::Clone for devlaunch_core::flows::launch::Launched +pub fn devlaunch_core::flows::launch::Launched::clone(&self) -> devlaunch_core::flows::launch::Launched +impl core::cmp::Eq for devlaunch_core::flows::launch::Launched +impl core::cmp::PartialEq for devlaunch_core::flows::launch::Launched +pub fn devlaunch_core::flows::launch::Launched::eq(&self, &devlaunch_core::flows::launch::Launched) -> bool +impl core::fmt::Debug for devlaunch_core::flows::launch::Launched +pub fn devlaunch_core::flows::launch::Launched::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::launch::Launched +pub enum devlaunch_core::flows::launch::NotPrepared +pub devlaunch_core::flows::launch::NotPrepared::Cold(devlaunch_core::flows::launch::ColdRefused) +pub devlaunch_core::flows::launch::NotPrepared::Preparation(devlaunch_core::flows::workspace_clone::PrepareColdError) +impl core::clone::Clone for devlaunch_core::flows::launch::NotPrepared +pub fn devlaunch_core::flows::launch::NotPrepared::clone(&self) -> devlaunch_core::flows::launch::NotPrepared +impl core::cmp::Eq for devlaunch_core::flows::launch::NotPrepared +impl core::cmp::PartialEq for devlaunch_core::flows::launch::NotPrepared +pub fn devlaunch_core::flows::launch::NotPrepared::eq(&self, &devlaunch_core::flows::launch::NotPrepared) -> bool +impl core::fmt::Debug for devlaunch_core::flows::launch::NotPrepared +pub fn devlaunch_core::flows::launch::NotPrepared::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::launch::NotPrepared +pub enum devlaunch_core::flows::launch::Placement +pub devlaunch_core::flows::launch::Placement::Creating +pub devlaunch_core::flows::launch::Placement::Creating::source: alloc::string::String +pub devlaunch_core::flows::launch::Placement::Creating::workspace_id: alloc::string::String +pub devlaunch_core::flows::launch::Placement::Known +pub devlaunch_core::flows::launch::Placement::Known::state: devlaunch_core::clients::devpod::ContainerState +pub devlaunch_core::flows::launch::Placement::Known::workspace_id: alloc::string::String +pub devlaunch_core::flows::launch::Placement::Listed +pub devlaunch_core::flows::launch::Placement::Listed::workspace_id: alloc::string::String +impl devlaunch_core::flows::launch::Placement +pub fn devlaunch_core::flows::launch::Placement::source(&self) -> &str +pub fn devlaunch_core::flows::launch::Placement::workspace_id(&self) -> &str +impl core::clone::Clone for devlaunch_core::flows::launch::Placement +pub fn devlaunch_core::flows::launch::Placement::clone(&self) -> devlaunch_core::flows::launch::Placement +impl core::cmp::Eq for devlaunch_core::flows::launch::Placement +impl core::cmp::PartialEq for devlaunch_core::flows::launch::Placement +pub fn devlaunch_core::flows::launch::Placement::eq(&self, &devlaunch_core::flows::launch::Placement) -> bool +impl core::fmt::Debug for devlaunch_core::flows::launch::Placement +pub fn devlaunch_core::flows::launch::Placement::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::launch::Placement +pub enum devlaunch_core::flows::launch::Plan +pub devlaunch_core::flows::launch::Plan::Creatable +pub devlaunch_core::flows::launch::Plan::Creatable::source: alloc::string::String +pub devlaunch_core::flows::launch::Plan::Creatable::workspace_id: alloc::string::String +pub devlaunch_core::flows::launch::Plan::Existing +pub devlaunch_core::flows::launch::Plan::Existing::name: alloc::string::String +pub devlaunch_core::flows::launch::Plan::Triple +pub devlaunch_core::flows::launch::Plan::Triple::branch: core::option::Option +pub devlaunch_core::flows::launch::Plan::Triple::owner: alloc::string::String +pub devlaunch_core::flows::launch::Plan::Triple::remote_url: alloc::string::String +pub devlaunch_core::flows::launch::Plan::Triple::repo: alloc::string::String +impl core::clone::Clone for devlaunch_core::flows::launch::Plan +pub fn devlaunch_core::flows::launch::Plan::clone(&self) -> devlaunch_core::flows::launch::Plan +impl core::cmp::Eq for devlaunch_core::flows::launch::Plan +impl core::cmp::PartialEq for devlaunch_core::flows::launch::Plan +pub fn devlaunch_core::flows::launch::Plan::eq(&self, &devlaunch_core::flows::launch::Plan) -> bool +impl core::fmt::Debug for devlaunch_core::flows::launch::Plan +pub fn devlaunch_core::flows::launch::Plan::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::launch::Plan +pub enum devlaunch_core::flows::launch::Resolution +pub devlaunch_core::flows::launch::Resolution::Cold +pub devlaunch_core::flows::launch::Resolution::Cold::workspace: devlaunch_core::domain::workspace_id::WorkspaceId +pub devlaunch_core::flows::launch::Resolution::Warm +pub devlaunch_core::flows::launch::Resolution::Warm::placement: devlaunch_core::flows::launch::Placement +impl core::clone::Clone for devlaunch_core::flows::launch::Resolution +pub fn devlaunch_core::flows::launch::Resolution::clone(&self) -> devlaunch_core::flows::launch::Resolution +impl core::cmp::Eq for devlaunch_core::flows::launch::Resolution +impl core::cmp::PartialEq for devlaunch_core::flows::launch::Resolution +pub fn devlaunch_core::flows::launch::Resolution::eq(&self, &devlaunch_core::flows::launch::Resolution) -> bool +impl core::fmt::Debug for devlaunch_core::flows::launch::Resolution +pub fn devlaunch_core::flows::launch::Resolution::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::launch::Resolution +pub enum devlaunch_core::flows::launch::Session +pub devlaunch_core::flows::launch::Session::DevpodFailed +pub devlaunch_core::flows::launch::Session::DevpodFailed::exit: devlaunch_runner::Exit +pub devlaunch_core::flows::launch::Session::RemoteExit +pub devlaunch_core::flows::launch::Session::RemoteExit::status: i32 +pub devlaunch_core::flows::launch::Session::Terminal +pub devlaunch_core::flows::launch::Session::Terminal::exit: devlaunch_runner::Exit +impl devlaunch_core::flows::launch::Session +pub fn devlaunch_core::flows::launch::Session::exit_status(self) -> i32 +impl core::clone::Clone for devlaunch_core::flows::launch::Session +pub fn devlaunch_core::flows::launch::Session::clone(&self) -> devlaunch_core::flows::launch::Session +impl core::cmp::Eq for devlaunch_core::flows::launch::Session +impl core::cmp::PartialEq for devlaunch_core::flows::launch::Session +pub fn devlaunch_core::flows::launch::Session::eq(&self, &devlaunch_core::flows::launch::Session) -> bool +impl core::fmt::Debug for devlaunch_core::flows::launch::Session +pub fn devlaunch_core::flows::launch::Session::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::Copy for devlaunch_core::flows::launch::Session +impl core::marker::StructuralPartialEq for devlaunch_core::flows::launch::Session +pub enum devlaunch_core::flows::launch::SessionRefused +pub devlaunch_core::flows::launch::SessionRefused::Devpod(devlaunch_core::clients::devpod::NotRun) +pub devlaunch_core::flows::launch::SessionRefused::Ssh(devlaunch_core::clients::ssh::NotRun) +pub devlaunch_core::flows::launch::SessionRefused::Unquotable(devlaunch_core::flows::launch::UnquotableCommand) +pub devlaunch_core::flows::launch::SessionRefused::UnsafeRequest(devlaunch_core::clients::ssh::UnsafeRequest) +impl core::clone::Clone for devlaunch_core::flows::launch::SessionRefused +pub fn devlaunch_core::flows::launch::SessionRefused::clone(&self) -> devlaunch_core::flows::launch::SessionRefused +impl core::cmp::Eq for devlaunch_core::flows::launch::SessionRefused +impl core::cmp::PartialEq for devlaunch_core::flows::launch::SessionRefused +pub fn devlaunch_core::flows::launch::SessionRefused::eq(&self, &devlaunch_core::flows::launch::SessionRefused) -> bool +impl core::fmt::Debug for devlaunch_core::flows::launch::SessionRefused +pub fn devlaunch_core::flows::launch::SessionRefused::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::launch::SessionRefused +pub struct devlaunch_core::flows::launch::Cold<'a, 'r> +pub devlaunch_core::flows::launch::Cold::clones: &'a devlaunch_core::flows::workspace_clone::WorkspaceCloneManager<'r> +pub devlaunch_core::flows::launch::Cold::storage: &'a mut devlaunch_core::domain::metadata::MetadataStorage +pub struct devlaunch_core::flows::launch::ColdRefused +pub devlaunch_core::flows::launch::ColdRefused::reason: alloc::string::String +impl core::clone::Clone for devlaunch_core::flows::launch::ColdRefused +pub fn devlaunch_core::flows::launch::ColdRefused::clone(&self) -> devlaunch_core::flows::launch::ColdRefused +impl core::cmp::Eq for devlaunch_core::flows::launch::ColdRefused +impl core::cmp::PartialEq for devlaunch_core::flows::launch::ColdRefused +pub fn devlaunch_core::flows::launch::ColdRefused::eq(&self, &devlaunch_core::flows::launch::ColdRefused) -> bool +impl core::fmt::Debug for devlaunch_core::flows::launch::ColdRefused +pub fn devlaunch_core::flows::launch::ColdRefused::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::launch::ColdRefused +pub struct devlaunch_core::flows::launch::Host +impl devlaunch_core::flows::launch::Host +pub fn devlaunch_core::flows::launch::Host::from_process(impl core::convert::Into) -> Self +impl core::clone::Clone for devlaunch_core::flows::launch::Host +pub fn devlaunch_core::flows::launch::Host::clone(&self) -> devlaunch_core::flows::launch::Host +impl core::cmp::Eq for devlaunch_core::flows::launch::Host +impl core::cmp::PartialEq for devlaunch_core::flows::launch::Host +pub fn devlaunch_core::flows::launch::Host::eq(&self, &devlaunch_core::flows::launch::Host) -> bool +impl core::default::Default for devlaunch_core::flows::launch::Host +pub fn devlaunch_core::flows::launch::Host::default() -> devlaunch_core::flows::launch::Host +impl core::fmt::Debug for devlaunch_core::flows::launch::Host +pub fn devlaunch_core::flows::launch::Host::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::launch::Host +pub struct devlaunch_core::flows::launch::Launch<'a, 'r, 'l> +impl<'a, 'r, 'l> devlaunch_core::flows::launch::Launch<'a, 'r, 'l> +pub fn devlaunch_core::flows::launch::Launch<'a, 'r, 'l>::new(&'a mut devlaunch_core::flows::listing::CommandContext<'r>, &'a mut devlaunch_core::flows::lifecycle::Refresh<'l>, &'a mut dyn devlaunch_core::flows::launch::ColdMachinery<'r>, &'a dyn devlaunch_core::flows::launch::Provision, &'a devlaunch_core::flows::launch::Host, &'a mut dyn core::ops::function::FnMut(&str), &'a mut dyn devlaunch_core::notices::Notices) -> Self +pub fn devlaunch_core::flows::launch::Launch<'a, 'r, 'l>::run(&mut self, &str, &devlaunch_core::flows::launch::LaunchVerb, core::option::Option<&devlaunch_core::domain::spec::DevcontainerPath>) -> core::result::Result +pub struct devlaunch_core::flows::launch::UnquotableCommand +pub devlaunch_core::flows::launch::UnquotableCommand::command: alloc::string::String +impl core::clone::Clone for devlaunch_core::flows::launch::UnquotableCommand +pub fn devlaunch_core::flows::launch::UnquotableCommand::clone(&self) -> devlaunch_core::flows::launch::UnquotableCommand +impl core::cmp::Eq for devlaunch_core::flows::launch::UnquotableCommand +impl core::cmp::PartialEq for devlaunch_core::flows::launch::UnquotableCommand +pub fn devlaunch_core::flows::launch::UnquotableCommand::eq(&self, &devlaunch_core::flows::launch::UnquotableCommand) -> bool +impl core::fmt::Debug for devlaunch_core::flows::launch::UnquotableCommand +pub fn devlaunch_core::flows::launch::UnquotableCommand::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::launch::UnquotableCommand +pub trait devlaunch_core::flows::launch::ColdMachinery<'r> +pub fn devlaunch_core::flows::launch::ColdMachinery::open(&mut self) -> core::result::Result, devlaunch_core::flows::launch::ColdRefused> +pub trait devlaunch_core::flows::launch::Provision +pub fn devlaunch_core::flows::launch::Provision::provision_tools(&self, &dyn devlaunch_runner::Runner, &str) -> core::result::Result<(), devlaunch_core::flows::provision::DevpodMissing> +pub fn devlaunch_core::flows::launch::plan(&str) -> core::result::Result +pub fn devlaunch_core::flows::launch::resolve_triple(&mut devlaunch_core::flows::listing::CommandContext<'_>, &mut dyn devlaunch_core::flows::launch::ColdMachinery<'_>, &devlaunch_core::domain::workspace_id::WorkspaceId, &mut dyn devlaunch_core::notices::Notices) -> core::result::Result +pub mod devlaunch_core::flows::lifecycle +pub enum devlaunch_core::flows::lifecycle::Adoption +pub devlaunch_core::flows::lifecycle::Adoption::Refused +pub devlaunch_core::flows::lifecycle::Adoption::Refused::failure: devlaunch_core::flows::lifecycle::RepointFailure +pub devlaunch_core::flows::lifecycle::Adoption::Refused::workspace_id: alloc::string::String +pub devlaunch_core::flows::lifecycle::Adoption::Repointed(alloc::boxed::Box) +impl core::clone::Clone for devlaunch_core::flows::lifecycle::Adoption +pub fn devlaunch_core::flows::lifecycle::Adoption::clone(&self) -> devlaunch_core::flows::lifecycle::Adoption +impl core::cmp::Eq for devlaunch_core::flows::lifecycle::Adoption +impl core::cmp::PartialEq for devlaunch_core::flows::lifecycle::Adoption +pub fn devlaunch_core::flows::lifecycle::Adoption::eq(&self, &devlaunch_core::flows::lifecycle::Adoption) -> bool +impl core::fmt::Debug for devlaunch_core::flows::lifecycle::Adoption +pub fn devlaunch_core::flows::lifecycle::Adoption::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::lifecycle::Adoption +pub enum devlaunch_core::flows::lifecycle::ChildWork +pub devlaunch_core::flows::lifecycle::ChildWork::NothingToDo +pub devlaunch_core::flows::lifecycle::ChildWork::RefreshAndSweep +impl core::clone::Clone for devlaunch_core::flows::lifecycle::ChildWork +pub fn devlaunch_core::flows::lifecycle::ChildWork::clone(&self) -> devlaunch_core::flows::lifecycle::ChildWork +impl core::cmp::Eq for devlaunch_core::flows::lifecycle::ChildWork +impl core::cmp::PartialEq for devlaunch_core::flows::lifecycle::ChildWork +pub fn devlaunch_core::flows::lifecycle::ChildWork::eq(&self, &devlaunch_core::flows::lifecycle::ChildWork) -> bool +impl core::fmt::Debug for devlaunch_core::flows::lifecycle::ChildWork +pub fn devlaunch_core::flows::lifecycle::ChildWork::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::Copy for devlaunch_core::flows::lifecycle::ChildWork +impl core::marker::StructuralPartialEq for devlaunch_core::flows::lifecycle::ChildWork +pub enum devlaunch_core::flows::lifecycle::DeleteOutcome +pub devlaunch_core::flows::lifecycle::DeleteOutcome::Deleted +pub devlaunch_core::flows::lifecycle::DeleteOutcome::Deleted::clone: core::result::Result +pub devlaunch_core::flows::lifecycle::DeleteOutcome::DevpodRefused +pub devlaunch_core::flows::lifecycle::DeleteOutcome::DevpodRefused::exit: devlaunch_runner::Exit +impl core::clone::Clone for devlaunch_core::flows::lifecycle::DeleteOutcome +pub fn devlaunch_core::flows::lifecycle::DeleteOutcome::clone(&self) -> devlaunch_core::flows::lifecycle::DeleteOutcome +impl core::cmp::Eq for devlaunch_core::flows::lifecycle::DeleteOutcome +impl core::cmp::PartialEq for devlaunch_core::flows::lifecycle::DeleteOutcome +pub fn devlaunch_core::flows::lifecycle::DeleteOutcome::eq(&self, &devlaunch_core::flows::lifecycle::DeleteOutcome) -> bool +impl core::fmt::Debug for devlaunch_core::flows::lifecycle::DeleteOutcome +pub fn devlaunch_core::flows::lifecycle::DeleteOutcome::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::lifecycle::DeleteOutcome +pub enum devlaunch_core::flows::lifecycle::Guarded +pub devlaunch_core::flows::lifecycle::Guarded::MayRemove +pub devlaunch_core::flows::lifecycle::Guarded::Refused(devlaunch_core::flows::lifecycle::RemovalRefused) +impl core::clone::Clone for devlaunch_core::flows::lifecycle::Guarded +pub fn devlaunch_core::flows::lifecycle::Guarded::clone(&self) -> devlaunch_core::flows::lifecycle::Guarded +impl core::cmp::Eq for devlaunch_core::flows::lifecycle::Guarded +impl core::cmp::PartialEq for devlaunch_core::flows::lifecycle::Guarded +pub fn devlaunch_core::flows::lifecycle::Guarded::eq(&self, &devlaunch_core::flows::lifecycle::Guarded) -> bool +impl core::fmt::Debug for devlaunch_core::flows::lifecycle::Guarded +pub fn devlaunch_core::flows::lifecycle::Guarded::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::lifecycle::Guarded +pub enum devlaunch_core::flows::lifecycle::Insistence +pub devlaunch_core::flows::lifecycle::Insistence::Insisted +pub devlaunch_core::flows::lifecycle::Insistence::NotInsisted +impl core::clone::Clone for devlaunch_core::flows::lifecycle::Insistence +pub fn devlaunch_core::flows::lifecycle::Insistence::clone(&self) -> devlaunch_core::flows::lifecycle::Insistence +impl core::cmp::Eq for devlaunch_core::flows::lifecycle::Insistence +impl core::cmp::PartialEq for devlaunch_core::flows::lifecycle::Insistence +pub fn devlaunch_core::flows::lifecycle::Insistence::eq(&self, &devlaunch_core::flows::lifecycle::Insistence) -> bool +impl core::fmt::Debug for devlaunch_core::flows::lifecycle::Insistence +pub fn devlaunch_core::flows::lifecycle::Insistence::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::Copy for devlaunch_core::flows::lifecycle::Insistence +impl core::marker::StructuralPartialEq for devlaunch_core::flows::lifecycle::Insistence +pub enum devlaunch_core::flows::lifecycle::KeptBecause +pub devlaunch_core::flows::lifecycle::KeptBecause::Objected(devlaunch_core::flows::lifecycle::Objection) +pub devlaunch_core::flows::lifecycle::KeptBecause::RecordsDisagree +pub devlaunch_core::flows::lifecycle::KeptBecause::RecordsDisagree::sourced_at: alloc::string::String +pub devlaunch_core::flows::lifecycle::KeptBecause::RecordsDisagree::workspace_id: alloc::string::String +pub devlaunch_core::flows::lifecycle::KeptBecause::StillOpened +pub devlaunch_core::flows::lifecycle::KeptBecause::StillOpened::workspace_id: alloc::string::String +impl core::clone::Clone for devlaunch_core::flows::lifecycle::KeptBecause +pub fn devlaunch_core::flows::lifecycle::KeptBecause::clone(&self) -> devlaunch_core::flows::lifecycle::KeptBecause +impl core::cmp::Eq for devlaunch_core::flows::lifecycle::KeptBecause +impl core::cmp::PartialEq for devlaunch_core::flows::lifecycle::KeptBecause +pub fn devlaunch_core::flows::lifecycle::KeptBecause::eq(&self, &devlaunch_core::flows::lifecycle::KeptBecause) -> bool +impl core::fmt::Debug for devlaunch_core::flows::lifecycle::KeptBecause +pub fn devlaunch_core::flows::lifecycle::KeptBecause::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::lifecycle::KeptBecause +pub enum devlaunch_core::flows::lifecycle::LifecycleNotice +pub devlaunch_core::flows::lifecycle::LifecycleNotice::AddressingRecordedWorkspace +pub devlaunch_core::flows::lifecycle::LifecycleNotice::AddressingRecordedWorkspace::branch: alloc::string::String +pub devlaunch_core::flows::lifecycle::LifecycleNotice::AddressingRecordedWorkspace::derived: alloc::string::String +pub devlaunch_core::flows::lifecycle::LifecycleNotice::AddressingRecordedWorkspace::owner: alloc::string::String +pub devlaunch_core::flows::lifecycle::LifecycleNotice::AddressingRecordedWorkspace::recorded: alloc::string::String +pub devlaunch_core::flows::lifecycle::LifecycleNotice::AddressingRecordedWorkspace::repo: alloc::string::String +pub devlaunch_core::flows::lifecycle::LifecycleNotice::Cache(devlaunch_core::flows::repo_manager::CacheNotice) +pub devlaunch_core::flows::lifecycle::LifecycleNotice::CloneNotRemoved +pub devlaunch_core::flows::lifecycle::LifecycleNotice::CloneNotRemoved::refusal: devlaunch_core::flows::workspace_clone::RemoveWorkspaceError +pub devlaunch_core::flows::lifecycle::LifecycleNotice::CloneNotRemoved::workspace_id: alloc::string::String +pub devlaunch_core::flows::lifecycle::LifecycleNotice::CloneRemoved +pub devlaunch_core::flows::lifecycle::LifecycleNotice::CloneRemoved::workspace_id: alloc::string::String +pub devlaunch_core::flows::lifecycle::LifecycleNotice::RecordNotDropped +pub devlaunch_core::flows::lifecycle::LifecycleNotice::RecordNotDropped::path: std::path::PathBuf +pub devlaunch_core::flows::lifecycle::LifecycleNotice::RecordNotDropped::refusal: devlaunch_core::domain::metadata::MetadataError +impl core::clone::Clone for devlaunch_core::flows::lifecycle::LifecycleNotice +pub fn devlaunch_core::flows::lifecycle::LifecycleNotice::clone(&self) -> devlaunch_core::flows::lifecycle::LifecycleNotice +impl core::cmp::Eq for devlaunch_core::flows::lifecycle::LifecycleNotice +impl core::cmp::PartialEq for devlaunch_core::flows::lifecycle::LifecycleNotice +pub fn devlaunch_core::flows::lifecycle::LifecycleNotice::eq(&self, &devlaunch_core::flows::lifecycle::LifecycleNotice) -> bool +impl core::fmt::Debug for devlaunch_core::flows::lifecycle::LifecycleNotice +pub fn devlaunch_core::flows::lifecycle::LifecycleNotice::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::lifecycle::LifecycleNotice +pub enum devlaunch_core::flows::lifecycle::NotAdopted +pub devlaunch_core::flows::lifecycle::NotAdopted::CloneWantedByManyWorkspaces +pub devlaunch_core::flows::lifecycle::NotAdopted::CloneWantedByManyWorkspaces::clone: std::path::PathBuf +pub devlaunch_core::flows::lifecycle::NotAdopted::CloneWantedByManyWorkspaces::workspaces: usize +pub devlaunch_core::flows::lifecycle::NotAdopted::NameAnsweredByManyClones(devlaunch_core::domain::workspace_state::NonEmpty) +pub devlaunch_core::flows::lifecycle::NotAdopted::NoCloneAnswers +impl core::clone::Clone for devlaunch_core::flows::lifecycle::NotAdopted +pub fn devlaunch_core::flows::lifecycle::NotAdopted::clone(&self) -> devlaunch_core::flows::lifecycle::NotAdopted +impl core::cmp::Eq for devlaunch_core::flows::lifecycle::NotAdopted +impl core::cmp::PartialEq for devlaunch_core::flows::lifecycle::NotAdopted +pub fn devlaunch_core::flows::lifecycle::NotAdopted::eq(&self, &devlaunch_core::flows::lifecycle::NotAdopted) -> bool +impl core::fmt::Debug for devlaunch_core::flows::lifecycle::NotAdopted +pub fn devlaunch_core::flows::lifecycle::NotAdopted::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::lifecycle::NotAdopted +pub enum devlaunch_core::flows::lifecycle::Objection +pub devlaunch_core::flows::lifecycle::Objection::CouldNotTell(devlaunch_core::domain::workspace_state::CouldNotTell) +pub devlaunch_core::flows::lifecycle::Objection::WouldLose(devlaunch_core::domain::workspace_state::NonEmpty) +impl core::clone::Clone for devlaunch_core::flows::lifecycle::Objection +pub fn devlaunch_core::flows::lifecycle::Objection::clone(&self) -> devlaunch_core::flows::lifecycle::Objection +impl core::cmp::Eq for devlaunch_core::flows::lifecycle::Objection +impl core::cmp::PartialEq for devlaunch_core::flows::lifecycle::Objection +pub fn devlaunch_core::flows::lifecycle::Objection::eq(&self, &devlaunch_core::flows::lifecycle::Objection) -> bool +impl core::fmt::Debug for devlaunch_core::flows::lifecycle::Objection +pub fn devlaunch_core::flows::lifecycle::Objection::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::lifecycle::Objection +pub enum devlaunch_core::flows::lifecycle::Promotion +pub devlaunch_core::flows::lifecycle::Promotion::Insisted +pub devlaunch_core::flows::lifecycle::Promotion::Insisted::despite: devlaunch_core::flows::lifecycle::Objection +pub devlaunch_core::flows::lifecycle::Promotion::Unopposed +impl core::clone::Clone for devlaunch_core::flows::lifecycle::Promotion +pub fn devlaunch_core::flows::lifecycle::Promotion::clone(&self) -> devlaunch_core::flows::lifecycle::Promotion +impl core::cmp::Eq for devlaunch_core::flows::lifecycle::Promotion +impl core::cmp::PartialEq for devlaunch_core::flows::lifecycle::Promotion +pub fn devlaunch_core::flows::lifecycle::Promotion::eq(&self, &devlaunch_core::flows::lifecycle::Promotion) -> bool +impl core::fmt::Debug for devlaunch_core::flows::lifecycle::Promotion +pub fn devlaunch_core::flows::lifecycle::Promotion::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::lifecycle::Promotion +pub enum devlaunch_core::flows::lifecycle::PruneError +pub devlaunch_core::flows::lifecycle::PruneError::Listing(devlaunch_core::clients::devpod::ListingUnreadable) +pub devlaunch_core::flows::lifecycle::PruneError::Lock(devlaunch_core::domain::locks::LockError) +impl core::fmt::Debug for devlaunch_core::flows::lifecycle::PruneError +pub fn devlaunch_core::flows::lifecycle::PruneError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +pub enum devlaunch_core::flows::lifecycle::PruneOutcome +pub devlaunch_core::flows::lifecycle::PruneOutcome::Acted(devlaunch_core::flows::lifecycle::PruneReport) +pub devlaunch_core::flows::lifecycle::PruneOutcome::Unlocatable(devlaunch_core::domain::workspace_state::NonEmpty) +impl core::clone::Clone for devlaunch_core::flows::lifecycle::PruneOutcome +pub fn devlaunch_core::flows::lifecycle::PruneOutcome::clone(&self) -> devlaunch_core::flows::lifecycle::PruneOutcome +impl core::cmp::Eq for devlaunch_core::flows::lifecycle::PruneOutcome +impl core::cmp::PartialEq for devlaunch_core::flows::lifecycle::PruneOutcome +pub fn devlaunch_core::flows::lifecycle::PruneOutcome::eq(&self, &devlaunch_core::flows::lifecycle::PruneOutcome) -> bool +impl core::fmt::Debug for devlaunch_core::flows::lifecycle::PruneOutcome +pub fn devlaunch_core::flows::lifecycle::PruneOutcome::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::lifecycle::PruneOutcome +pub enum devlaunch_core::flows::lifecycle::PurgeOutcome +pub devlaunch_core::flows::lifecycle::PurgeOutcome::NoCacheDirectory +pub devlaunch_core::flows::lifecycle::PurgeOutcome::NothingToPurge +pub devlaunch_core::flows::lifecycle::PurgeOutcome::Removed +pub devlaunch_core::flows::lifecycle::PurgeOutcome::Removed::cache_dir: std::path::PathBuf +pub devlaunch_core::flows::lifecycle::PurgeOutcome::RemovedNothing +pub devlaunch_core::flows::lifecycle::PurgeOutcome::RemovedNothing::cache_dir: std::path::PathBuf +pub devlaunch_core::flows::lifecycle::PurgeOutcome::RemovedNothing::refused: devlaunch_core::domain::workspace_state::NonEmpty +pub devlaunch_core::flows::lifecycle::PurgeOutcome::RemovedWhatItCould +pub devlaunch_core::flows::lifecycle::PurgeOutcome::RemovedWhatItCould::cache_dir: std::path::PathBuf +pub devlaunch_core::flows::lifecycle::PurgeOutcome::RemovedWhatItCould::refused: devlaunch_core::domain::workspace_state::NonEmpty +impl devlaunch_core::flows::lifecycle::PurgeOutcome +pub fn devlaunch_core::flows::lifecycle::PurgeOutcome::finished(&self) -> bool +impl core::clone::Clone for devlaunch_core::flows::lifecycle::PurgeOutcome +pub fn devlaunch_core::flows::lifecycle::PurgeOutcome::clone(&self) -> devlaunch_core::flows::lifecycle::PurgeOutcome +impl core::cmp::Eq for devlaunch_core::flows::lifecycle::PurgeOutcome +impl core::cmp::PartialEq for devlaunch_core::flows::lifecycle::PurgeOutcome +pub fn devlaunch_core::flows::lifecycle::PurgeOutcome::eq(&self, &devlaunch_core::flows::lifecycle::PurgeOutcome) -> bool +impl core::fmt::Debug for devlaunch_core::flows::lifecycle::PurgeOutcome +pub fn devlaunch_core::flows::lifecycle::PurgeOutcome::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::lifecycle::PurgeOutcome +pub enum devlaunch_core::flows::lifecycle::PurgeStep +pub devlaunch_core::flows::lifecycle::PurgeStep::Deleting +pub devlaunch_core::flows::lifecycle::PurgeStep::Deleting::workspace_id: alloc::string::String +pub devlaunch_core::flows::lifecycle::PurgeStep::NotDeleted +pub devlaunch_core::flows::lifecycle::PurgeStep::NotDeleted::exit: devlaunch_runner::Exit +pub devlaunch_core::flows::lifecycle::PurgeStep::NotDeleted::stderr: alloc::string::String +pub devlaunch_core::flows::lifecycle::PurgeStep::NotDeleted::workspace_id: alloc::string::String +impl core::clone::Clone for devlaunch_core::flows::lifecycle::PurgeStep +pub fn devlaunch_core::flows::lifecycle::PurgeStep::clone(&self) -> devlaunch_core::flows::lifecycle::PurgeStep +impl core::cmp::Eq for devlaunch_core::flows::lifecycle::PurgeStep +impl core::cmp::PartialEq for devlaunch_core::flows::lifecycle::PurgeStep +pub fn devlaunch_core::flows::lifecycle::PurgeStep::eq(&self, &devlaunch_core::flows::lifecycle::PurgeStep) -> bool +impl core::fmt::Debug for devlaunch_core::flows::lifecycle::PurgeStep +pub fn devlaunch_core::flows::lifecycle::PurgeStep::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::lifecycle::PurgeStep +pub enum devlaunch_core::flows::lifecycle::RefreshReason +pub devlaunch_core::flows::lifecycle::RefreshReason::Forced +pub devlaunch_core::flows::lifecycle::RefreshReason::IfStale +impl core::clone::Clone for devlaunch_core::flows::lifecycle::RefreshReason +pub fn devlaunch_core::flows::lifecycle::RefreshReason::clone(&self) -> devlaunch_core::flows::lifecycle::RefreshReason +impl core::cmp::Eq for devlaunch_core::flows::lifecycle::RefreshReason +impl core::cmp::PartialEq for devlaunch_core::flows::lifecycle::RefreshReason +pub fn devlaunch_core::flows::lifecycle::RefreshReason::eq(&self, &devlaunch_core::flows::lifecycle::RefreshReason) -> bool +impl core::fmt::Debug for devlaunch_core::flows::lifecycle::RefreshReason +pub fn devlaunch_core::flows::lifecycle::RefreshReason::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::Copy for devlaunch_core::flows::lifecycle::RefreshReason +impl core::marker::StructuralPartialEq for devlaunch_core::flows::lifecycle::RefreshReason +pub enum devlaunch_core::flows::lifecycle::RefreshSpawn +pub devlaunch_core::flows::lifecycle::RefreshSpawn::AlreadySpawned +pub devlaunch_core::flows::lifecycle::RefreshSpawn::CacheStillFresh +pub devlaunch_core::flows::lifecycle::RefreshSpawn::NotStarted(devlaunch_core::flows::lifecycle::SpawnRefused) +pub devlaunch_core::flows::lifecycle::RefreshSpawn::Spawned +pub devlaunch_core::flows::lifecycle::RefreshSpawn::Spawned::pid: u32 +impl core::clone::Clone for devlaunch_core::flows::lifecycle::RefreshSpawn +pub fn devlaunch_core::flows::lifecycle::RefreshSpawn::clone(&self) -> devlaunch_core::flows::lifecycle::RefreshSpawn +impl core::cmp::Eq for devlaunch_core::flows::lifecycle::RefreshSpawn +impl core::cmp::PartialEq for devlaunch_core::flows::lifecycle::RefreshSpawn +pub fn devlaunch_core::flows::lifecycle::RefreshSpawn::eq(&self, &devlaunch_core::flows::lifecycle::RefreshSpawn) -> bool +impl core::fmt::Debug for devlaunch_core::flows::lifecycle::RefreshSpawn +pub fn devlaunch_core::flows::lifecycle::RefreshSpawn::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::Copy for devlaunch_core::flows::lifecycle::RefreshSpawn +impl core::marker::StructuralPartialEq for devlaunch_core::flows::lifecycle::RefreshSpawn +pub enum devlaunch_core::flows::lifecycle::RemovalRefused +pub devlaunch_core::flows::lifecycle::RemovalRefused::CouldNotTell +pub devlaunch_core::flows::lifecycle::RemovalRefused::CouldNotTell::cause: devlaunch_core::domain::workspace_state::CouldNotTell +pub devlaunch_core::flows::lifecycle::RemovalRefused::CouldNotTell::workspace_id: alloc::string::String +pub devlaunch_core::flows::lifecycle::RemovalRefused::WouldLose +pub devlaunch_core::flows::lifecycle::RemovalRefused::WouldLose::losses: devlaunch_core::domain::workspace_state::NonEmpty +pub devlaunch_core::flows::lifecycle::RemovalRefused::WouldLose::workspace_id: alloc::string::String +impl core::clone::Clone for devlaunch_core::flows::lifecycle::RemovalRefused +pub fn devlaunch_core::flows::lifecycle::RemovalRefused::clone(&self) -> devlaunch_core::flows::lifecycle::RemovalRefused +impl core::cmp::Eq for devlaunch_core::flows::lifecycle::RemovalRefused +impl core::cmp::PartialEq for devlaunch_core::flows::lifecycle::RemovalRefused +pub fn devlaunch_core::flows::lifecycle::RemovalRefused::eq(&self, &devlaunch_core::flows::lifecycle::RemovalRefused) -> bool +impl core::fmt::Debug for devlaunch_core::flows::lifecycle::RemovalRefused +pub fn devlaunch_core::flows::lifecycle::RemovalRefused::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::lifecycle::RemovalRefused +pub enum devlaunch_core::flows::lifecycle::RepointFailure +pub devlaunch_core::flows::lifecycle::RepointFailure::NotADevpodRecord +pub devlaunch_core::flows::lifecycle::RepointFailure::NotADevpodRecord::path: std::path::PathBuf +pub devlaunch_core::flows::lifecycle::RepointFailure::NotJson +pub devlaunch_core::flows::lifecycle::RepointFailure::NotJson::path: std::path::PathBuf +pub devlaunch_core::flows::lifecycle::RepointFailure::NotJson::reason: alloc::string::String +pub devlaunch_core::flows::lifecycle::RepointFailure::Unreadable +pub devlaunch_core::flows::lifecycle::RepointFailure::Unreadable::path: std::path::PathBuf +pub devlaunch_core::flows::lifecycle::RepointFailure::Unreadable::reason: alloc::string::String +pub devlaunch_core::flows::lifecycle::RepointFailure::Unwritable +pub devlaunch_core::flows::lifecycle::RepointFailure::Unwritable::path: std::path::PathBuf +pub devlaunch_core::flows::lifecycle::RepointFailure::Unwritable::reason: alloc::string::String +impl core::clone::Clone for devlaunch_core::flows::lifecycle::RepointFailure +pub fn devlaunch_core::flows::lifecycle::RepointFailure::clone(&self) -> devlaunch_core::flows::lifecycle::RepointFailure +impl core::cmp::Eq for devlaunch_core::flows::lifecycle::RepointFailure +impl core::cmp::PartialEq for devlaunch_core::flows::lifecycle::RepointFailure +pub fn devlaunch_core::flows::lifecycle::RepointFailure::eq(&self, &devlaunch_core::flows::lifecycle::RepointFailure) -> bool +impl core::fmt::Debug for devlaunch_core::flows::lifecycle::RepointFailure +pub fn devlaunch_core::flows::lifecycle::RepointFailure::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::lifecycle::RepointFailure +pub enum devlaunch_core::flows::lifecycle::SpawnRefused +pub devlaunch_core::flows::lifecycle::SpawnRefused::Blocked(devlaunch_runner::OsFailure) +pub devlaunch_core::flows::lifecycle::SpawnRefused::ProgramNotFound +impl core::clone::Clone for devlaunch_core::flows::lifecycle::SpawnRefused +pub fn devlaunch_core::flows::lifecycle::SpawnRefused::clone(&self) -> devlaunch_core::flows::lifecycle::SpawnRefused +impl core::cmp::Eq for devlaunch_core::flows::lifecycle::SpawnRefused +impl core::cmp::PartialEq for devlaunch_core::flows::lifecycle::SpawnRefused +pub fn devlaunch_core::flows::lifecycle::SpawnRefused::eq(&self, &devlaunch_core::flows::lifecycle::SpawnRefused) -> bool +impl core::fmt::Debug for devlaunch_core::flows::lifecycle::SpawnRefused +pub fn devlaunch_core::flows::lifecycle::SpawnRefused::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::Copy for devlaunch_core::flows::lifecycle::SpawnRefused +impl core::marker::StructuralPartialEq for devlaunch_core::flows::lifecycle::SpawnRefused +pub enum devlaunch_core::flows::lifecycle::StopOutcome +pub devlaunch_core::flows::lifecycle::StopOutcome::DevpodRefused +pub devlaunch_core::flows::lifecycle::StopOutcome::DevpodRefused::exit: devlaunch_runner::Exit +pub devlaunch_core::flows::lifecycle::StopOutcome::Stopped +impl core::clone::Clone for devlaunch_core::flows::lifecycle::StopOutcome +pub fn devlaunch_core::flows::lifecycle::StopOutcome::clone(&self) -> devlaunch_core::flows::lifecycle::StopOutcome +impl core::cmp::Eq for devlaunch_core::flows::lifecycle::StopOutcome +impl core::cmp::PartialEq for devlaunch_core::flows::lifecycle::StopOutcome +pub fn devlaunch_core::flows::lifecycle::StopOutcome::eq(&self, &devlaunch_core::flows::lifecycle::StopOutcome) -> bool +impl core::fmt::Debug for devlaunch_core::flows::lifecycle::StopOutcome +pub fn devlaunch_core::flows::lifecycle::StopOutcome::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::Copy for devlaunch_core::flows::lifecycle::StopOutcome +impl core::marker::StructuralPartialEq for devlaunch_core::flows::lifecycle::StopOutcome +pub struct devlaunch_core::flows::lifecycle::Adoptable +pub devlaunch_core::flows::lifecycle::Adoptable::clone: std::path::PathBuf +pub devlaunch_core::flows::lifecycle::Adoptable::context: alloc::string::String +pub devlaunch_core::flows::lifecycle::Adoptable::record: devlaunch_core::domain::model::WorktreeInfo +pub devlaunch_core::flows::lifecycle::Adoptable::sourced_at: alloc::string::String +pub devlaunch_core::flows::lifecycle::Adoptable::workspace_id: alloc::string::String +impl core::clone::Clone for devlaunch_core::flows::lifecycle::Adoptable +pub fn devlaunch_core::flows::lifecycle::Adoptable::clone(&self) -> devlaunch_core::flows::lifecycle::Adoptable +impl core::cmp::Eq for devlaunch_core::flows::lifecycle::Adoptable +impl core::cmp::PartialEq for devlaunch_core::flows::lifecycle::Adoptable +pub fn devlaunch_core::flows::lifecycle::Adoptable::eq(&self, &devlaunch_core::flows::lifecycle::Adoptable) -> bool +impl core::fmt::Debug for devlaunch_core::flows::lifecycle::Adoptable +pub fn devlaunch_core::flows::lifecycle::Adoptable::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::lifecycle::Adoptable +pub struct devlaunch_core::flows::lifecycle::CloneDirectories<'a, 'r> +impl<'a, 'r> devlaunch_core::flows::lifecycle::CloneDirectories<'a, 'r> +pub fn devlaunch_core::flows::lifecycle::CloneDirectories<'a, 'r>::of(&'a devlaunch_core::flows::workspace_clone::WorkspaceCloneManager<'r>) -> Self +pub fn devlaunch_core::flows::lifecycle::CloneDirectories<'a, 'r>::take_notices(&self) -> alloc::vec::Vec +impl devlaunch_core::flows::listing::ClonePathResolver for devlaunch_core::flows::lifecycle::CloneDirectories<'_, '_> +pub fn devlaunch_core::flows::lifecycle::CloneDirectories<'_, '_>::clone_path(&self, &devlaunch_core::domain::model::WorktreeInfo) -> core::option::Option +pub struct devlaunch_core::flows::lifecycle::ClonePlacement +impl devlaunch_core::flows::lifecycle::ClonePlacement +pub fn devlaunch_core::flows::lifecycle::ClonePlacement::resolve(&devlaunch_core::flows::workspace_clone::WorkspaceCloneManager<'_>, &[devlaunch_core::clients::devpod::Workspace]) -> Self +pub fn devlaunch_core::flows::lifecycle::ClonePlacement::unlocatable(&self) -> core::option::Option> +impl core::clone::Clone for devlaunch_core::flows::lifecycle::ClonePlacement +pub fn devlaunch_core::flows::lifecycle::ClonePlacement::clone(&self) -> devlaunch_core::flows::lifecycle::ClonePlacement +impl core::cmp::Eq for devlaunch_core::flows::lifecycle::ClonePlacement +impl core::cmp::PartialEq for devlaunch_core::flows::lifecycle::ClonePlacement +pub fn devlaunch_core::flows::lifecycle::ClonePlacement::eq(&self, &devlaunch_core::flows::lifecycle::ClonePlacement) -> bool +impl core::fmt::Debug for devlaunch_core::flows::lifecycle::ClonePlacement +pub fn devlaunch_core::flows::lifecycle::ClonePlacement::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::lifecycle::ClonePlacement +pub struct devlaunch_core::flows::lifecycle::Kept +pub devlaunch_core::flows::lifecycle::Kept::because: devlaunch_core::flows::lifecycle::KeptBecause +pub devlaunch_core::flows::lifecycle::Kept::path: std::path::PathBuf +impl core::clone::Clone for devlaunch_core::flows::lifecycle::Kept +pub fn devlaunch_core::flows::lifecycle::Kept::clone(&self) -> devlaunch_core::flows::lifecycle::Kept +impl core::cmp::Eq for devlaunch_core::flows::lifecycle::Kept +impl core::cmp::PartialEq for devlaunch_core::flows::lifecycle::Kept +pub fn devlaunch_core::flows::lifecycle::Kept::eq(&self, &devlaunch_core::flows::lifecycle::Kept) -> bool +impl core::fmt::Debug for devlaunch_core::flows::lifecycle::Kept +pub fn devlaunch_core::flows::lifecycle::Kept::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::lifecycle::Kept +pub struct devlaunch_core::flows::lifecycle::PrunePlan +impl devlaunch_core::flows::lifecycle::PrunePlan +pub fn devlaunch_core::flows::lifecycle::PrunePlan::freed(&self) -> devlaunch_core::flows::disk_usage::DiskUsage +pub fn devlaunch_core::flows::lifecycle::PrunePlan::keeping(&self) -> &[devlaunch_core::flows::lifecycle::Kept] +pub fn devlaunch_core::flows::lifecycle::PrunePlan::nothing_to_do(&self) -> bool +pub fn devlaunch_core::flows::lifecycle::PrunePlan::removing(&self) -> &[devlaunch_core::flows::lifecycle::Reclaimable] +pub fn devlaunch_core::flows::lifecycle::PrunePlan::root(&self) -> &std::path::Path +pub fn devlaunch_core::flows::lifecycle::PrunePlan::stale_records(&self) -> &[devlaunch_core::domain::model::WorktreeInfo] +impl core::clone::Clone for devlaunch_core::flows::lifecycle::PrunePlan +pub fn devlaunch_core::flows::lifecycle::PrunePlan::clone(&self) -> devlaunch_core::flows::lifecycle::PrunePlan +impl core::cmp::Eq for devlaunch_core::flows::lifecycle::PrunePlan +impl core::cmp::PartialEq for devlaunch_core::flows::lifecycle::PrunePlan +pub fn devlaunch_core::flows::lifecycle::PrunePlan::eq(&self, &devlaunch_core::flows::lifecycle::PrunePlan) -> bool +impl core::fmt::Debug for devlaunch_core::flows::lifecycle::PrunePlan +pub fn devlaunch_core::flows::lifecycle::PrunePlan::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::lifecycle::PrunePlan +pub struct devlaunch_core::flows::lifecycle::PruneReport +pub devlaunch_core::flows::lifecycle::PruneReport::refused: alloc::vec::Vec +pub devlaunch_core::flows::lifecycle::PruneReport::removed: alloc::vec::Vec +pub devlaunch_core::flows::lifecycle::PruneReport::withheld: alloc::vec::Vec +impl devlaunch_core::flows::lifecycle::PruneReport +pub fn devlaunch_core::flows::lifecycle::PruneReport::finished(&self) -> bool +pub fn devlaunch_core::flows::lifecycle::PruneReport::freed(&self) -> devlaunch_core::flows::disk_usage::DiskUsage +impl core::clone::Clone for devlaunch_core::flows::lifecycle::PruneReport +pub fn devlaunch_core::flows::lifecycle::PruneReport::clone(&self) -> devlaunch_core::flows::lifecycle::PruneReport +impl core::cmp::Eq for devlaunch_core::flows::lifecycle::PruneReport +impl core::cmp::PartialEq for devlaunch_core::flows::lifecycle::PruneReport +pub fn devlaunch_core::flows::lifecycle::PruneReport::eq(&self, &devlaunch_core::flows::lifecycle::PruneReport) -> bool +impl core::fmt::Debug for devlaunch_core::flows::lifecycle::PruneReport +pub fn devlaunch_core::flows::lifecycle::PruneReport::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::lifecycle::PruneReport +pub struct devlaunch_core::flows::lifecycle::PurgePlan +impl devlaunch_core::flows::lifecycle::PurgePlan +pub fn devlaunch_core::flows::lifecycle::PurgePlan::cache_dir(&self) -> &std::path::Path +pub fn devlaunch_core::flows::lifecycle::PurgePlan::ownership(&self) -> &devlaunch_core::flows::listing::WorkspaceOwnership +impl core::clone::Clone for devlaunch_core::flows::lifecycle::PurgePlan +pub fn devlaunch_core::flows::lifecycle::PurgePlan::clone(&self) -> devlaunch_core::flows::lifecycle::PurgePlan +impl core::cmp::Eq for devlaunch_core::flows::lifecycle::PurgePlan +impl core::cmp::PartialEq for devlaunch_core::flows::lifecycle::PurgePlan +pub fn devlaunch_core::flows::lifecycle::PurgePlan::eq(&self, &devlaunch_core::flows::lifecycle::PurgePlan) -> bool +impl core::fmt::Debug for devlaunch_core::flows::lifecycle::PurgePlan +pub fn devlaunch_core::flows::lifecycle::PurgePlan::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::lifecycle::PurgePlan +pub struct devlaunch_core::flows::lifecycle::Reclaimable +pub devlaunch_core::flows::lifecycle::Reclaimable::owner: alloc::string::String +pub devlaunch_core::flows::lifecycle::Reclaimable::path: std::path::PathBuf +pub devlaunch_core::flows::lifecycle::Reclaimable::promotion: devlaunch_core::flows::lifecycle::Promotion +pub devlaunch_core::flows::lifecycle::Reclaimable::repo: alloc::string::String +pub devlaunch_core::flows::lifecycle::Reclaimable::usage: devlaunch_core::flows::disk_usage::DiskUsage +impl core::clone::Clone for devlaunch_core::flows::lifecycle::Reclaimable +pub fn devlaunch_core::flows::lifecycle::Reclaimable::clone(&self) -> devlaunch_core::flows::lifecycle::Reclaimable +impl core::cmp::Eq for devlaunch_core::flows::lifecycle::Reclaimable +impl core::cmp::PartialEq for devlaunch_core::flows::lifecycle::Reclaimable +pub fn devlaunch_core::flows::lifecycle::Reclaimable::eq(&self, &devlaunch_core::flows::lifecycle::Reclaimable) -> bool +impl core::fmt::Debug for devlaunch_core::flows::lifecycle::Reclaimable +pub fn devlaunch_core::flows::lifecycle::Reclaimable::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::lifecycle::Reclaimable +pub struct devlaunch_core::flows::lifecycle::ReconcilePlan +impl devlaunch_core::flows::lifecycle::ReconcilePlan +pub fn devlaunch_core::flows::lifecycle::ReconcilePlan::adopting(&self) -> &[devlaunch_core::flows::lifecycle::Adoptable] +pub fn devlaunch_core::flows::lifecycle::ReconcilePlan::nothing_to_do(&self) -> bool +pub fn devlaunch_core::flows::lifecycle::ReconcilePlan::reporting(&self) -> &[devlaunch_core::flows::lifecycle::Unadoptable] +pub fn devlaunch_core::flows::lifecycle::ReconcilePlan::root(&self) -> &std::path::Path +impl core::clone::Clone for devlaunch_core::flows::lifecycle::ReconcilePlan +pub fn devlaunch_core::flows::lifecycle::ReconcilePlan::clone(&self) -> devlaunch_core::flows::lifecycle::ReconcilePlan +impl core::cmp::Eq for devlaunch_core::flows::lifecycle::ReconcilePlan +impl core::cmp::PartialEq for devlaunch_core::flows::lifecycle::ReconcilePlan +pub fn devlaunch_core::flows::lifecycle::ReconcilePlan::eq(&self, &devlaunch_core::flows::lifecycle::ReconcilePlan) -> bool +impl core::fmt::Debug for devlaunch_core::flows::lifecycle::ReconcilePlan +pub fn devlaunch_core::flows::lifecycle::ReconcilePlan::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::lifecycle::ReconcilePlan +pub struct devlaunch_core::flows::lifecycle::ReconcileReport +impl devlaunch_core::flows::lifecycle::ReconcileReport +pub fn devlaunch_core::flows::lifecycle::ReconcileReport::adoptions(&self) -> &[devlaunch_core::flows::lifecycle::Adoption] +pub fn devlaunch_core::flows::lifecycle::ReconcileReport::finished(&self) -> bool +impl core::clone::Clone for devlaunch_core::flows::lifecycle::ReconcileReport +pub fn devlaunch_core::flows::lifecycle::ReconcileReport::clone(&self) -> devlaunch_core::flows::lifecycle::ReconcileReport +impl core::cmp::Eq for devlaunch_core::flows::lifecycle::ReconcileReport +impl core::cmp::PartialEq for devlaunch_core::flows::lifecycle::ReconcileReport +pub fn devlaunch_core::flows::lifecycle::ReconcileReport::eq(&self, &devlaunch_core::flows::lifecycle::ReconcileReport) -> bool +impl core::fmt::Debug for devlaunch_core::flows::lifecycle::ReconcileReport +pub fn devlaunch_core::flows::lifecycle::ReconcileReport::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::lifecycle::ReconcileReport +pub struct devlaunch_core::flows::lifecycle::Refresh<'a> +impl<'a> devlaunch_core::flows::lifecycle::Refresh<'a> +pub fn devlaunch_core::flows::lifecycle::Refresh<'a>::ask(&mut self, &dyn devlaunch_runner::Runner, devlaunch_core::flows::lifecycle::RefreshReason) -> devlaunch_core::flows::lifecycle::RefreshSpawn +pub fn devlaunch_core::flows::lifecycle::Refresh<'a>::new(&'a devlaunch_core::flows::lifecycle::SelfInvocation, &'a std::path::Path) -> Self +pub fn devlaunch_core::flows::lifecycle::Refresh<'a>::spawned(&self) -> bool +pub struct devlaunch_core::flows::lifecycle::SelfInvocation +impl devlaunch_core::flows::lifecycle::SelfInvocation +pub fn devlaunch_core::flows::lifecycle::SelfInvocation::new(impl core::convert::Into) -> Self +impl core::clone::Clone for devlaunch_core::flows::lifecycle::SelfInvocation +pub fn devlaunch_core::flows::lifecycle::SelfInvocation::clone(&self) -> devlaunch_core::flows::lifecycle::SelfInvocation +impl core::cmp::Eq for devlaunch_core::flows::lifecycle::SelfInvocation +impl core::cmp::PartialEq for devlaunch_core::flows::lifecycle::SelfInvocation +pub fn devlaunch_core::flows::lifecycle::SelfInvocation::eq(&self, &devlaunch_core::flows::lifecycle::SelfInvocation) -> bool +impl core::fmt::Debug for devlaunch_core::flows::lifecycle::SelfInvocation +pub fn devlaunch_core::flows::lifecycle::SelfInvocation::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::lifecycle::SelfInvocation +pub struct devlaunch_core::flows::lifecycle::SweepReport +pub devlaunch_core::flows::lifecycle::SweepReport::notices: alloc::vec::Vec +impl core::default::Default for devlaunch_core::flows::lifecycle::SweepReport +pub fn devlaunch_core::flows::lifecycle::SweepReport::default() -> devlaunch_core::flows::lifecycle::SweepReport +impl core::fmt::Debug for devlaunch_core::flows::lifecycle::SweepReport +pub fn devlaunch_core::flows::lifecycle::SweepReport::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +pub struct devlaunch_core::flows::lifecycle::Unadoptable +pub devlaunch_core::flows::lifecycle::Unadoptable::because: devlaunch_core::flows::lifecycle::NotAdopted +pub devlaunch_core::flows::lifecycle::Unadoptable::sourced_at: alloc::string::String +pub devlaunch_core::flows::lifecycle::Unadoptable::workspace_id: alloc::string::String +impl core::clone::Clone for devlaunch_core::flows::lifecycle::Unadoptable +pub fn devlaunch_core::flows::lifecycle::Unadoptable::clone(&self) -> devlaunch_core::flows::lifecycle::Unadoptable +impl core::cmp::Eq for devlaunch_core::flows::lifecycle::Unadoptable +impl core::cmp::PartialEq for devlaunch_core::flows::lifecycle::Unadoptable +pub fn devlaunch_core::flows::lifecycle::Unadoptable::eq(&self, &devlaunch_core::flows::lifecycle::Unadoptable) -> bool +impl core::fmt::Debug for devlaunch_core::flows::lifecycle::Unadoptable +pub fn devlaunch_core::flows::lifecycle::Unadoptable::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::lifecycle::Unadoptable +pub struct devlaunch_core::flows::lifecycle::Unlocatable +pub devlaunch_core::flows::lifecycle::Unlocatable::detail: alloc::string::String +pub devlaunch_core::flows::lifecycle::Unlocatable::workspace_id: alloc::string::String +impl core::clone::Clone for devlaunch_core::flows::lifecycle::Unlocatable +pub fn devlaunch_core::flows::lifecycle::Unlocatable::clone(&self) -> devlaunch_core::flows::lifecycle::Unlocatable +impl core::cmp::Eq for devlaunch_core::flows::lifecycle::Unlocatable +impl core::cmp::PartialEq for devlaunch_core::flows::lifecycle::Unlocatable +pub fn devlaunch_core::flows::lifecycle::Unlocatable::eq(&self, &devlaunch_core::flows::lifecycle::Unlocatable) -> bool +impl core::fmt::Debug for devlaunch_core::flows::lifecycle::Unlocatable +pub fn devlaunch_core::flows::lifecycle::Unlocatable::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::lifecycle::Unlocatable +pub struct devlaunch_core::flows::lifecycle::Withheld +pub devlaunch_core::flows::lifecycle::Withheld::because: devlaunch_core::flows::lifecycle::KeptBecause +pub devlaunch_core::flows::lifecycle::Withheld::path: std::path::PathBuf +impl core::clone::Clone for devlaunch_core::flows::lifecycle::Withheld +pub fn devlaunch_core::flows::lifecycle::Withheld::clone(&self) -> devlaunch_core::flows::lifecycle::Withheld +impl core::cmp::Eq for devlaunch_core::flows::lifecycle::Withheld +impl core::cmp::PartialEq for devlaunch_core::flows::lifecycle::Withheld +pub fn devlaunch_core::flows::lifecycle::Withheld::eq(&self, &devlaunch_core::flows::lifecycle::Withheld) -> bool +impl core::fmt::Debug for devlaunch_core::flows::lifecycle::Withheld +pub fn devlaunch_core::flows::lifecycle::Withheld::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::lifecycle::Withheld +pub fn devlaunch_core::flows::lifecycle::apply_reconciliation(&mut devlaunch_core::flows::listing::CommandContext<'_>, &mut devlaunch_core::flows::lifecycle::Refresh<'_>, &mut devlaunch_core::domain::metadata::MetadataStorage, &std::path::Path, &devlaunch_core::flows::lifecycle::ReconcilePlan, &mut dyn devlaunch_core::notices::Notices) -> devlaunch_core::flows::lifecycle::ReconcileReport +pub fn devlaunch_core::flows::lifecycle::child_work(&std::path::Path, devlaunch_core::flows::lifecycle::RefreshReason) -> devlaunch_core::flows::lifecycle::ChildWork +pub fn devlaunch_core::flows::lifecycle::devpod_home() -> core::option::Option +pub fn devlaunch_core::flows::lifecycle::guard_removal(&str, devlaunch_core::domain::workspace_state::Unsaved, devlaunch_core::flows::lifecycle::Insistence) -> devlaunch_core::flows::lifecycle::Guarded +pub fn devlaunch_core::flows::lifecycle::objection(&devlaunch_core::domain::workspace_state::Unsaved) -> core::option::Option +pub fn devlaunch_core::flows::lifecycle::prune_clones(&mut devlaunch_core::flows::listing::CommandContext<'_>, &devlaunch_core::flows::workspace_clone::WorkspaceCloneManager<'_>, &mut devlaunch_core::domain::metadata::MetadataStorage, &devlaunch_core::flows::lifecycle::PrunePlan, &mut dyn devlaunch_core::notices::Notices) -> core::result::Result +pub fn devlaunch_core::flows::lifecycle::prune_plan(&devlaunch_core::flows::workspace_clone::WorkspaceCloneManager<'_>, &devlaunch_core::domain::metadata::MetadataStorage, &[devlaunch_core::clients::devpod::Workspace], &devlaunch_core::flows::lifecycle::ClonePlacement, devlaunch_core::flows::lifecycle::Insistence, &mut dyn devlaunch_core::notices::Notices) -> core::result::Result +pub fn devlaunch_core::flows::lifecycle::purge_all_data(&mut devlaunch_core::flows::listing::CommandContext<'_>, &devlaunch_core::flows::lifecycle::PurgePlan, &mut dyn core::ops::function::FnMut(devlaunch_core::flows::lifecycle::PurgeStep)) -> core::result::Result +pub fn devlaunch_core::flows::lifecycle::purge_plan(&mut devlaunch_core::flows::listing::CommandContext<'_>, &std::path::Path) -> core::result::Result +pub fn devlaunch_core::flows::lifecycle::reconcile_plan(&devlaunch_core::flows::workspace_clone::WorkspaceCloneManager<'_>, &devlaunch_core::domain::metadata::MetadataStorage, &[devlaunch_core::clients::devpod::Workspace], &devlaunch_core::flows::lifecycle::ClonePlacement, &mut dyn devlaunch_core::notices::Notices) -> devlaunch_core::flows::lifecycle::ReconcilePlan +pub fn devlaunch_core::flows::lifecycle::sweep_repo_fetches(&devlaunch_core::flows::repo_manager::RepositoryManager<'_>, &mut devlaunch_core::domain::metadata::MetadataStorage) -> devlaunch_core::flows::lifecycle::SweepReport +pub fn devlaunch_core::flows::lifecycle::unsaved_work_in(&devlaunch_core::flows::workspace_clone::WorkspaceCloneManager<'_>, &devlaunch_core::domain::metadata::MetadataStorage, &devlaunch_core::clients::git::Git<'_>, &std::path::Path, &str, &mut dyn devlaunch_core::notices::Notices) -> devlaunch_core::domain::workspace_state::Unsaved +pub fn devlaunch_core::flows::lifecycle::workspace_delete(&mut devlaunch_core::flows::listing::CommandContext<'_>, &mut devlaunch_core::flows::lifecycle::Refresh<'_>, &devlaunch_core::flows::workspace_clone::WorkspaceCloneManager<'_>, &mut devlaunch_core::domain::metadata::MetadataStorage, &str, devlaunch_core::flows::lifecycle::Insistence, &mut dyn devlaunch_core::notices::Notices) -> core::result::Result +pub fn devlaunch_core::flows::lifecycle::workspace_state(&dyn devlaunch_runner::Runner, &str) -> core::result::Result +pub fn devlaunch_core::flows::lifecycle::workspace_stop(&mut devlaunch_core::flows::listing::CommandContext<'_>, &mut devlaunch_core::flows::lifecycle::Refresh<'_>, &str) -> core::result::Result +pub mod devlaunch_core::flows::listing +pub enum devlaunch_core::flows::listing::LastUsed +pub devlaunch_core::flows::listing::LastUsed::At(alloc::string::String) +pub devlaunch_core::flows::listing::LastUsed::Never +impl core::clone::Clone for devlaunch_core::flows::listing::LastUsed +pub fn devlaunch_core::flows::listing::LastUsed::clone(&self) -> devlaunch_core::flows::listing::LastUsed +impl core::cmp::Eq for devlaunch_core::flows::listing::LastUsed +impl core::cmp::PartialEq for devlaunch_core::flows::listing::LastUsed +pub fn devlaunch_core::flows::listing::LastUsed::eq(&self, &devlaunch_core::flows::listing::LastUsed) -> bool +impl core::fmt::Debug for devlaunch_core::flows::listing::LastUsed +pub fn devlaunch_core::flows::listing::LastUsed::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::listing::LastUsed +pub enum devlaunch_core::flows::listing::SizeCell +pub devlaunch_core::flows::listing::SizeCell::Measured(devlaunch_core::flows::disk_usage::DiskUsage) +pub devlaunch_core::flows::listing::SizeCell::NoColumn +pub devlaunch_core::flows::listing::SizeCell::NotOurs +impl core::clone::Clone for devlaunch_core::flows::listing::SizeCell +pub fn devlaunch_core::flows::listing::SizeCell::clone(&self) -> devlaunch_core::flows::listing::SizeCell +impl core::cmp::Eq for devlaunch_core::flows::listing::SizeCell +impl core::cmp::PartialEq for devlaunch_core::flows::listing::SizeCell +pub fn devlaunch_core::flows::listing::SizeCell::eq(&self, &devlaunch_core::flows::listing::SizeCell) -> bool +impl core::fmt::Debug for devlaunch_core::flows::listing::SizeCell +pub fn devlaunch_core::flows::listing::SizeCell::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::listing::SizeCell +pub enum devlaunch_core::flows::listing::Sizes +pub devlaunch_core::flows::listing::Sizes::Measure +pub devlaunch_core::flows::listing::Sizes::Skip +impl core::clone::Clone for devlaunch_core::flows::listing::Sizes +pub fn devlaunch_core::flows::listing::Sizes::clone(&self) -> devlaunch_core::flows::listing::Sizes +impl core::cmp::Eq for devlaunch_core::flows::listing::Sizes +impl core::cmp::PartialEq for devlaunch_core::flows::listing::Sizes +pub fn devlaunch_core::flows::listing::Sizes::eq(&self, &devlaunch_core::flows::listing::Sizes) -> bool +impl core::fmt::Debug for devlaunch_core::flows::listing::Sizes +pub fn devlaunch_core::flows::listing::Sizes::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::Copy for devlaunch_core::flows::listing::Sizes +impl core::marker::StructuralPartialEq for devlaunch_core::flows::listing::Sizes +pub enum devlaunch_core::flows::listing::SourceKind +pub devlaunch_core::flows::listing::SourceKind::Git +pub devlaunch_core::flows::listing::SourceKind::Local +pub devlaunch_core::flows::listing::SourceKind::Unknown +impl devlaunch_core::flows::listing::SourceKind +pub fn devlaunch_core::flows::listing::SourceKind::word(self) -> &'static str +impl core::clone::Clone for devlaunch_core::flows::listing::SourceKind +pub fn devlaunch_core::flows::listing::SourceKind::clone(&self) -> devlaunch_core::flows::listing::SourceKind +impl core::cmp::Eq for devlaunch_core::flows::listing::SourceKind +impl core::cmp::PartialEq for devlaunch_core::flows::listing::SourceKind +pub fn devlaunch_core::flows::listing::SourceKind::eq(&self, &devlaunch_core::flows::listing::SourceKind) -> bool +impl core::fmt::Debug for devlaunch_core::flows::listing::SourceKind +pub fn devlaunch_core::flows::listing::SourceKind::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::Copy for devlaunch_core::flows::listing::SourceKind +impl core::marker::StructuralPartialEq for devlaunch_core::flows::listing::SourceKind +pub enum devlaunch_core::flows::listing::WorkspaceTable +pub devlaunch_core::flows::listing::WorkspaceTable::Nothing +pub devlaunch_core::flows::listing::WorkspaceTable::Rows(devlaunch_core::domain::workspace_state::NonEmpty) +impl core::clone::Clone for devlaunch_core::flows::listing::WorkspaceTable +pub fn devlaunch_core::flows::listing::WorkspaceTable::clone(&self) -> devlaunch_core::flows::listing::WorkspaceTable +impl core::cmp::Eq for devlaunch_core::flows::listing::WorkspaceTable +impl core::cmp::PartialEq for devlaunch_core::flows::listing::WorkspaceTable +pub fn devlaunch_core::flows::listing::WorkspaceTable::eq(&self, &devlaunch_core::flows::listing::WorkspaceTable) -> bool +impl core::fmt::Debug for devlaunch_core::flows::listing::WorkspaceTable +pub fn devlaunch_core::flows::listing::WorkspaceTable::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::listing::WorkspaceTable +pub struct devlaunch_core::flows::listing::CommandContext<'r> +impl<'r> devlaunch_core::flows::listing::CommandContext<'r> +pub fn devlaunch_core::flows::listing::CommandContext<'r>::git(&self) -> devlaunch_core::clients::git::Git<'r> +pub fn devlaunch_core::flows::listing::CommandContext<'r>::new(&'r dyn devlaunch_runner::Runner) -> Self +pub fn devlaunch_core::flows::listing::CommandContext<'r>::workspaces(&mut self) -> core::result::Result, devlaunch_core::clients::devpod::ListingUnreadable> +pub struct devlaunch_core::flows::listing::DlView<'a> +pub devlaunch_core::flows::listing::DlView::cache_dir: &'a std::path::Path +pub devlaunch_core::flows::listing::DlView::clones: &'a dyn devlaunch_core::flows::listing::ClonePathResolver +pub devlaunch_core::flows::listing::DlView::storage: &'a devlaunch_core::domain::metadata::MetadataStorage +pub struct devlaunch_core::flows::listing::ListedWorkspace +impl core::clone::Clone for devlaunch_core::flows::listing::ListedWorkspace +pub fn devlaunch_core::flows::listing::ListedWorkspace::clone(&self) -> devlaunch_core::flows::listing::ListedWorkspace +impl core::cmp::Eq for devlaunch_core::flows::listing::ListedWorkspace +impl core::cmp::PartialEq for devlaunch_core::flows::listing::ListedWorkspace +pub fn devlaunch_core::flows::listing::ListedWorkspace::eq(&self, &devlaunch_core::flows::listing::ListedWorkspace) -> bool +impl core::fmt::Debug for devlaunch_core::flows::listing::ListedWorkspace +pub fn devlaunch_core::flows::listing::ListedWorkspace::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::listing::ListedWorkspace +pub struct devlaunch_core::flows::listing::RepoDiscovery +pub devlaunch_core::flows::listing::RepoDiscovery::repos: indexmap::map::IndexMap> +pub devlaunch_core::flows::listing::RepoDiscovery::unreadable: alloc::vec::Vec +impl core::clone::Clone for devlaunch_core::flows::listing::RepoDiscovery +pub fn devlaunch_core::flows::listing::RepoDiscovery::clone(&self) -> devlaunch_core::flows::listing::RepoDiscovery +impl core::cmp::Eq for devlaunch_core::flows::listing::RepoDiscovery +impl core::cmp::PartialEq for devlaunch_core::flows::listing::RepoDiscovery +pub fn devlaunch_core::flows::listing::RepoDiscovery::eq(&self, &devlaunch_core::flows::listing::RepoDiscovery) -> bool +impl core::default::Default for devlaunch_core::flows::listing::RepoDiscovery +pub fn devlaunch_core::flows::listing::RepoDiscovery::default() -> devlaunch_core::flows::listing::RepoDiscovery +impl core::fmt::Debug for devlaunch_core::flows::listing::RepoDiscovery +pub fn devlaunch_core::flows::listing::RepoDiscovery::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::listing::RepoDiscovery +pub struct devlaunch_core::flows::listing::SourceDescription +pub devlaunch_core::flows::listing::SourceDescription::detail: alloc::string::String +pub devlaunch_core::flows::listing::SourceDescription::kind: devlaunch_core::flows::listing::SourceKind +impl core::clone::Clone for devlaunch_core::flows::listing::SourceDescription +pub fn devlaunch_core::flows::listing::SourceDescription::clone(&self) -> devlaunch_core::flows::listing::SourceDescription +impl core::cmp::Eq for devlaunch_core::flows::listing::SourceDescription +impl core::cmp::PartialEq for devlaunch_core::flows::listing::SourceDescription +pub fn devlaunch_core::flows::listing::SourceDescription::eq(&self, &devlaunch_core::flows::listing::SourceDescription) -> bool +impl core::fmt::Debug for devlaunch_core::flows::listing::SourceDescription +pub fn devlaunch_core::flows::listing::SourceDescription::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::listing::SourceDescription +pub struct devlaunch_core::flows::listing::TableRow +pub devlaunch_core::flows::listing::TableRow::id: alloc::string::String +pub devlaunch_core::flows::listing::TableRow::last_used: devlaunch_core::flows::listing::LastUsed +pub devlaunch_core::flows::listing::TableRow::size: devlaunch_core::flows::listing::SizeCell +pub devlaunch_core::flows::listing::TableRow::source: devlaunch_core::flows::listing::SourceDescription +impl core::clone::Clone for devlaunch_core::flows::listing::TableRow +pub fn devlaunch_core::flows::listing::TableRow::clone(&self) -> devlaunch_core::flows::listing::TableRow +impl core::cmp::Eq for devlaunch_core::flows::listing::TableRow +impl core::cmp::PartialEq for devlaunch_core::flows::listing::TableRow +pub fn devlaunch_core::flows::listing::TableRow::eq(&self, &devlaunch_core::flows::listing::TableRow) -> bool +impl core::fmt::Debug for devlaunch_core::flows::listing::TableRow +pub fn devlaunch_core::flows::listing::TableRow::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::listing::TableRow +pub struct devlaunch_core::flows::listing::UnreadableSource +pub devlaunch_core::flows::listing::UnreadableSource::payload: alloc::string::String +pub devlaunch_core::flows::listing::UnreadableSource::workspace_id: alloc::string::String +impl core::clone::Clone for devlaunch_core::flows::listing::UnreadableSource +pub fn devlaunch_core::flows::listing::UnreadableSource::clone(&self) -> devlaunch_core::flows::listing::UnreadableSource +impl core::cmp::Eq for devlaunch_core::flows::listing::UnreadableSource +impl core::cmp::PartialEq for devlaunch_core::flows::listing::UnreadableSource +pub fn devlaunch_core::flows::listing::UnreadableSource::eq(&self, &devlaunch_core::flows::listing::UnreadableSource) -> bool +impl core::fmt::Debug for devlaunch_core::flows::listing::UnreadableSource +pub fn devlaunch_core::flows::listing::UnreadableSource::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::listing::UnreadableSource +pub struct devlaunch_core::flows::listing::WorkspaceOwnership +pub devlaunch_core::flows::listing::WorkspaceOwnership::foreign: alloc::vec::Vec +pub devlaunch_core::flows::listing::WorkspaceOwnership::mine: alloc::vec::Vec +impl core::clone::Clone for devlaunch_core::flows::listing::WorkspaceOwnership +pub fn devlaunch_core::flows::listing::WorkspaceOwnership::clone(&self) -> devlaunch_core::flows::listing::WorkspaceOwnership +impl core::cmp::Eq for devlaunch_core::flows::listing::WorkspaceOwnership +impl core::cmp::PartialEq for devlaunch_core::flows::listing::WorkspaceOwnership +pub fn devlaunch_core::flows::listing::WorkspaceOwnership::eq(&self, &devlaunch_core::flows::listing::WorkspaceOwnership) -> bool +impl core::default::Default for devlaunch_core::flows::listing::WorkspaceOwnership +pub fn devlaunch_core::flows::listing::WorkspaceOwnership::default() -> devlaunch_core::flows::listing::WorkspaceOwnership +impl core::fmt::Debug for devlaunch_core::flows::listing::WorkspaceOwnership +pub fn devlaunch_core::flows::listing::WorkspaceOwnership::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::listing::WorkspaceOwnership +pub trait devlaunch_core::flows::listing::ClonePathResolver +pub fn devlaunch_core::flows::listing::ClonePathResolver::clone_path(&self, &devlaunch_core::domain::model::WorktreeInfo) -> core::option::Option +impl devlaunch_core::flows::listing::ClonePathResolver for devlaunch_core::flows::lifecycle::CloneDirectories<'_, '_> +pub fn devlaunch_core::flows::lifecycle::CloneDirectories<'_, '_>::clone_path(&self, &devlaunch_core::domain::model::WorktreeInfo) -> core::option::Option +pub fn devlaunch_core::flows::listing::describe_source(&devlaunch_core::clients::devpod::WorkspaceSource) -> devlaunch_core::flows::listing::SourceDescription +pub fn devlaunch_core::flows::listing::discover_repos_from_workspaces(&devlaunch_core::clients::git::Git<'_>, &[devlaunch_core::clients::devpod::Workspace]) -> devlaunch_core::flows::listing::RepoDiscovery +pub fn devlaunch_core::flows::listing::enriched_listing(&mut devlaunch_core::flows::listing::CommandContext<'_>, &devlaunch_core::flows::listing::DlView<'_>, devlaunch_core::flows::listing::Sizes) -> core::result::Result, devlaunch_core::clients::devpod::ListingUnreadable> +pub fn devlaunch_core::flows::listing::flatten_repos(&indexmap::map::IndexMap>) -> alloc::vec::Vec +pub fn devlaunch_core::flows::listing::json_document(&[devlaunch_core::flows::listing::ListedWorkspace]) -> serde_json::value::Value +pub fn devlaunch_core::flows::listing::workspace_table(&mut devlaunch_core::flows::listing::CommandContext<'_>, &std::path::Path, devlaunch_core::flows::listing::Sizes) -> core::result::Result +pub mod devlaunch_core::flows::migration +pub enum devlaunch_core::flows::migration::Listing +pub devlaunch_core::flows::migration::Listing::CouldNotWrite +pub devlaunch_core::flows::migration::Listing::CouldNotWrite::path: std::path::PathBuf +pub devlaunch_core::flows::migration::Listing::CouldNotWrite::reason: alloc::string::String +pub devlaunch_core::flows::migration::Listing::NothingToList +pub devlaunch_core::flows::migration::Listing::Written +pub devlaunch_core::flows::migration::Listing::Written::lines: usize +pub devlaunch_core::flows::migration::Listing::Written::path: std::path::PathBuf +impl core::clone::Clone for devlaunch_core::flows::migration::Listing +pub fn devlaunch_core::flows::migration::Listing::clone(&self) -> devlaunch_core::flows::migration::Listing +impl core::cmp::Eq for devlaunch_core::flows::migration::Listing +impl core::cmp::PartialEq for devlaunch_core::flows::migration::Listing +pub fn devlaunch_core::flows::migration::Listing::eq(&self, &devlaunch_core::flows::migration::Listing) -> bool +impl core::default::Default for devlaunch_core::flows::migration::Listing +pub fn devlaunch_core::flows::migration::Listing::default() -> devlaunch_core::flows::migration::Listing +impl core::fmt::Debug for devlaunch_core::flows::migration::Listing +pub fn devlaunch_core::flows::migration::Listing::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::migration::Listing +pub struct devlaunch_core::flows::migration::Blocked +pub devlaunch_core::flows::migration::Blocked::from: std::path::PathBuf +pub devlaunch_core::flows::migration::Blocked::to: std::path::PathBuf +impl core::clone::Clone for devlaunch_core::flows::migration::Blocked +pub fn devlaunch_core::flows::migration::Blocked::clone(&self) -> devlaunch_core::flows::migration::Blocked +impl core::cmp::Eq for devlaunch_core::flows::migration::Blocked +impl core::cmp::PartialEq for devlaunch_core::flows::migration::Blocked +pub fn devlaunch_core::flows::migration::Blocked::eq(&self, &devlaunch_core::flows::migration::Blocked) -> bool +impl core::fmt::Debug for devlaunch_core::flows::migration::Blocked +pub fn devlaunch_core::flows::migration::Blocked::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::migration::Blocked +pub struct devlaunch_core::flows::migration::MigrationReport +pub devlaunch_core::flows::migration::MigrationReport::blocked: alloc::vec::Vec +pub devlaunch_core::flows::migration::MigrationReport::failed: alloc::vec::Vec +pub devlaunch_core::flows::migration::MigrationReport::missing: alloc::vec::Vec +pub devlaunch_core::flows::migration::MigrationReport::not_scanned: alloc::vec::Vec +pub devlaunch_core::flows::migration::MigrationReport::orphan_listing: devlaunch_core::flows::migration::Listing +pub devlaunch_core::flows::migration::MigrationReport::orphaned_ids: alloc::vec::Vec +pub devlaunch_core::flows::migration::MigrationReport::renamed: alloc::vec::Vec +pub devlaunch_core::flows::migration::MigrationReport::unmigrated: alloc::vec::Vec +pub devlaunch_core::flows::migration::MigrationReport::unmigrated_listing: devlaunch_core::flows::migration::Listing +pub devlaunch_core::flows::migration::MigrationReport::unusable: alloc::vec::Vec +impl core::clone::Clone for devlaunch_core::flows::migration::MigrationReport +pub fn devlaunch_core::flows::migration::MigrationReport::clone(&self) -> devlaunch_core::flows::migration::MigrationReport +impl core::cmp::Eq for devlaunch_core::flows::migration::MigrationReport +impl core::cmp::PartialEq for devlaunch_core::flows::migration::MigrationReport +pub fn devlaunch_core::flows::migration::MigrationReport::eq(&self, &devlaunch_core::flows::migration::MigrationReport) -> bool +impl core::default::Default for devlaunch_core::flows::migration::MigrationReport +pub fn devlaunch_core::flows::migration::MigrationReport::default() -> devlaunch_core::flows::migration::MigrationReport +impl core::fmt::Debug for devlaunch_core::flows::migration::MigrationReport +pub fn devlaunch_core::flows::migration::MigrationReport::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::migration::MigrationReport +pub struct devlaunch_core::flows::migration::NotScanned +pub devlaunch_core::flows::migration::NotScanned::path: std::path::PathBuf +pub devlaunch_core::flows::migration::NotScanned::reason: alloc::string::String +impl core::clone::Clone for devlaunch_core::flows::migration::NotScanned +pub fn devlaunch_core::flows::migration::NotScanned::clone(&self) -> devlaunch_core::flows::migration::NotScanned +impl core::cmp::Eq for devlaunch_core::flows::migration::NotScanned +impl core::cmp::PartialEq for devlaunch_core::flows::migration::NotScanned +pub fn devlaunch_core::flows::migration::NotScanned::eq(&self, &devlaunch_core::flows::migration::NotScanned) -> bool +impl core::fmt::Debug for devlaunch_core::flows::migration::NotScanned +pub fn devlaunch_core::flows::migration::NotScanned::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::migration::NotScanned +pub struct devlaunch_core::flows::migration::RenameRefused +pub devlaunch_core::flows::migration::RenameRefused::from: std::path::PathBuf +pub devlaunch_core::flows::migration::RenameRefused::reason: alloc::string::String +pub devlaunch_core::flows::migration::RenameRefused::to: std::path::PathBuf +impl core::clone::Clone for devlaunch_core::flows::migration::RenameRefused +pub fn devlaunch_core::flows::migration::RenameRefused::clone(&self) -> devlaunch_core::flows::migration::RenameRefused +impl core::cmp::Eq for devlaunch_core::flows::migration::RenameRefused +impl core::cmp::PartialEq for devlaunch_core::flows::migration::RenameRefused +pub fn devlaunch_core::flows::migration::RenameRefused::eq(&self, &devlaunch_core::flows::migration::RenameRefused) -> bool +impl core::fmt::Debug for devlaunch_core::flows::migration::RenameRefused +pub fn devlaunch_core::flows::migration::RenameRefused::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::migration::RenameRefused +pub struct devlaunch_core::flows::migration::Renamed +pub devlaunch_core::flows::migration::Renamed::from: std::path::PathBuf +pub devlaunch_core::flows::migration::Renamed::to: std::path::PathBuf +impl core::clone::Clone for devlaunch_core::flows::migration::Renamed +pub fn devlaunch_core::flows::migration::Renamed::clone(&self) -> devlaunch_core::flows::migration::Renamed +impl core::cmp::Eq for devlaunch_core::flows::migration::Renamed +impl core::cmp::PartialEq for devlaunch_core::flows::migration::Renamed +pub fn devlaunch_core::flows::migration::Renamed::eq(&self, &devlaunch_core::flows::migration::Renamed) -> bool +impl core::fmt::Debug for devlaunch_core::flows::migration::Renamed +pub fn devlaunch_core::flows::migration::Renamed::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::migration::Renamed +pub struct devlaunch_core::flows::migration::UnusableRecord +pub devlaunch_core::flows::migration::UnusableRecord::branch: alloc::string::String +pub devlaunch_core::flows::migration::UnusableRecord::path: std::path::PathBuf +impl core::clone::Clone for devlaunch_core::flows::migration::UnusableRecord +pub fn devlaunch_core::flows::migration::UnusableRecord::clone(&self) -> devlaunch_core::flows::migration::UnusableRecord +impl core::cmp::Eq for devlaunch_core::flows::migration::UnusableRecord +impl core::cmp::PartialEq for devlaunch_core::flows::migration::UnusableRecord +pub fn devlaunch_core::flows::migration::UnusableRecord::eq(&self, &devlaunch_core::flows::migration::UnusableRecord) -> bool +impl core::fmt::Debug for devlaunch_core::flows::migration::UnusableRecord +pub fn devlaunch_core::flows::migration::UnusableRecord::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::migration::UnusableRecord +pub fn devlaunch_core::flows::migration::migrate_cache(&mut devlaunch_core::domain::metadata::MetadataStorage, &std::path::Path) -> core::result::Result, devlaunch_core::domain::metadata::MetadataError> +pub mod devlaunch_core::flows::provision +pub enum devlaunch_core::flows::provision::BundleFailed +pub devlaunch_core::flows::provision::BundleFailed::NameTooLong +pub devlaunch_core::flows::provision::BundleFailed::NameTooLong::arcname: alloc::string::String +pub devlaunch_core::flows::provision::BundleFailed::NotWritten +pub devlaunch_core::flows::provision::BundleFailed::NotWritten::error: devlaunch_runner::OsFailure +pub devlaunch_core::flows::provision::BundleFailed::NotWritten::path: std::path::PathBuf +pub devlaunch_core::flows::provision::BundleFailed::Truncated +pub devlaunch_core::flows::provision::BundleFailed::Truncated::path: std::path::PathBuf +pub devlaunch_core::flows::provision::BundleFailed::Unreadable +pub devlaunch_core::flows::provision::BundleFailed::Unreadable::error: devlaunch_runner::OsFailure +pub devlaunch_core::flows::provision::BundleFailed::Unreadable::path: std::path::PathBuf +impl core::clone::Clone for devlaunch_core::flows::provision::BundleFailed +pub fn devlaunch_core::flows::provision::BundleFailed::clone(&self) -> devlaunch_core::flows::provision::BundleFailed +impl core::cmp::Eq for devlaunch_core::flows::provision::BundleFailed +impl core::cmp::PartialEq for devlaunch_core::flows::provision::BundleFailed +pub fn devlaunch_core::flows::provision::BundleFailed::eq(&self, &devlaunch_core::flows::provision::BundleFailed) -> bool +impl core::fmt::Debug for devlaunch_core::flows::provision::BundleFailed +pub fn devlaunch_core::flows::provision::BundleFailed::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::provision::BundleFailed +pub enum devlaunch_core::flows::provision::FailureLevel +pub devlaunch_core::flows::provision::FailureLevel::Info +pub devlaunch_core::flows::provision::FailureLevel::Warning +impl core::clone::Clone for devlaunch_core::flows::provision::FailureLevel +pub fn devlaunch_core::flows::provision::FailureLevel::clone(&self) -> devlaunch_core::flows::provision::FailureLevel +impl core::cmp::Eq for devlaunch_core::flows::provision::FailureLevel +impl core::cmp::PartialEq for devlaunch_core::flows::provision::FailureLevel +pub fn devlaunch_core::flows::provision::FailureLevel::eq(&self, &devlaunch_core::flows::provision::FailureLevel) -> bool +impl core::default::Default for devlaunch_core::flows::provision::FailureLevel +pub fn devlaunch_core::flows::provision::FailureLevel::default() -> devlaunch_core::flows::provision::FailureLevel +impl core::fmt::Debug for devlaunch_core::flows::provision::FailureLevel +pub fn devlaunch_core::flows::provision::FailureLevel::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::Copy for devlaunch_core::flows::provision::FailureLevel +impl core::marker::StructuralPartialEq for devlaunch_core::flows::provision::FailureLevel +pub enum devlaunch_core::flows::provision::ProvisionEvent +pub devlaunch_core::flows::provision::ProvisionEvent::NotInstalled +pub devlaunch_core::flows::provision::ProvisionEvent::NotInstalled::exit: devlaunch_runner::Exit +pub devlaunch_core::flows::provision::ProvisionEvent::NotInstalled::tools: alloc::vec::Vec<&'static str> +pub devlaunch_core::flows::provision::ProvisionEvent::NotInstalled::workspace: alloc::string::String +pub devlaunch_core::flows::provision::ProvisionEvent::PayloadNotBundled +pub devlaunch_core::flows::provision::ProvisionEvent::PayloadNotBundled::failure: devlaunch_core::flows::provision::BundleFailed +pub devlaunch_core::flows::provision::ProvisionEvent::ProvisioningDisabled +pub devlaunch_core::flows::provision::ProvisionEvent::ProvisioningDisabled::workspace: alloc::string::String +pub devlaunch_core::flows::provision::ProvisionEvent::StageFailed +pub devlaunch_core::flows::provision::ProvisionEvent::StageFailed::loudness: devlaunch_core::flows::provision::FailureLevel +pub devlaunch_core::flows::provision::ProvisionEvent::StageFailed::stage: &'static str +pub devlaunch_core::flows::provision::ProvisionEvent::StageFailed::status: i32 +pub devlaunch_core::flows::provision::ProvisionEvent::StageFailed::workspace: alloc::string::String +pub devlaunch_core::flows::provision::ProvisionEvent::StageNotReported +pub devlaunch_core::flows::provision::ProvisionEvent::StageNotReported::loudness: devlaunch_core::flows::provision::FailureLevel +pub devlaunch_core::flows::provision::ProvisionEvent::StageNotReported::stage: &'static str +pub devlaunch_core::flows::provision::ProvisionEvent::StageNotReported::workspace: alloc::string::String +pub devlaunch_core::flows::provision::ProvisionEvent::TripRefused +pub devlaunch_core::flows::provision::ProvisionEvent::TripRefused::refusal: devlaunch_core::clients::devpod::NotRun +pub devlaunch_core::flows::provision::ProvisionEvent::TripRefused::workspace: alloc::string::String +impl core::clone::Clone for devlaunch_core::flows::provision::ProvisionEvent +pub fn devlaunch_core::flows::provision::ProvisionEvent::clone(&self) -> devlaunch_core::flows::provision::ProvisionEvent +impl core::cmp::Eq for devlaunch_core::flows::provision::ProvisionEvent +impl core::cmp::PartialEq for devlaunch_core::flows::provision::ProvisionEvent +pub fn devlaunch_core::flows::provision::ProvisionEvent::eq(&self, &devlaunch_core::flows::provision::ProvisionEvent) -> bool +impl core::fmt::Debug for devlaunch_core::flows::provision::ProvisionEvent +pub fn devlaunch_core::flows::provision::ProvisionEvent::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::provision::ProvisionEvent +pub enum devlaunch_core::flows::provision::Provisioning +pub devlaunch_core::flows::provision::Provisioning::AlreadyProvisioned +pub devlaunch_core::flows::provision::Provisioning::Disabled +pub devlaunch_core::flows::provision::Provisioning::InstallRefused +pub devlaunch_core::flows::provision::Provisioning::InstallRefused::exit: devlaunch_runner::Exit +pub devlaunch_core::flows::provision::Provisioning::Installed +pub devlaunch_core::flows::provision::Provisioning::Lent +pub devlaunch_core::flows::provision::Provisioning::ShimKept +pub devlaunch_core::flows::provision::Provisioning::TripRefused +pub devlaunch_core::flows::provision::Provisioning::TripRefused::refusal: devlaunch_core::clients::devpod::NotRun +impl core::clone::Clone for devlaunch_core::flows::provision::Provisioning +pub fn devlaunch_core::flows::provision::Provisioning::clone(&self) -> devlaunch_core::flows::provision::Provisioning +impl core::cmp::Eq for devlaunch_core::flows::provision::Provisioning +impl core::cmp::PartialEq for devlaunch_core::flows::provision::Provisioning +pub fn devlaunch_core::flows::provision::Provisioning::eq(&self, &devlaunch_core::flows::provision::Provisioning) -> bool +impl core::fmt::Debug for devlaunch_core::flows::provision::Provisioning +pub fn devlaunch_core::flows::provision::Provisioning::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::provision::Provisioning +pub enum devlaunch_core::flows::provision::ToolsSwitch +pub devlaunch_core::flows::provision::ToolsSwitch::Install +pub devlaunch_core::flows::provision::ToolsSwitch::Skip +impl devlaunch_core::flows::provision::ToolsSwitch +pub fn devlaunch_core::flows::provision::ToolsSwitch::from_env() -> Self +impl core::clone::Clone for devlaunch_core::flows::provision::ToolsSwitch +pub fn devlaunch_core::flows::provision::ToolsSwitch::clone(&self) -> devlaunch_core::flows::provision::ToolsSwitch +impl core::cmp::Eq for devlaunch_core::flows::provision::ToolsSwitch +impl core::cmp::PartialEq for devlaunch_core::flows::provision::ToolsSwitch +pub fn devlaunch_core::flows::provision::ToolsSwitch::eq(&self, &devlaunch_core::flows::provision::ToolsSwitch) -> bool +impl core::fmt::Debug for devlaunch_core::flows::provision::ToolsSwitch +pub fn devlaunch_core::flows::provision::ToolsSwitch::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::Copy for devlaunch_core::flows::provision::ToolsSwitch +impl core::marker::StructuralPartialEq for devlaunch_core::flows::provision::ToolsSwitch +pub struct devlaunch_core::flows::provision::DevpodMissing +impl core::clone::Clone for devlaunch_core::flows::provision::DevpodMissing +pub fn devlaunch_core::flows::provision::DevpodMissing::clone(&self) -> devlaunch_core::flows::provision::DevpodMissing +impl core::cmp::Eq for devlaunch_core::flows::provision::DevpodMissing +impl core::cmp::PartialEq for devlaunch_core::flows::provision::DevpodMissing +pub fn devlaunch_core::flows::provision::DevpodMissing::eq(&self, &devlaunch_core::flows::provision::DevpodMissing) -> bool +impl core::fmt::Debug for devlaunch_core::flows::provision::DevpodMissing +pub fn devlaunch_core::flows::provision::DevpodMissing::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::provision::DevpodMissing +pub struct devlaunch_core::flows::provision::HostLayout +impl devlaunch_core::flows::provision::HostLayout +pub fn devlaunch_core::flows::provision::HostLayout::from_env() -> core::option::Option +impl core::clone::Clone for devlaunch_core::flows::provision::HostLayout +pub fn devlaunch_core::flows::provision::HostLayout::clone(&self) -> devlaunch_core::flows::provision::HostLayout +impl core::cmp::Eq for devlaunch_core::flows::provision::HostLayout +impl core::cmp::PartialEq for devlaunch_core::flows::provision::HostLayout +pub fn devlaunch_core::flows::provision::HostLayout::eq(&self, &devlaunch_core::flows::provision::HostLayout) -> bool +impl core::fmt::Debug for devlaunch_core::flows::provision::HostLayout +pub fn devlaunch_core::flows::provision::HostLayout::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::provision::HostLayout +pub fn devlaunch_core::flows::provision::provision_tools(&dyn devlaunch_runner::Runner, &str, devlaunch_core::flows::provision::ToolsSwitch, core::option::Option<&devlaunch_core::flows::provision::HostLayout>, &mut dyn devlaunch_core::notices::Notices) -> core::result::Result +pub mod devlaunch_core::flows::repo_manager +pub enum devlaunch_core::flows::repo_manager::CacheNotice +pub devlaunch_core::flows::repo_manager::CacheNotice::AdoptedBareClone +pub devlaunch_core::flows::repo_manager::CacheNotice::AdoptedBareClone::bare: std::path::PathBuf +pub devlaunch_core::flows::repo_manager::CacheNotice::AdoptedBareClone::owner: alloc::string::String +pub devlaunch_core::flows::repo_manager::CacheNotice::AdoptedBareClone::repo: alloc::string::String +pub devlaunch_core::flows::repo_manager::CacheNotice::BranchAlreadyBothSides +pub devlaunch_core::flows::repo_manager::CacheNotice::BranchAlreadyBothSides::branch: alloc::string::String +pub devlaunch_core::flows::repo_manager::CacheNotice::BranchCreated +pub devlaunch_core::flows::repo_manager::CacheNotice::BranchCreated::branch: alloc::string::String +pub devlaunch_core::flows::repo_manager::CacheNotice::BranchCutFromRemote +pub devlaunch_core::flows::repo_manager::CacheNotice::BranchCutFromRemote::branch: alloc::string::String +pub devlaunch_core::flows::repo_manager::CacheNotice::BranchCutFromRemote::remote: alloc::string::String +pub devlaunch_core::flows::repo_manager::CacheNotice::BranchPushed +pub devlaunch_core::flows::repo_manager::CacheNotice::BranchPushed::branch: alloc::string::String +pub devlaunch_core::flows::repo_manager::CacheNotice::BranchPushed::remote: alloc::string::String +pub devlaunch_core::flows::repo_manager::CacheNotice::ClearedPartialClone +pub devlaunch_core::flows::repo_manager::CacheNotice::ClearedPartialClone::bare: std::path::PathBuf +pub devlaunch_core::flows::repo_manager::CacheNotice::CloneNotNamed +pub devlaunch_core::flows::repo_manager::CacheNotice::CloneNotNamed::branch: alloc::string::String +pub devlaunch_core::flows::repo_manager::CacheNotice::CloneNotNamed::owner: alloc::string::String +pub devlaunch_core::flows::repo_manager::CacheNotice::CloneNotNamed::refused: devlaunch_core::domain::workspace_id::UnsafeName +pub devlaunch_core::flows::repo_manager::CacheNotice::CloneNotNamed::repo: alloc::string::String +pub devlaunch_core::flows::repo_manager::CacheNotice::ClonedRepository +pub devlaunch_core::flows::repo_manager::CacheNotice::ClonedRepository::owner: alloc::string::String +pub devlaunch_core::flows::repo_manager::CacheNotice::ClonedRepository::repo: alloc::string::String +pub devlaunch_core::flows::repo_manager::CacheNotice::CloningRepository +pub devlaunch_core::flows::repo_manager::CacheNotice::CloningRepository::bare: std::path::PathBuf +pub devlaunch_core::flows::repo_manager::CacheNotice::CloningRepository::remote_url: alloc::string::String +pub devlaunch_core::flows::repo_manager::CacheNotice::CreatingWorkspaceClone +pub devlaunch_core::flows::repo_manager::CacheNotice::CreatingWorkspaceClone::path: std::path::PathBuf +pub devlaunch_core::flows::repo_manager::CacheNotice::DefaultBranchUnknown +pub devlaunch_core::flows::repo_manager::CacheNotice::DefaultBranchUnknown::owner: alloc::string::String +pub devlaunch_core::flows::repo_manager::CacheNotice::DefaultBranchUnknown::reason: devlaunch_core::flows::repo_manager::NotRefreshed +pub devlaunch_core::flows::repo_manager::CacheNotice::DefaultBranchUnknown::repo: alloc::string::String +pub devlaunch_core::flows::repo_manager::CacheNotice::FetchedUpdates +pub devlaunch_core::flows::repo_manager::CacheNotice::FetchedUpdates::owner: alloc::string::String +pub devlaunch_core::flows::repo_manager::CacheNotice::FetchedUpdates::repo: alloc::string::String +pub devlaunch_core::flows::repo_manager::CacheNotice::FetchingRef +pub devlaunch_core::flows::repo_manager::CacheNotice::FetchingRef::branch: alloc::string::String +pub devlaunch_core::flows::repo_manager::CacheNotice::FetchingRef::owner: alloc::string::String +pub devlaunch_core::flows::repo_manager::CacheNotice::FetchingRef::repo: alloc::string::String +pub devlaunch_core::flows::repo_manager::CacheNotice::FetchingUpdates +pub devlaunch_core::flows::repo_manager::CacheNotice::FetchingUpdates::owner: alloc::string::String +pub devlaunch_core::flows::repo_manager::CacheNotice::FetchingUpdates::repo: alloc::string::String +pub devlaunch_core::flows::repo_manager::CacheNotice::FillingLfsCache +pub devlaunch_core::flows::repo_manager::CacheNotice::LfsCacheNotFilled +pub devlaunch_core::flows::repo_manager::CacheNotice::LfsCacheNotFilled::reason: alloc::string::String +pub devlaunch_core::flows::repo_manager::CacheNotice::LfsFilesNotListed +pub devlaunch_core::flows::repo_manager::CacheNotice::LfsFilesNotListed::reason: alloc::string::String +pub devlaunch_core::flows::repo_manager::CacheNotice::LfsNotPulledFromCache +pub devlaunch_core::flows::repo_manager::CacheNotice::LfsNotPulledFromCache::reason: alloc::string::String +pub devlaunch_core::flows::repo_manager::CacheNotice::Metadata(devlaunch_core::domain::metadata::Notice) +pub devlaunch_core::flows::repo_manager::CacheNotice::NoWorkspaceCloneToRemove +pub devlaunch_core::flows::repo_manager::CacheNotice::NoWorkspaceCloneToRemove::path: std::path::PathBuf +pub devlaunch_core::flows::repo_manager::CacheNotice::PreparedFromStaleBase +pub devlaunch_core::flows::repo_manager::CacheNotice::PreparedFromStaleBase::base: alloc::string::String +pub devlaunch_core::flows::repo_manager::CacheNotice::PreparedFromStaleBase::branch: alloc::string::String +pub devlaunch_core::flows::repo_manager::CacheNotice::PreparedFromStaleBase::owner: alloc::string::String +pub devlaunch_core::flows::repo_manager::CacheNotice::PreparedFromStaleBase::reason: devlaunch_core::flows::repo_manager::NotRefreshed +pub devlaunch_core::flows::repo_manager::CacheNotice::PreparedFromStaleBase::repo: alloc::string::String +pub devlaunch_core::flows::repo_manager::CacheNotice::PullingLfsFromOrigin +pub devlaunch_core::flows::repo_manager::CacheNotice::RecordWithoutClone +pub devlaunch_core::flows::repo_manager::CacheNotice::RecordWithoutClone::bare: std::path::PathBuf +pub devlaunch_core::flows::repo_manager::CacheNotice::RecordWithoutClone::owner: alloc::string::String +pub devlaunch_core::flows::repo_manager::CacheNotice::RecordWithoutClone::repo: alloc::string::String +pub devlaunch_core::flows::repo_manager::CacheNotice::RecordedDefaultBranchUnsafe +pub devlaunch_core::flows::repo_manager::CacheNotice::RecordedDefaultBranchUnsafe::refused: devlaunch_core::domain::workspace_id::UnsafeName +pub devlaunch_core::flows::repo_manager::CacheNotice::RefNotFetched +pub devlaunch_core::flows::repo_manager::CacheNotice::RefNotFetched::branch: alloc::string::String +pub devlaunch_core::flows::repo_manager::CacheNotice::RefNotFetched::owner: alloc::string::String +pub devlaunch_core::flows::repo_manager::CacheNotice::RefNotFetched::reason: devlaunch_core::flows::repo_manager::NotRefreshed +pub devlaunch_core::flows::repo_manager::CacheNotice::RefNotFetched::repo: alloc::string::String +pub devlaunch_core::flows::repo_manager::CacheNotice::TrackedFilesNotListed +pub devlaunch_core::flows::repo_manager::CacheNotice::TrackedFilesNotListed::reason: alloc::string::String +pub devlaunch_core::flows::repo_manager::CacheNotice::WorkspaceCloneRemoved +pub devlaunch_core::flows::repo_manager::CacheNotice::WorkspaceCloneRemoved::path: std::path::PathBuf +pub devlaunch_core::flows::repo_manager::CacheNotice::WorkspaceNotRecorded +pub devlaunch_core::flows::repo_manager::CacheNotice::WorkspaceNotRecorded::refusal: devlaunch_core::domain::metadata::MetadataError +pub devlaunch_core::flows::repo_manager::CacheNotice::WorkspaceRecordNotRemoved +pub devlaunch_core::flows::repo_manager::CacheNotice::WorkspaceRecordNotRemoved::refusal: devlaunch_core::domain::metadata::MetadataError +impl core::clone::Clone for devlaunch_core::flows::repo_manager::CacheNotice +pub fn devlaunch_core::flows::repo_manager::CacheNotice::clone(&self) -> devlaunch_core::flows::repo_manager::CacheNotice +impl core::cmp::Eq for devlaunch_core::flows::repo_manager::CacheNotice +impl core::cmp::PartialEq for devlaunch_core::flows::repo_manager::CacheNotice +pub fn devlaunch_core::flows::repo_manager::CacheNotice::eq(&self, &devlaunch_core::flows::repo_manager::CacheNotice) -> bool +impl core::fmt::Debug for devlaunch_core::flows::repo_manager::CacheNotice +pub fn devlaunch_core::flows::repo_manager::CacheNotice::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::repo_manager::CacheNotice +pub enum devlaunch_core::flows::repo_manager::Cleanup +pub devlaunch_core::flows::repo_manager::Cleanup::Cleared +pub devlaunch_core::flows::repo_manager::Cleanup::Left(devlaunch_core::flows::repo_manager::RemoveTreeError) +impl core::clone::Clone for devlaunch_core::flows::repo_manager::Cleanup +pub fn devlaunch_core::flows::repo_manager::Cleanup::clone(&self) -> devlaunch_core::flows::repo_manager::Cleanup +impl core::cmp::Eq for devlaunch_core::flows::repo_manager::Cleanup +impl core::cmp::PartialEq for devlaunch_core::flows::repo_manager::Cleanup +pub fn devlaunch_core::flows::repo_manager::Cleanup::eq(&self, &devlaunch_core::flows::repo_manager::Cleanup) -> bool +impl core::fmt::Debug for devlaunch_core::flows::repo_manager::Cleanup +pub fn devlaunch_core::flows::repo_manager::Cleanup::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::repo_manager::Cleanup +pub enum devlaunch_core::flows::repo_manager::CloneError +pub devlaunch_core::flows::repo_manager::CloneError::GitRefused +pub devlaunch_core::flows::repo_manager::CloneError::GitRefused::cleanup: devlaunch_core::flows::repo_manager::Cleanup +pub devlaunch_core::flows::repo_manager::CloneError::GitRefused::refused: devlaunch_core::clients::git::GitRefused +pub devlaunch_core::flows::repo_manager::CloneError::NotRecorded(devlaunch_core::domain::metadata::MetadataError) +pub devlaunch_core::flows::repo_manager::CloneError::ParentNotCreated +pub devlaunch_core::flows::repo_manager::CloneError::ParentNotCreated::path: std::path::PathBuf +pub devlaunch_core::flows::repo_manager::CloneError::ParentNotCreated::reason: alloc::string::String +pub devlaunch_core::flows::repo_manager::CloneError::PartialCloneNotCleared(devlaunch_core::flows::repo_manager::RemoveTreeError) +impl core::clone::Clone for devlaunch_core::flows::repo_manager::CloneError +pub fn devlaunch_core::flows::repo_manager::CloneError::clone(&self) -> devlaunch_core::flows::repo_manager::CloneError +impl core::cmp::Eq for devlaunch_core::flows::repo_manager::CloneError +impl core::cmp::PartialEq for devlaunch_core::flows::repo_manager::CloneError +pub fn devlaunch_core::flows::repo_manager::CloneError::eq(&self, &devlaunch_core::flows::repo_manager::CloneError) -> bool +impl core::fmt::Debug for devlaunch_core::flows::repo_manager::CloneError +pub fn devlaunch_core::flows::repo_manager::CloneError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::repo_manager::CloneError +pub enum devlaunch_core::flows::repo_manager::EnsureRepoError +pub devlaunch_core::flows::repo_manager::EnsureRepoError::Clone(devlaunch_core::flows::repo_manager::CloneError) +pub devlaunch_core::flows::repo_manager::EnsureRepoError::Lock(devlaunch_core::domain::locks::LockError) +pub devlaunch_core::flows::repo_manager::EnsureRepoError::WrongRepoLock(devlaunch_core::flows::repo_manager::WrongRepoLock) +impl core::clone::Clone for devlaunch_core::flows::repo_manager::EnsureRepoError +pub fn devlaunch_core::flows::repo_manager::EnsureRepoError::clone(&self) -> devlaunch_core::flows::repo_manager::EnsureRepoError +impl core::cmp::Eq for devlaunch_core::flows::repo_manager::EnsureRepoError +impl core::cmp::PartialEq for devlaunch_core::flows::repo_manager::EnsureRepoError +pub fn devlaunch_core::flows::repo_manager::EnsureRepoError::eq(&self, &devlaunch_core::flows::repo_manager::EnsureRepoError) -> bool +impl core::fmt::Debug for devlaunch_core::flows::repo_manager::EnsureRepoError +pub fn devlaunch_core::flows::repo_manager::EnsureRepoError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::repo_manager::EnsureRepoError +pub enum devlaunch_core::flows::repo_manager::NotRefreshed +pub devlaunch_core::flows::repo_manager::NotRefreshed::FetchFailed +pub devlaunch_core::flows::repo_manager::NotRefreshed::FetchFailed::reason: alloc::string::String +pub devlaunch_core::flows::repo_manager::NotRefreshed::NoBranchOnRemote +pub devlaunch_core::flows::repo_manager::NotRefreshed::NoBranchOnRemote::branch: alloc::string::String +pub devlaunch_core::flows::repo_manager::NotRefreshed::NoDefaultBranchRecorded +pub devlaunch_core::flows::repo_manager::NotRefreshed::UnsafeName(devlaunch_core::domain::workspace_id::UnsafeName) +impl core::clone::Clone for devlaunch_core::flows::repo_manager::NotRefreshed +pub fn devlaunch_core::flows::repo_manager::NotRefreshed::clone(&self) -> devlaunch_core::flows::repo_manager::NotRefreshed +impl core::cmp::Eq for devlaunch_core::flows::repo_manager::NotRefreshed +impl core::cmp::PartialEq for devlaunch_core::flows::repo_manager::NotRefreshed +pub fn devlaunch_core::flows::repo_manager::NotRefreshed::eq(&self, &devlaunch_core::flows::repo_manager::NotRefreshed) -> bool +impl core::fmt::Debug for devlaunch_core::flows::repo_manager::NotRefreshed +pub fn devlaunch_core::flows::repo_manager::NotRefreshed::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::repo_manager::NotRefreshed +pub enum devlaunch_core::flows::repo_manager::RefusalReason +pub devlaunch_core::flows::repo_manager::RefusalReason::RootIsSymlink +pub devlaunch_core::flows::repo_manager::RefusalReason::RootIsSymlink::points_at: core::option::Option +pub devlaunch_core::flows::repo_manager::RefusalReason::System(alloc::string::String) +impl core::clone::Clone for devlaunch_core::flows::repo_manager::RefusalReason +pub fn devlaunch_core::flows::repo_manager::RefusalReason::clone(&self) -> devlaunch_core::flows::repo_manager::RefusalReason +impl core::cmp::Eq for devlaunch_core::flows::repo_manager::RefusalReason +impl core::cmp::PartialEq for devlaunch_core::flows::repo_manager::RefusalReason +pub fn devlaunch_core::flows::repo_manager::RefusalReason::eq(&self, &devlaunch_core::flows::repo_manager::RefusalReason) -> bool +impl core::fmt::Debug for devlaunch_core::flows::repo_manager::RefusalReason +pub fn devlaunch_core::flows::repo_manager::RefusalReason::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::repo_manager::RefusalReason +pub enum devlaunch_core::flows::repo_manager::RemoveTreeError +pub devlaunch_core::flows::repo_manager::RemoveTreeError::CouldNotLook +pub devlaunch_core::flows::repo_manager::RemoveTreeError::CouldNotLook::path: std::path::PathBuf +pub devlaunch_core::flows::repo_manager::RemoveTreeError::CouldNotLook::reason: alloc::string::String +pub devlaunch_core::flows::repo_manager::RemoveTreeError::Refused +pub devlaunch_core::flows::repo_manager::RemoveTreeError::Refused::path: std::path::PathBuf +pub devlaunch_core::flows::repo_manager::RemoveTreeError::Refused::reason: alloc::string::String +pub devlaunch_core::flows::repo_manager::RemoveTreeError::RootIsSymlink +pub devlaunch_core::flows::repo_manager::RemoveTreeError::RootIsSymlink::path: std::path::PathBuf +pub devlaunch_core::flows::repo_manager::RemoveTreeError::RootIsSymlink::points_at: core::option::Option +impl core::clone::Clone for devlaunch_core::flows::repo_manager::RemoveTreeError +pub fn devlaunch_core::flows::repo_manager::RemoveTreeError::clone(&self) -> devlaunch_core::flows::repo_manager::RemoveTreeError +impl core::cmp::Eq for devlaunch_core::flows::repo_manager::RemoveTreeError +impl core::cmp::PartialEq for devlaunch_core::flows::repo_manager::RemoveTreeError +pub fn devlaunch_core::flows::repo_manager::RemoveTreeError::eq(&self, &devlaunch_core::flows::repo_manager::RemoveTreeError) -> bool +impl core::fmt::Debug for devlaunch_core::flows::repo_manager::RemoveTreeError +pub fn devlaunch_core::flows::repo_manager::RemoveTreeError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::repo_manager::RemoveTreeError +pub struct devlaunch_core::flows::repo_manager::Refusal +pub devlaunch_core::flows::repo_manager::Refusal::path: std::path::PathBuf +pub devlaunch_core::flows::repo_manager::Refusal::reason: devlaunch_core::flows::repo_manager::RefusalReason +impl core::clone::Clone for devlaunch_core::flows::repo_manager::Refusal +pub fn devlaunch_core::flows::repo_manager::Refusal::clone(&self) -> devlaunch_core::flows::repo_manager::Refusal +impl core::cmp::Eq for devlaunch_core::flows::repo_manager::Refusal +impl core::cmp::PartialEq for devlaunch_core::flows::repo_manager::Refusal +pub fn devlaunch_core::flows::repo_manager::Refusal::eq(&self, &devlaunch_core::flows::repo_manager::Refusal) -> bool +impl core::fmt::Debug for devlaunch_core::flows::repo_manager::Refusal +pub fn devlaunch_core::flows::repo_manager::Refusal::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::repo_manager::Refusal +pub struct devlaunch_core::flows::repo_manager::RepositoryManager<'r> +impl<'r> devlaunch_core::flows::repo_manager::RepositoryManager<'r> +pub fn devlaunch_core::flows::repo_manager::RepositoryManager<'r>::get_default_branch(&self, &devlaunch_core::domain::metadata::MetadataStorage, &str, &str, &mut dyn devlaunch_core::notices::Notices) -> alloc::string::String +impl core::fmt::Debug for devlaunch_core::flows::repo_manager::RepositoryManager<'_> +pub fn devlaunch_core::flows::repo_manager::RepositoryManager<'_>::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +pub struct devlaunch_core::flows::repo_manager::WrongRepoLock +pub devlaunch_core::flows::repo_manager::WrongRepoLock::held: (alloc::string::String, alloc::string::String) +pub devlaunch_core::flows::repo_manager::WrongRepoLock::wanted: (alloc::string::String, alloc::string::String) +impl core::clone::Clone for devlaunch_core::flows::repo_manager::WrongRepoLock +pub fn devlaunch_core::flows::repo_manager::WrongRepoLock::clone(&self) -> devlaunch_core::flows::repo_manager::WrongRepoLock +impl core::cmp::Eq for devlaunch_core::flows::repo_manager::WrongRepoLock +impl core::cmp::PartialEq for devlaunch_core::flows::repo_manager::WrongRepoLock +pub fn devlaunch_core::flows::repo_manager::WrongRepoLock::eq(&self, &devlaunch_core::flows::repo_manager::WrongRepoLock) -> bool +impl core::fmt::Debug for devlaunch_core::flows::repo_manager::WrongRepoLock +pub fn devlaunch_core::flows::repo_manager::WrongRepoLock::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::repo_manager::WrongRepoLock +pub mod devlaunch_core::flows::workspace_clone +pub enum devlaunch_core::flows::workspace_clone::EnsureBranchError +pub devlaunch_core::flows::workspace_clone::EnsureBranchError::Branch(devlaunch_core::flows::branch_manager::BranchError) +pub devlaunch_core::flows::workspace_clone::EnsureBranchError::WrongRepoLock(devlaunch_core::flows::repo_manager::WrongRepoLock) +impl core::clone::Clone for devlaunch_core::flows::workspace_clone::EnsureBranchError +pub fn devlaunch_core::flows::workspace_clone::EnsureBranchError::clone(&self) -> devlaunch_core::flows::workspace_clone::EnsureBranchError +impl core::cmp::Eq for devlaunch_core::flows::workspace_clone::EnsureBranchError +impl core::cmp::PartialEq for devlaunch_core::flows::workspace_clone::EnsureBranchError +pub fn devlaunch_core::flows::workspace_clone::EnsureBranchError::eq(&self, &devlaunch_core::flows::workspace_clone::EnsureBranchError) -> bool +impl core::fmt::Debug for devlaunch_core::flows::workspace_clone::EnsureBranchError +pub fn devlaunch_core::flows::workspace_clone::EnsureBranchError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::workspace_clone::EnsureBranchError +pub enum devlaunch_core::flows::workspace_clone::PrepareColdError +pub devlaunch_core::flows::workspace_clone::PrepareColdError::Branch(devlaunch_core::flows::workspace_clone::EnsureBranchError) +pub devlaunch_core::flows::workspace_clone::PrepareColdError::Clone(devlaunch_core::flows::repo_manager::CloneError) +pub devlaunch_core::flows::workspace_clone::PrepareColdError::Lock(devlaunch_core::domain::locks::LockError) +pub devlaunch_core::flows::workspace_clone::PrepareColdError::UnsafeTriple(devlaunch_core::domain::workspace_id::UnsafeName) +pub devlaunch_core::flows::workspace_clone::PrepareColdError::Workspace(devlaunch_core::flows::workspace_clone::PrepareWorkspaceError) +pub devlaunch_core::flows::workspace_clone::PrepareColdError::WrongRepoLock(devlaunch_core::flows::repo_manager::WrongRepoLock) +impl core::clone::Clone for devlaunch_core::flows::workspace_clone::PrepareColdError +pub fn devlaunch_core::flows::workspace_clone::PrepareColdError::clone(&self) -> devlaunch_core::flows::workspace_clone::PrepareColdError +impl core::cmp::Eq for devlaunch_core::flows::workspace_clone::PrepareColdError +impl core::cmp::PartialEq for devlaunch_core::flows::workspace_clone::PrepareColdError +pub fn devlaunch_core::flows::workspace_clone::PrepareColdError::eq(&self, &devlaunch_core::flows::workspace_clone::PrepareColdError) -> bool +impl core::fmt::Debug for devlaunch_core::flows::workspace_clone::PrepareColdError +pub fn devlaunch_core::flows::workspace_clone::PrepareColdError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::workspace_clone::PrepareColdError +pub enum devlaunch_core::flows::workspace_clone::PrepareWorkspaceError +pub devlaunch_core::flows::workspace_clone::PrepareWorkspaceError::CheckoutRefused +pub devlaunch_core::flows::workspace_clone::PrepareWorkspaceError::CheckoutRefused::branch: alloc::string::String +pub devlaunch_core::flows::workspace_clone::PrepareWorkspaceError::CheckoutRefused::reason: alloc::string::String +pub devlaunch_core::flows::workspace_clone::PrepareWorkspaceError::CloneRefused +pub devlaunch_core::flows::workspace_clone::PrepareWorkspaceError::CloneRefused::cleanup: devlaunch_core::flows::repo_manager::Cleanup +pub devlaunch_core::flows::workspace_clone::PrepareWorkspaceError::CloneRefused::reason: alloc::string::String +pub devlaunch_core::flows::workspace_clone::PrepareWorkspaceError::LfsNotMaterialized +pub devlaunch_core::flows::workspace_clone::PrepareWorkspaceError::LfsNotMaterialized::reason: alloc::string::String +pub devlaunch_core::flows::workspace_clone::PrepareWorkspaceError::NoStartPoint +pub devlaunch_core::flows::workspace_clone::PrepareWorkspaceError::NoStartPoint::branch: alloc::string::String +pub devlaunch_core::flows::workspace_clone::PrepareWorkspaceError::NoStartPoint::default_branch: alloc::string::String +pub devlaunch_core::flows::workspace_clone::PrepareWorkspaceError::ParentNotCreated +pub devlaunch_core::flows::workspace_clone::PrepareWorkspaceError::ParentNotCreated::path: std::path::PathBuf +pub devlaunch_core::flows::workspace_clone::PrepareWorkspaceError::ParentNotCreated::reason: alloc::string::String +pub devlaunch_core::flows::workspace_clone::PrepareWorkspaceError::RemoteNotRepointed +pub devlaunch_core::flows::workspace_clone::PrepareWorkspaceError::RemoteNotRepointed::reason: alloc::string::String +pub devlaunch_core::flows::workspace_clone::PrepareWorkspaceError::UnsafeRefName(devlaunch_core::domain::workspace_id::UnsafeName) +pub devlaunch_core::flows::workspace_clone::PrepareWorkspaceError::WrongRepoLock(devlaunch_core::flows::repo_manager::WrongRepoLock) +impl core::clone::Clone for devlaunch_core::flows::workspace_clone::PrepareWorkspaceError +pub fn devlaunch_core::flows::workspace_clone::PrepareWorkspaceError::clone(&self) -> devlaunch_core::flows::workspace_clone::PrepareWorkspaceError +impl core::cmp::Eq for devlaunch_core::flows::workspace_clone::PrepareWorkspaceError +impl core::cmp::PartialEq for devlaunch_core::flows::workspace_clone::PrepareWorkspaceError +pub fn devlaunch_core::flows::workspace_clone::PrepareWorkspaceError::eq(&self, &devlaunch_core::flows::workspace_clone::PrepareWorkspaceError) -> bool +impl core::fmt::Debug for devlaunch_core::flows::workspace_clone::PrepareWorkspaceError +pub fn devlaunch_core::flows::workspace_clone::PrepareWorkspaceError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::workspace_clone::PrepareWorkspaceError +pub enum devlaunch_core::flows::workspace_clone::RemoveWorkspaceError +pub devlaunch_core::flows::workspace_clone::RemoveWorkspaceError::DirectoryLeft(devlaunch_core::flows::repo_manager::RemoveTreeError) +pub devlaunch_core::flows::workspace_clone::RemoveWorkspaceError::UnsafeTriple(devlaunch_core::domain::workspace_id::UnsafeName) +impl core::clone::Clone for devlaunch_core::flows::workspace_clone::RemoveWorkspaceError +pub fn devlaunch_core::flows::workspace_clone::RemoveWorkspaceError::clone(&self) -> devlaunch_core::flows::workspace_clone::RemoveWorkspaceError +impl core::cmp::Eq for devlaunch_core::flows::workspace_clone::RemoveWorkspaceError +impl core::cmp::PartialEq for devlaunch_core::flows::workspace_clone::RemoveWorkspaceError +pub fn devlaunch_core::flows::workspace_clone::RemoveWorkspaceError::eq(&self, &devlaunch_core::flows::workspace_clone::RemoveWorkspaceError) -> bool +impl core::fmt::Debug for devlaunch_core::flows::workspace_clone::RemoveWorkspaceError +pub fn devlaunch_core::flows::workspace_clone::RemoveWorkspaceError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::workspace_clone::RemoveWorkspaceError +pub enum devlaunch_core::flows::workspace_clone::Removed +pub devlaunch_core::flows::workspace_clone::Removed::Clone +pub devlaunch_core::flows::workspace_clone::Removed::DirectoryAbsent +pub devlaunch_core::flows::workspace_clone::Removed::DirectoryNotNamed +pub devlaunch_core::flows::workspace_clone::Removed::NothingRecorded +impl core::clone::Clone for devlaunch_core::flows::workspace_clone::Removed +pub fn devlaunch_core::flows::workspace_clone::Removed::clone(&self) -> devlaunch_core::flows::workspace_clone::Removed +impl core::cmp::Eq for devlaunch_core::flows::workspace_clone::Removed +impl core::cmp::PartialEq for devlaunch_core::flows::workspace_clone::Removed +pub fn devlaunch_core::flows::workspace_clone::Removed::eq(&self, &devlaunch_core::flows::workspace_clone::Removed) -> bool +impl core::fmt::Debug for devlaunch_core::flows::workspace_clone::Removed +pub fn devlaunch_core::flows::workspace_clone::Removed::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::Copy for devlaunch_core::flows::workspace_clone::Removed +impl core::marker::StructuralPartialEq for devlaunch_core::flows::workspace_clone::Removed +pub struct devlaunch_core::flows::workspace_clone::WorkspaceCloneManager<'r> +impl<'r> devlaunch_core::flows::workspace_clone::WorkspaceCloneManager<'r> +pub fn devlaunch_core::flows::workspace_clone::WorkspaceCloneManager<'r>::from_config(&devlaunch_core::domain::config::WorktreeConfig, devlaunch_core::clients::git::Git<'r>) -> Self +pub fn devlaunch_core::flows::workspace_clone::WorkspaceCloneManager<'r>::on_repo_lock_wait(&mut self, impl core::ops::function::Fn(&str, &str) + 'static) +pub fn devlaunch_core::flows::workspace_clone::WorkspaceCloneManager<'r>::repo_manager(&self) -> &devlaunch_core::flows::repo_manager::RepositoryManager<'r> +impl core::fmt::Debug for devlaunch_core::flows::workspace_clone::WorkspaceCloneManager<'_> +pub fn devlaunch_core::flows::workspace_clone::WorkspaceCloneManager<'_>::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +pub mod devlaunch_core::json +pub enum devlaunch_core::json::JsonKind +pub devlaunch_core::json::JsonKind::Array +pub devlaunch_core::json::JsonKind::Bool +pub devlaunch_core::json::JsonKind::Null +pub devlaunch_core::json::JsonKind::Number +pub devlaunch_core::json::JsonKind::Object +pub devlaunch_core::json::JsonKind::String +impl core::clone::Clone for devlaunch_core::json::JsonKind +pub fn devlaunch_core::json::JsonKind::clone(&self) -> devlaunch_core::json::JsonKind +impl core::cmp::Eq for devlaunch_core::json::JsonKind +impl core::cmp::PartialEq for devlaunch_core::json::JsonKind +pub fn devlaunch_core::json::JsonKind::eq(&self, &devlaunch_core::json::JsonKind) -> bool +impl core::fmt::Debug for devlaunch_core::json::JsonKind +pub fn devlaunch_core::json::JsonKind::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::Copy for devlaunch_core::json::JsonKind +impl core::marker::StructuralPartialEq for devlaunch_core::json::JsonKind +pub mod devlaunch_core::notices +pub trait devlaunch_core::notices::Notices +pub fn devlaunch_core::notices::Notices::say(&mut self, T) +impl devlaunch_core::notices::Notices for alloc::vec::Vec +pub fn alloc::vec::Vec::say(&mut self, T) +pub mod devlaunch_core::runner +pub use devlaunch_core::runner::<> +pub mod devlaunch_core::shell +pub fn devlaunch_core::shell::join<'a>(impl core::iter::traits::collect::IntoIterator) -> alloc::string::String +pub fn devlaunch_core::shell::quote(&str) -> alloc::borrow::Cow<'_, str> +pub mod devlaunch_core::timing +pub enum devlaunch_core::timing::Report +pub devlaunch_core::timing::Report::Document(Document) +pub devlaunch_core::timing::Report::Prose(Prose) +impl devlaunch_core::timing::Report +pub fn devlaunch_core::timing::Report::lines(&self) -> alloc::vec::Vec +impl core::clone::Clone for devlaunch_core::timing::Report +pub fn devlaunch_core::timing::Report::clone(&self) -> devlaunch_core::timing::Report +impl core::cmp::PartialEq for devlaunch_core::timing::Report +pub fn devlaunch_core::timing::Report::eq(&self, &devlaunch_core::timing::Report) -> bool +impl core::fmt::Debug for devlaunch_core::timing::Report +pub fn devlaunch_core::timing::Report::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::timing::Report +pub const devlaunch_core::timing::HANDOFF_VAR: &str +pub const devlaunch_core::timing::PREWARM_VAR: &str +pub fn devlaunch_core::timing::begin() +pub fn devlaunch_core::timing::emit() -> core::option::Option diff --git a/rust/devlaunch-core/src/lib.rs b/rust/devlaunch-core/src/lib.rs index 02a8e5c8..10c03700 100644 --- a/rust/devlaunch-core/src/lib.rs +++ b/rust/devlaunch-core/src/lib.rs @@ -38,8 +38,10 @@ //! //! So a `pub` item here is not automatically part of the promised API. Only //! what [`api`] re-exports is. The distinction is enforced by keeping [`api`] -//! the single re-export point and by a `cargo public-api` snapshot in CI; -//! until that lands the two tiers are told apart by the doc note above. +//! the single re-export point, by the doc note above on every binary-surface +//! item, and by the `cargo public-api` snapshot in `public-api.txt`, which CI +//! diffs on every pull request: any change to the crate's public surface is a +//! committed, reviewed diff or a red tick. // `runner` is pub so `devlaunch-test-support` can implement the trait; that // crate is dev-only and never shipped.