|

config: add rename_project registry mutation

Author: Chris Tusa <chris.tusa@leafscale.com>
Date: Jul 15, 2026 14:25
Changeset: 216a154d7f7d76a6c1971c0468eaad6375618711
Branch: default

Diff

diff -r 1cca34dfff9a -r 216a154d7f7d src/config.reef
--- a/src/config.reef	Wed Jul 15 14:24:07 2026 -0500
+++ b/src/config.reef	Wed Jul 15 14:25:37 2026 -0500
@@ -42,6 +42,7 @@
     fn add_project(reg: Registry, p: Project): rg.Result[Registry, string]
     fn update_last_sync(reg: Registry, name: string, ts: string): rg.Result[Registry, string]
     fn remove_project(reg: Registry, name: string): rg.Result[Registry, string]
+    fn rename_project(reg: Registry, old: string, new_name: string): rg.Result[Registry, string]
     fn registry_path(home_dir: string): string
     fn default_registry(home_dir: string): Registry
     fn load_or_init(home_dir: string): rg.Result[Registry, string]
@@ -488,6 +489,56 @@
     return @Result[Registry, string].Ok(with_projects(reg, new_projects))
 end remove_project
 
+// Rename the project named `old` to `new`. The repo dirname moves in lockstep
+// only when it was coupled to the old name (repo == old); a deliberately
+// decoupled dirname (e.g. a nested monorepo path) is preserved. Errors if `old`
+// is absent or `new` is already used by a different project.
+fn rename_project(reg: Registry, old: string, new_name: string): rg.Result[Registry, string]
+    let n: int = reg.projects.length()
+    mut found: int = -1
+    mut i: int = 0
+    while i < n
+        if reg.projects[i].name == old
+            found = i
+        end if
+        i = i + 1
+    end while
+
+    if found < 0
+        return @Result[Registry, string].Err("project not in registry: " + old)
+    end if
+
+    mut j: int = 0
+    while j < n
+        if j != found and reg.projects[j].name == new_name
+            return @Result[Registry, string].Err("project already exists: " + new_name)
+        end if
+        j = j + 1
+    end while
+
+    mut new_projects: [Project] = new [Project](n)
+    mut k: int = 0
+    while k < n
+        if k == found
+            let old_p: Project = reg.projects[k]
+            mut new_repo: string = old_p.repo
+            if old_p.repo == old
+                new_repo = new_name
+            end if
+            new_projects[k] = Project {
+                name: new_name, repo: new_repo, image: old_p.image,
+                profiles: old_p.profiles, created: old_p.created,
+                last_sync: old_p.last_sync, backup: old_p.backup
+            }
+        else
+            new_projects[k] = reg.projects[k]
+        end if
+        k = k + 1
+    end while
+
+    return @Result[Registry, string].Ok(with_projects(reg, new_projects))
+end rename_project
+
 fn registry_path(home_dir: string): string
     let cfg_dir: string = paths.join(home_dir, ".config/repoman")
     return paths.join(cfg_dir, "repoman.toml")
diff -r 1cca34dfff9a -r 216a154d7f7d tests/test_config_rename.reef
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test_config_rename.reef	Wed Jul 15 14:25:37 2026 -0500
@@ -0,0 +1,76 @@
+/******************************************************************************
+                __               ____                __
+               / /   ___  ____ _/ __/_____________ _/ /__
+              / /   / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \
+             / /___/  __/ /_/ / __(__  ) /__/ /_/ / /  __/
+            /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/
+
+    (C)opyright 2026, Leafscale, LLC -  https://www.leafscale.com
+
+    Project: repoman
+   Filename: tests/test_config_rename.reef
+    Authors: Chris Tusa <chris.tusa@leafscale.com>
+    License: <see LICENSE file included with this source code>
+Description: Tests: rename_project registry mutation
+
+******************************************************************************/
+
+import config
+import test.framework
+import core.result as rg
+
+fn empty_defaults(): config.Defaults
+    return config.Defaults {
+        repos_root: "/r", backup_root: "/b", logdir: "/l", incus_project: "p",
+        default_image: "img", profiles: new [string](0)
+    }
+end empty_defaults
+
+fn mk_project(name: string, repo: string): config.Project
+    return config.Project {
+        name: name, repo: repo, image: "img",
+        profiles: new [string](0), created: "t", last_sync: "", backup: true
+    }
+end mk_project
+
+proc main()
+    let runner = new framework.TestRunner()
+
+    // Two projects: a coupled one (name==repo) and a decoupled one (name!=repo).
+    mut projs: [config.Project] = new [config.Project](2)
+    projs[0] = mk_project("driftohm", "driftohm")
+    projs[1] = mk_project("portal", "leafscale-infrastructure/portal.leafscale.com")
+    let reg0: config.Registry = config.Registry {
+        schema: 3, host: config.Host { lan_ip: "" }, output: "quiet",
+        defaults: empty_defaults(), projects: projs
+    }
+
+    // Coupled rename: both name and repo move.
+    let r1 = config.rename_project(reg0, "driftohm", "mist")
+    runner.assert_eq_bool(rg.is_ok(r1), true, "coupled rename ok")
+    if rg.is_ok(r1)
+        let reg1 = rg.unwrap_ok(r1)
+        runner.assert_eq_string(reg1.projects[0].name, "mist", "coupled: name moved")
+        runner.assert_eq_string(reg1.projects[0].repo, "mist", "coupled: repo moved in lockstep")
+        runner.assert_eq_string(reg1.output, "quiet", "output preserved through rename_project")
+    end if
+
+    // Decoupled rename: only name moves, repo (nested path) preserved.
+    let r2 = config.rename_project(reg0, "portal", "gateway")
+    runner.assert_eq_bool(rg.is_ok(r2), true, "decoupled rename ok")
+    if rg.is_ok(r2)
+        let reg2 = rg.unwrap_ok(r2)
+        runner.assert_eq_string(reg2.projects[1].name, "gateway", "decoupled: name moved")
+        runner.assert_eq_string(reg2.projects[1].repo, "leafscale-infrastructure/portal.leafscale.com", "decoupled: repo dirname preserved")
+    end if
+
+    // Collision: rename onto an existing project name fails.
+    let r3 = config.rename_project(reg0, "driftohm", "portal")
+    runner.assert_eq_bool(rg.is_err(r3), true, "rename onto existing name rejected")
+
+    // Unknown source name fails.
+    let r4 = config.rename_project(reg0, "ghost", "whatever")
+    runner.assert_eq_bool(rg.is_err(r4), true, "unknown source name rejected")
+
+    runner.report()
+end main