WorkingSetResolver.java

package network.ike.workspace;

import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;

/**
 * Resolves the {@link WorkingSet} a working-tree operation acts on, from a
 * starting directory (IKE-Network/ike-issues#609, under #601).
 *
 * <p>Searches upward from the start directory for a manifest —
 * {@value #MANIFEST_FILE}, or {@value #LEGACY_MANIFEST_FILE} in a working
 * set the scaffold has not upgraded yet (ike-issues#1054). When one is
 * found, the working set is what it declares: its subprojects plus the
 * working-set root. When none is found, the working set is the single
 * repository at the start directory: a working set of one.
 *
 * <p>This is the single home for the "am I in a declared working set, or a
 * lone repo?" decision that working-tree goals otherwise each make for
 * themselves — the scattered {@code isWorkspaceMode()} + bare-mode branches
 * the migration in ike-issues#611 retires.
 */
public final class WorkingSetResolver {

    /**
     * The manifest file name — the thing it describes is a <em>working
     * set</em>, so that is what the file is called
     * (IKE-Network/ike-issues#1054).
     */
    public static final String MANIFEST_FILE = "working-set.yaml";

    /**
     * The manifest's former name. Read forever: manifests written before
     * the rename live in tagged trees, and a working set is upgraded by
     * the scaffold, not by every reader assuming it already happened.
     */
    public static final String LEGACY_MANIFEST_FILE = "workspace.yaml";

    /**
     * The manifest already present in a directory — the current name
     * preferred, the former name accepted.
     *
     * @param dir the directory to look in
     * @return the existing manifest path, or {@code null} when the
     *         directory holds neither
     */
    public static Path manifestIn(Path dir) {
        Path current = dir.resolve(MANIFEST_FILE);
        if (current.toFile().exists()) {
            return current;
        }
        Path legacy = dir.resolve(LEGACY_MANIFEST_FILE);
        return legacy.toFile().exists() ? legacy : null;
    }

    /**
     * Where a writer should write a directory's manifest: the file that is
     * already there, or the current name when there is none. A writer must
     * never introduce a second manifest beside an existing one.
     *
     * @param dir the working-set root directory
     * @return the path to write
     */
    public static Path manifestToWrite(Path dir) {
        Path existing = manifestIn(dir);
        return existing != null ? existing : dir.resolve(MANIFEST_FILE);
    }

    private WorkingSetResolver() {}

    /**
     * Resolve the working set from a starting directory.
     *
     * @param startDir the directory to resolve from (typically the CWD)
     * @return the declared working set when a manifest is found at or
     *         above {@code startDir}; otherwise the single-repository
     *         working set rooted at {@code startDir}
     */
    public static WorkingSet resolve(Path startDir) {
        Path manifest = findManifest(startDir);
        return manifest == null ? singleRepo(startDir) : declared(manifest);
    }

    /**
     * Build a single-repository working set rooted at {@code dir}, without
     * searching for a manifest — a working set of one.
     *
     * @param dir the repository directory
     * @return the single-repository working set
     */
    public static WorkingSet singleRepo(Path dir) {
        Path root = dir.toAbsolutePath().normalize();
        String name = fileName(root);
        return new WorkingSet(root, null, name,
                List.of(WorkingSet.Member.aggregator(name, root)));
    }

    private static WorkingSet declared(Path manifest) {
        Path root = manifest.getParent();
        Manifest model = ManifestReader.read(manifest);
        List<WorkingSet.Member> members = new ArrayList<>();
        for (String name : model.subprojects().keySet()) {
            members.add(WorkingSet.Member.subproject(name, root.resolve(name)));
        }
        members.add(WorkingSet.Member.aggregator(fileName(root), root));
        return new WorkingSet(root, manifest, baseName(model, root),
                List.copyOf(members));
    }

    /**
     * Resolve the working set's base name — the identity used for derived
     * names such as sibling directories. Prefers the manifest
     * {@code workspace-root:} {@code artifactId} (schema 1.1+ — the key
     * keeps its spelling; renaming a manifest key is a separate migration);
     * falls back to the root directory name when absent (legacy 1.0
     * manifests).
     *
     * @param model the parsed manifest
     * @param root  the working-set root directory
     * @return the declared root artifactId when present, else the directory
     *         name
     */
    private static String baseName(Manifest model, Path root) {
        WorkspaceRoot wr = model.workspaceRoot();
        if (wr != null && wr.artifactId() != null && !wr.artifactId().isBlank()) {
            return wr.artifactId();
        }
        return fileName(root);
    }

    /**
     * Search upward from {@code startDir} for a manifest, under either
     * name.
     *
     * @param startDir the directory to start the upward search from
     * @return the absolute manifest path, or {@code null} if none is found
     */
    static Path findManifest(Path startDir) {
        Path dir = startDir.toAbsolutePath().normalize();
        while (dir != null) {
            Path candidate = manifestIn(dir);
            if (candidate != null) {
                return candidate;
            }
            dir = dir.getParent();
        }
        return null;
    }

    private static String fileName(Path dir) {
        Path name = dir.getFileName();
        return name == null ? dir.toString() : name.toString();
    }
}