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 workspace operation acts
 * on, from a starting directory (IKE-Network/ike-issues#609, under #601).
 *
 * <p>Searches upward from the start directory for a {@code workspace.yaml}.
 * When one is found, the working set is that workspace — its declared
 * subprojects plus the workspace 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 workspace, 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 searched for when walking up from a directory. */
    public static final String MANIFEST_FILE = "workspace.yaml";

    private WorkingSetResolver() {}

    /**
     * Resolve the working set from a starting directory.
     *
     * @param startDir the directory to resolve from (typically the CWD)
     * @return a workspace working set when a {@code workspace.yaml} 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) : workspace(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 workspace(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+); falls back to
     * the root directory name when absent (legacy 1.0 manifests).
     *
     * @param model the parsed manifest
     * @param root  the workspace root directory
     * @return the workspace-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 {@code workspace.yaml}.
     *
     * @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 = dir.resolve(MANIFEST_FILE);
            if (candidate.toFile().exists()) {
                return candidate;
            }
            dir = dir.getParent();
        }
        return null;
    }

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