All files / src/utils projectIdentifier.ts

100% Statements 27/27
100% Branches 16/16
100% Functions 6/6
100% Lines 27/27

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 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117                                            67x             78x                   9x 68x     68x 8x                   60x   4x     56x     60x                       9x 62x 62x             9x 59x             9x 15x 4x     11x 11x 1x       10x 3x       7x     7x 1x     6x    
/**
 * Smart project identifier handling utilities
 * Handles both numeric IDs and namespace paths (URL-encoded or not)
 */
 
/**
 * Smart project identifier that can be numeric ID or namespace path
 */
export interface ProjectIdentifier {
  /** The normalized project identifier for API calls */
  identifier: string;
  /** Whether this is a numeric ID (true) or namespace path (false) */
  isNumericId: boolean;
  /** Original input value */
  originalValue: string;
}
 
/**
 * Detects if a string is URL-encoded by checking for common encoded characters
 */
function isUrlEncoded(str: string): boolean {
  // Check for common URL encoded patterns: %20, %2F, %3A, etc.
  return /%[0-9A-Fa-f]{2}/.test(str);
}
 
/**
 * Detects if a string is a numeric ID
 */
function isNumericId(str: string): boolean {
  return /^\d+$/.test(str.trim());
}
 
/**
 * Smart processing of project identifier that handles:
 * - Numeric IDs: "123" -> stays as "123"
 * - URL-encoded paths: "group%2Fproject" -> stays as "group%2Fproject" (for API)
 * - Regular paths: "group/project" -> converts to "group%2Fproject"
 * - Already properly encoded paths: detected and preserved
 */
export function processProjectIdentifier(input: string): ProjectIdentifier {
  const trimmedInput = input.trim();
 
  // Check if it's a numeric ID
  if (isNumericId(trimmedInput)) {
    return {
      identifier: trimmedInput,
      isNumericId: true,
      originalValue: input,
    };
  }
 
  // For namespace paths, we need to ensure proper encoding for GitLab API
  let identifier: string;
 
  if (isUrlEncoded(trimmedInput)) {
    // Already URL-encoded, use as-is (avoid double encoding)
    identifier = trimmedInput;
  } else {
    // Regular path, needs URL encoding
    identifier = encodeURIComponent(trimmedInput);
  }
 
  return {
    identifier,
    isNumericId: false,
    originalValue: input,
  };
}
 
/**
 * Safe URL component encoding that avoids double-encoding
 * If input is already encoded, returns as-is
 * If input needs encoding, applies encodeURIComponent
 */
export function safeEncodeProjectId(projectId: string): string {
  const processed = processProjectIdentifier(projectId);
  return processed.identifier;
}
 
/**
 * Normalize project identifier for API calls
 * Ensures the identifier is properly formatted for GitLab REST API
 */
export function normalizeProjectId(projectId: string): string {
  return safeEncodeProjectId(projectId);
}
 
/**
 * Validate project identifier format
 * Returns error message if invalid, null if valid
 */
export function validateProjectIdentifier(input: string): string | null {
  if (!input || typeof input !== "string") {
    return "Project identifier is required and must be a string";
  }
 
  const trimmed = input.trim();
  if (trimmed.length === 0) {
    return "Project identifier cannot be empty";
  }
 
  // Check if it's numeric ID
  if (isNumericId(trimmed)) {
    return null; // Valid numeric ID
  }
 
  // Check if it's a valid namespace path (with or without encoding)
  const decoded = isUrlEncoded(trimmed) ? decodeURIComponent(trimmed) : trimmed;
 
  // Basic validation for namespace/project format
  if (!/^[a-zA-Z0-9\-_./]+$/.test(decoded)) {
    return "Invalid project identifier format. Use numeric ID or namespace/project path.";
  }
 
  return null; // Valid
}