checkout BOOKMARK switches to it; workspace add takes a bookmark and its subrepos a46f3e4f50b1
oxy checkout NAME, NAME a bookmark other than the current one, is bookmark switch; it used to check NAME's files out under the current bookmark, and in metalgear three features were committed onto feature/46-actor-banks from a workspace that said it was on develop. workspace add PATH BOOKMARK puts the workspace on BOOKMARK. A new workspace gets each subrepo at its pin by a local clone of this workspace's copy, its origin set back to the subrepo's URL. Test. closes #60 closes #61
- Author
- Andy McCall @andymccall <[email protected]>
- When
- · 2026-10-02 21:06 UTC
- Signature
- ✔ signed by Andy McCall
- Hash
a46f3e4f50b1899b5e7b404baf4ae8f4a396d496bb822a900eb922b5b312bd74- Depends on
- 132b884b9a8d 4e88e774777a 75167672ebcd 7b9809e67c3d 827281eed2ed 8fd78eb52ad2 982864068a59 bea00d4177c6 c07406bcc237 fce44ae9ad29
- Depended on by
- 47ce8b8e2d4b 6a0008781d5e
- In bookmarks
- agents-in-readme bare-hashes carried-not-removed carry-identical doctor-trust identical-sides keyless-tags main merge-remote-bookmark only-help-103 paths-for-resolutions-and-amend push-notes show-stat subrepo-pin-state switch-says
- In tags
- v0.10.0 v0.11.0 v0.3.1 v0.4.0 v0.4.1 v0.4.2 v0.4.3 v0.4.4 v0.4.5 v0.4.6 v0.5.0 v0.5.1 v0.6.0 v0.7.0 v0.8.0 v0.9.0
- Edits
- 11 atoms, 0 tree operations, 4 files
Browse the tree at this change
modified CHANGELOG.md +4 −0
| … | 1 unchanged lines |
||
| 2 | 2 | ||
| 3 | 3 | ## Unreleased |
|
| 4 | 4 | ||
| 5 | + | - **Being on the bookmark you asked for (#60).** `oxy checkout BOOKMARK` switches to that bookmark (as `oxy bookmark switch` does, with `--discard` or `--carry`), where it used to write its files under the current bookmark's name, so the next commit moved the wrong bookmark. `oxy workspace add PATH BOOKMARK` makes a workspace on that bookmark rather than this one's. |
|
| 6 | + | ||
| 7 | + | - **A new workspace has its subrepos (#61).** `oxy workspace add` brings each subrepo at its pin from this workspace's own copy, with no network or sign-in, its remote pointing where the subrepo comes from. |
|
| 8 | + | ||
| 5 | 9 | ## 0.3.0 (2026-10-02) |
|
| 6 | 10 | ||
| 7 | 11 | - **Fetch says when a bookmark is behind (#56).** `oxy fetch` reports each bookmark against the remote's copy ("main is 1 change behind: oxy pull to take it", ahead, diverged) and says "up to date" only when they match; `--json` gives `ahead` and `behind`. A change that was fetched but not pulled resolves by id or hash, and `oxy show` says it is only in `origin/main`. With one remote not called `origin`, fetch, pull and push use it, and `bookmark list` and the prompt count against it. |
|
| … | 70 unchanged lines |
modified crates/oxy-core/src/repo/tests.rs +55 −0
| … | 6938 unchanged lines |
||
| 6939 | 6939 | assert_eq!(b.config.remotes["origin"], url); |
|
| 6940 | 6940 | b.fetch("origin").unwrap(); |
|
| 6941 | 6941 | } |
|
| 6942 | + | ||
| 6943 | + | /// oxygen#60 and #61: a workspace added on a bookmark is on that bookmark, |
|
| 6944 | + | /// and gets its subrepos at their pins from this workspace's copies, with |
|
| 6945 | + | /// their remotes pointing where the subrepos come from. |
|
| 6946 | + | #[test] |
|
| 6947 | + | fn a_workspace_added_on_a_bookmark_is_on_it_with_its_subrepos() { |
|
| 6948 | + | hermetic(); |
|
| 6949 | + | let dir = tempfile::tempdir().unwrap(); |
|
| 6950 | + | let root = dir.path(); |
|
| 6951 | + | Repo::init(&root.join("lib-origin")).unwrap(); |
|
| 6952 | + | let lib_url = root.join("lib-origin").to_string_lossy().to_string(); |
|
| 6953 | + | let lib = Repo::clone(&lib_url, &root.join("lib")) |
|
| 6954 | + | .unwrap() |
|
| 6955 | + | .with_home(root.join("home")); |
|
| 6956 | + | write(&root.join("lib"), "lib.txt", b"v1\n"); |
|
| 6957 | + | let l1 = lib.commit("lib v1", &[]).unwrap(); |
|
| 6958 | + | assert!(lib.push("origin", false).unwrap()[0].ok); |
|
| 6959 | + | ||
| 6960 | + | let app_dir = root.join("app"); |
|
| 6961 | + | let app = Repo::init(&app_dir).unwrap().with_home(root.join("home")); |
|
| 6962 | + | write(&app_dir, "main.txt", b"app\n"); |
|
| 6963 | + | app.submodule_add(&lib_url, "libs/lib", None).unwrap(); |
|
| 6964 | + | app.commit("app with lib", &[]).unwrap(); |
|
| 6965 | + | // A second bookmark, and the main workspace on another. |
|
| 6966 | + | app.bookmark_create("develop", None).unwrap(); |
|
| 6967 | + | app.bookmark_create("feature", None).unwrap(); |
|
| 6968 | + | let mut app = app; |
|
| 6969 | + | app.bookmark_switch("feature", false).unwrap(); |
|
| 6970 | + | assert_eq!(app.config.bookmark, "feature"); |
|
| 6971 | + | ||
| 6972 | + | // The library's origin goes away: the subrepo must come from here. |
|
| 6973 | + | std::fs::rename(root.join("lib-origin"), root.join("lib-origin-gone")).unwrap(); |
|
| 6974 | + | let heads = app.resolve_heads("develop").unwrap(); |
|
| 6975 | + | let ws = app |
|
| 6976 | + | .workspace_add_on(&root.join("ws"), Some(&heads), Some("develop")) |
|
| 6977 | + | .unwrap(); |
|
| 6978 | + | assert_eq!( |
|
| 6979 | + | ws.config.bookmark, "develop", |
|
| 6980 | + | "the new workspace is on develop" |
|
| 6981 | + | ); |
|
| 6982 | + | assert_eq!( |
|
| 6983 | + | app.config.bookmark, "feature", |
|
| 6984 | + | "and this one stays on its own" |
|
| 6985 | + | ); |
|
| 6986 | + | let sub = Repo::open(&root.join("ws/libs/lib")).unwrap(); |
|
| 6987 | + | assert_eq!( |
|
| 6988 | + | sub.config.remotes["origin"], lib_url, |
|
| 6989 | + | "its origin is the subrepo's URL" |
|
| 6990 | + | ); |
|
| 6991 | + | assert_eq!(sub.bookmark_heads().unwrap(), vec![l1], "at its pin"); |
|
| 6992 | + | assert_eq!( |
|
| 6993 | + | std::fs::read(root.join("ws/libs/lib/lib.txt")).unwrap(), |
|
| 6994 | + | b"v1\n" |
|
| 6995 | + | ); |
|
| 6996 | + | } |
modified crates/oxy-core/src/repo/workspace.rs +67 −1
| … | 183 unchanged lines |
||
| 184 | 184 | ||
| 185 | 185 | /// Create a workspace at `dir` checked out on `heads` (default the |
|
| 186 | 186 | /// bookmark's), and return a `Repo` rooted there. |
|
| 187 | + | /// `bookmark` is the new workspace's current bookmark (oxygen#60), by |
|
| 188 | + | /// default this workspace's. |
|
| 187 | 189 | pub fn workspace_add(&self, dir: &Path, heads: Option<&[ChangeHash]>) -> Result<Repo> { |
|
| 190 | + | self.workspace_add_on(dir, heads, None) |
|
| 191 | + | } |
|
| 192 | + | ||
| 193 | + | /// [`Repo::workspace_add`] with the new workspace on `bookmark`. |
|
| 194 | + | pub fn workspace_add_on( |
|
| 195 | + | &self, |
|
| 196 | + | dir: &Path, |
|
| 197 | + | heads: Option<&[ChangeHash]>, |
|
| 198 | + | bookmark: Option<&str>, |
|
| 199 | + | ) -> Result<Repo> { |
|
| 188 | 200 | if dir.exists() && fs::read_dir(dir)?.next().is_some() { |
|
| 189 | 201 | return fail(format!("{} exists and is not empty", dir.display())); |
|
| 190 | 202 | } |
|
| … | 18 unchanged lines |
||
| 209 | 221 | view, |
|
| 210 | 222 | )?; |
|
| 211 | 223 | self.register_workspace(&id, &dir)?; |
|
| 212 | - | self.set_workspace_bookmark(&id, &self.config.bookmark)?; |
|
| 224 | + | self.set_workspace_bookmark(&id, bookmark.unwrap_or(&self.config.bookmark))?; |
|
| 213 | 225 | write_atomic( |
|
| 214 | 226 | &dir.join(OXY_DIR), |
|
| 215 | 227 | format!( |
|
| … | 5 unchanged lines |
||
| 221 | 233 | )?; |
|
| 222 | 234 | let repo = Repo::open(&dir)?.with_home(self.keys_dir.parent().unwrap_or(&self.keys_dir)); |
|
| 223 | 235 | repo.checkout(&heads, true, "oxy workspace add (checkout)")?; |
|
| 236 | + | self.copy_subrepos_into(&repo); |
|
| 224 | 237 | Ok(repo) |
|
| 238 | + | } |
|
| 239 | + | ||
| 240 | + | /// The new workspace's subrepos at their pins, from this workspace's |
|
| 241 | + | /// own copies (oxygen#61): a local clone, its remote then pointed at |
|
| 242 | + | /// the subrepo's URL, and the pin checked out, with no network unless |
|
| 243 | + | /// this copy lacks the pinned changes. A subrepo this workspace does |
|
| 244 | + | /// not have stays for `oxy submodule update --init`. Best effort. |
|
| 245 | + | fn copy_subrepos_into(&self, new: &Repo) { |
|
| 246 | + | let Ok(subs) = new.subrepos() else { return }; |
|
| 247 | + | let modules = new.modules_on_disk(); |
|
| 248 | + | for s in subs { |
|
| 249 | + | let (src, dst) = (self.root.join(&s.path), new.root.join(&s.path)); |
|
| 250 | + | if dst.join(OXY_DIR).exists() || dst.join(".git").exists() { |
|
| 251 | + | continue; |
|
| 252 | + | } |
|
| 253 | + | let url = modules.entries.get(&s.path).map(|m| m.url.clone()); |
|
| 254 | + | if let Some(parent) = dst.parent() { |
|
| 255 | + | let _ = fs::create_dir_all(parent); |
|
| 256 | + | } |
|
| 257 | + | let _ = fs::remove_dir(&dst); |
|
| 258 | + | if src.join(OXY_DIR).exists() { |
|
| 259 | + | match Repo::clone(&src.to_string_lossy(), &dst) { |
|
| 260 | + | Ok(mut sub) => { |
|
| 261 | + | if let Some(u) = &url { |
|
| 262 | + | if sub.config.set("remote.origin.url", u).is_ok() { |
|
| 263 | + | let _ = sub.save_config(); |
|
| 264 | + | } |
|
| 265 | + | } |
|
| 266 | + | } |
|
| 267 | + | Err(_) => continue, |
|
| 268 | + | } |
|
| 269 | + | } else if src.join(".git").exists() { |
|
| 270 | + | let cloned = std::process::Command::new("git") |
|
| 271 | + | .args(["clone", "-q", "--local"]) |
|
| 272 | + | .arg(&src) |
|
| 273 | + | .arg(&dst) |
|
| 274 | + | .status() |
|
| 275 | + | .is_ok_and(|st| st.success()); |
|
| 276 | + | if !cloned { |
|
| 277 | + | continue; |
|
| 278 | + | } |
|
| 279 | + | if let Some(u) = &url { |
|
| 280 | + | let _ = std::process::Command::new("git") |
|
| 281 | + | .arg("-C") |
|
| 282 | + | .arg(&dst) |
|
| 283 | + | .args(["remote", "set-url", "origin", u]) |
|
| 284 | + | .status(); |
|
| 285 | + | } |
|
| 286 | + | } else { |
|
| 287 | + | continue; |
|
| 288 | + | } |
|
| 289 | + | let _ = new.sync_subrepo(&s.path, &s.pin, false); |
|
| 290 | + | } |
|
| 225 | 291 | } |
|
| 226 | 292 | ||
| 227 | 293 | /// Drop a workspace from the view and registry; its files stay. |
|
| … | 61 unchanged lines |
modified src/cli/workspace.rs +37 −0
| … | 1114 unchanged lines |
||
| 1115 | 1115 | ||
| 1116 | 1116 | pub fn checkout(changes: &[String], discard: bool, carry: bool) -> Result<i32> { |
|
| 1117 | 1117 | let repo = open()?; |
|
| 1118 | + | // `oxy checkout BOOKMARK` is being on that bookmark, as `git checkout |
|
| 1119 | + | // BRANCH` is: checking its files out under another bookmark's name |
|
| 1120 | + | // made the next commit move the wrong bookmark (oxygen#60). |
|
| 1121 | + | if let [name] = changes { |
|
| 1122 | + | if *name != repo.config.bookmark && names_bookmark(&repo, name)? { |
|
| 1123 | + | drop(repo); |
|
| 1124 | + | return bookmark_switch(name, discard, carry); |
|
| 1125 | + | } |
|
| 1126 | + | } |
|
| 1118 | 1127 | let heads = if changes.is_empty() { |
|
| 1119 | 1128 | repo.bookmark_heads()? |
|
| 1120 | 1129 | } else { |
|
| … | 788 unchanged lines |
||
| 1909 | 1918 | ); |
|
| 1910 | 1919 | } |
|
| 1911 | 1920 | Ok(0) |
|
| 1921 | + | } |
|
| 1922 | + | ||
| 1923 | + | /// Whether `name` is a bookmark, here or only on a remote: what checkout |
|
| 1924 | + | /// and workspace add take as "be on this bookmark" (oxygen#60). |
|
| 1925 | + | fn names_bookmark(repo: &oxy_core::repo::Repo, name: &str) -> Result<bool> { |
|
| 1926 | + | Ok(repo.view()?.bookmarks.contains_key(name) |
|
| 1927 | + | || repo |
|
| 1928 | + | .remote_only_bookmarks()? |
|
| 1929 | + | .iter() |
|
| 1930 | + | .any(|b| b.name == name || format!("{}/{}", b.remote, b.name) == name)) |
|
| 1912 | 1931 | } |
|
| 1913 | 1932 | ||
| 1914 | 1933 | pub fn workspace_add(path: &std::path::Path, changes: &[String]) -> Result<i32> { |
|
| 1915 | 1934 | let repo = open()?; |
|
| 1935 | + | // One bookmark named: the new workspace is on it (oxygen#60), not on |
|
| 1936 | + | // this workspace's bookmark with that one's files. |
|
| 1937 | + | if let [name] = changes { |
|
| 1938 | + | if names_bookmark(&repo, name)? { |
|
| 1939 | + | let (local, _) = repo.local_bookmark_for(name)?; |
|
| 1940 | + | let heads = repo.resolve_heads(&local)?; |
|
| 1941 | + | let sp = ui::spinner(&format!("Creating workspace at {}…", path.display())); |
|
| 1942 | + | let ws = repo.workspace_add_on(path, Some(&heads), Some(&local))?; |
|
| 1943 | + | sp.done(&format!( |
|
| 1944 | + | "{} {} (workspace {}) on {}", |
|
| 1945 | + | ui::done("Workspace created at"), |
|
| 1946 | + | ui::path(ws.root().display()), |
|
| 1947 | + | ui::id(ws.workspace().short(8)), |
|
| 1948 | + | ui::bookmark(&local) |
|
| 1949 | + | )); |
|
| 1950 | + | return Ok(0); |
|
| 1951 | + | } |
|
| 1952 | + | } |
|
| 1916 | 1953 | let heads = if changes.is_empty() { |
|
| 1917 | 1954 | None |
|
| 1918 | 1955 | } else { |
|
| … | 486 unchanged lines |