config: add rename_project registry mutation
Author:
Chris Tusa <chris.tusa@leafscale.com>
Date:
Jul 15, 2026 14:25
Changeset:
216a154d7f7d76a6c1971c0468eaad6375618711
Branch:
default
Changed files:
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