Switch to a different view and update the working copy. This is the primary method for switching views. It: 1. Validates the view exists 2. Updates the current view pointer 3. Materializes the working copy to match the new view's state # Arguments `view` - The name of the view to switch to # Returns Statistics about the materialize operation (files written, etc.) # Errors Returns an error i
(&mut self, view: &str)
| 89 | /// println!("Updated {} files", result.files_written); |
| 90 | /// ``` |
| 91 | pub fn switch_view(&mut self, view: &str) -> Result<MaterializeResult, RepositoryError> { |
| 92 | let old_view_name = self.current_view.clone(); |
| 93 | |
| 94 | // Compute files visible on the OLD view. |
| 95 | let old_files = self.visible_file_paths(&old_view_name)?; |
| 96 | |
| 97 | // Set the current view (validates it exists) |
| 98 | self.set_current_view(view)?; |
| 99 | |
| 100 | // Compute files visible on the NEW view. |
| 101 | let new_files = self.visible_file_paths(view)?; |
| 102 | |
| 103 | if std::env::var_os("ATOMIC_TRACE_SWITCH").is_some() { |
| 104 | eprintln!("[switch] {} -> {}", old_view_name, view); |
| 105 | eprintln!( |
| 106 | "[switch] old_files={} new_files={}", |
| 107 | old_files.len(), |
| 108 | new_files.len() |
| 109 | ); |
| 110 | for f in old_files.difference(&new_files) { |
| 111 | eprintln!("[switch] REMOVE (old only): {}", f); |
| 112 | } |
| 113 | for f in new_files.difference(&old_files) { |
| 114 | eprintln!("[switch] ADD (new only): {}", f); |
| 115 | } |
| 116 | } |
| 117 | |
| 118 | let working_copy = FileSystem::from_root(&self.root); |
| 119 | |
| 120 | // ── Phase 1: Shelve ignored files into the OLD view's workspace ── |
| 121 | // |
| 122 | // All ignored files are shelved per-view EXCEPT paths listed in |
| 123 | // `[workspace] expose` in `.atomic/config.toml`. Exposed paths |
| 124 | // persist across all views (tool configs like .opencode/, .vscode/). |
| 125 | // |
| 126 | // This uses `rename()` which is O(1) on the same filesystem — |
| 127 | // no data is copied, just inode pointers are updated. |
| 128 | // |
| 129 | // The rule: |
| 130 | // - Tracked files → managed by the graph (phases 2-4) |
| 131 | // - Untracked, ignored, exposed → left alone (persists across views) |
| 132 | // - Untracked, ignored, NOT exposed → shelved/restored per-view (phases 1 & 5) |
| 133 | // - Untracked, novel → user's undecided work, left alone |
| 134 | let old_ws = workspace_path(&self.dot_dir, &old_view_name); |
| 135 | ensure_workspace_dir(&self.dot_dir, &old_view_name)?; |
| 136 | |
| 137 | let repo_expose = atomic_config::RepoConfig::load(&self.config_path()) |
| 138 | .unwrap_or_default() |
| 139 | .workspace |
| 140 | .expose; |
| 141 | let global_expose = atomic_config::GlobalConfig::load() |
| 142 | .map(|c| c.workspace.expose) |
| 143 | .unwrap_or_default(); |
| 144 | |
| 145 | // Merge global + repo-local expose patterns (deduplicated) |
| 146 | let mut expose_patterns = global_expose; |
| 147 | for p in repo_expose { |
| 148 | if !expose_patterns.contains(&p) { |