All files / src/utils namespace.ts

100% Statements 35/35
100% Branches 18/18
100% Functions 5/5
100% Lines 31/31

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 10235x                           35x 43x 1x     42x     42x 4x       38x             35x 95x             35x   95x   77x 77x   4x 4x     2x     18x 18x   2x 2x     1x                     101x 101x 101x   101x   99x     2x               35x       87x 87x          
import { enhancedFetch } from "./fetch";
 
/**
 * Extract namespace (group path) from a full project path.
 *
 * Examples:
 * - "group/project" -> "group"
 * - "group/subgroup/project" -> "group/subgroup"
 * - "myproject" (single segment) -> "myproject" (root-level project)
 * - "" (empty) -> undefined
 *
 * @param projectPath - Full project path (e.g., "group/project")
 * @returns Namespace path or undefined if projectPath is empty
 */
export function extractNamespaceFromPath(projectPath: string): string | undefined {
  if (!projectPath) {
    return undefined;
  }
 
  const pathParts = projectPath.split("/");
 
  // Single segment = root-level project, namespace equals project path
  if (pathParts.length === 1) {
    return projectPath;
  }
 
  // Multiple segments = namespace is everything except the last part
  return pathParts.slice(0, -1).join("/");
}
 
/**
 * Simple heuristic to determine if a path likely represents a project
 * Projects typically contain a slash (group/project), while groups usually don't
 */
export function isLikelyProjectPath(namespacePath: string): boolean {
  return namespacePath.includes("/");
}
 
/**
 * Detect namespace type by attempting to fetch from GitLab API
 * Tries both project and group endpoints to determine which one exists
 */
export async function detectNamespaceType(namespacePath: string): Promise<"project" | "group"> {
  // First try heuristic for common cases
  if (isLikelyProjectPath(namespacePath)) {
    // Try project first, fallback to group if needed
    const isProject = await verifyNamespaceType(namespacePath, "project");
    if (isProject) return "project";
 
    const isGroup = await verifyNamespaceType(namespacePath, "group");
    if (isGroup) return "group";
 
    // Default fallback for paths with slash
    return "project";
  } else {
    // Try group first, fallback to project if needed
    const isGroup = await verifyNamespaceType(namespacePath, "group");
    if (isGroup) return "group";
 
    const isProject = await verifyNamespaceType(namespacePath, "project");
    if (isProject) return "project";
 
    // Default fallback for paths without slash
    return "group";
  }
}
 
/**
 * Verify if a namespace exists as the specified type by making a lightweight API call
 */
async function verifyNamespaceType(
  namespacePath: string,
  type: "project" | "group"
): Promise<boolean> {
  try {
    const entityType = type === "project" ? "projects" : "groups";
    const apiUrl = `${process.env.GITLAB_API_URL}/api/v4/${entityType}/${encodeURIComponent(namespacePath)}`;
 
    const response = await enhancedFetch(apiUrl);
 
    return response.ok;
  } catch {
    // If API call fails, return false
    return false;
  }
}
 
/**
 * Determine the appropriate entity type and path for GitLab API calls
 * Returns the entity type ('projects' or 'groups') and ensures proper encoding
 */
export async function resolveNamespaceForAPI(namespacePath: string): Promise<{
  entityType: "projects" | "groups";
  encodedPath: string;
}> {
  const namespaceType = await detectNamespaceType(namespacePath);
  return {
    entityType: namespaceType === "project" ? "projects" : "groups",
    encodedPath: encodeURIComponent(namespacePath),
  };
}