Source: github/container.js

/**
 * @fileoverview Container operations for GitHub
 * @license Apache-2.0
 * @version 3.0.0
 * @author Michael Hay <michael.hay@mediumroast.io>
 * @copyright 2025 Mediumroast, Inc. All rights reserved.
 */

import ResponseFactory from './response.js';
import { encodeContent } from './utils.js';

/**
 * Manages container operations (locking, object manipulation)
 */
class ContainerOperations {
  /**
   * @constructor
   * @param {Object} octokit - Octokit instance
   * @param {String} orgName - GitHub organization name
   * @param {String} repoName - GitHub repository name
   * @param {String} mainBranchName - Main branch name
   * @param {String} lockFileName - Lock file name
   */
  constructor(octokit, orgName, repoName, mainBranchName, lockFileName) {
    this.octokit = octokit;
    this.orgName = orgName;
    this.repoName = repoName;
    this.mainBranchName = mainBranchName;
    this.lockFileName = lockFileName;
  }

  /**
   * Checks if a container is locked
   * @param {String} containerName - Container name
   * @returns {Promise<Array>} ResponseFactory result
   */
  async checkForLock(containerName) {
    try {
      // Get the latest commit
      const latestCommit = await this.octokit.rest.repos.getCommit({
        owner: this.orgName,
        repo: this.repoName,
        ref: this.mainBranchName,
      });

      // Check if the lock file exists
      const mainContents = await this.octokit.rest.repos.getContent({
        owner: this.orgName,
        repo: this.repoName,
        ref: latestCommit.data.sha,
        path: containerName
      });

      const lockExists = mainContents.data.some(
        item => item.path === `${containerName}/${this.lockFileName}`
      );

      if (lockExists) {
        return ResponseFactory.success(
          `Container ${containerName} is locked with lock file ${this.lockFileName}`,
          lockExists,
          200
        );
      } else {
        return ResponseFactory.success(
          `Container ${containerName} is not locked with lock file ${this.lockFileName}`,
          lockExists,
          404
        );
      }
    } catch (err) {
      return ResponseFactory.error(
        `Failed to check if container ${containerName} is locked: ${err.message}`,
        err
      );
    }
  }

  /**
   * Locks a container
   * @param {String} containerName - Container name
   * @returns {Promise<Array>} ResponseFactory result
   */
  async lockContainer(containerName) {
    // Define the full path to the lockfile
    const lockFile = `${containerName}/${this.lockFileName}`;

    try {
      // Get the latest commit
      const { data: latestCommit } = await this.octokit.rest.repos.getCommit({
        owner: this.orgName,
        repo: this.repoName,
        ref: this.mainBranchName,
      });

      const lockResponse = await this.octokit.rest.repos.createOrUpdateFileContents({
        owner: this.orgName,
        repo: this.repoName,
        path: lockFile,
        content: encodeContent(''),
        branch: this.mainBranchName,
        message: `Locking container [${containerName}]`,
        sha: latestCommit.sha
      });

      return ResponseFactory.success(
        `Locked the container ${containerName}`, 
        lockResponse.data
      );
    } catch (err) {
      return ResponseFactory.error(
        `Unable to lock the container ${containerName}: ${err.message}`, 
        err
      );
    }
  }

  /**
   * Unlocks a container
   * @param {String} containerName - Container name
   * @param {String} commitSha - SHA of the lock file
   * @param {String} branchName - Branch name
   * @returns {Promise<Array>} ResponseFactory result
   */
  async unlockContainer(containerName, commitSha, branchName) {
    // Define the full path to the lockfile
    const lockFile = `${containerName}/${this.lockFileName}`;
    
    try {
      const lockExists = await this.checkForLock(containerName);

      if (lockExists[0] && lockExists[2]) {
        const unlockResponse = await this.octokit.rest.repos.deleteFile({
          owner: this.orgName,
          repo: this.repoName,
          path: lockFile,
          branch: branchName,
          message: `Unlocking container [${containerName}]`,
          sha: commitSha
        });

        return ResponseFactory.success(
          `Unlocked the container ${containerName}`, 
          unlockResponse.data
        );
      } else {
        return ResponseFactory.error(
          `Unable to unlock the container ${containerName}: Lock file not found`, 
          null
        );
      }
    } catch (err) {
      return ResponseFactory.error(
        `Error unlocking container ${containerName}: ${err.message}`, 
        err
      );
    }
  }

  /**
   * Catches multiple containers (locks them and prepares for operations)
   * @param {Object} repoMetadata - Container metadata object
   * @param {Object} objectFiles - Mapping of container names to their object files
   * @param {Function} createBranchFn - Function to create a branch
   * @param {Function} readObjectsFn - Function to read container objects
   * @returns {Promise<Array>} ResponseFactory result
   */
  async catchContainers(repoMetadata, objectFiles, createBranchFn, readObjectsFn) {
    // Check locks
    for (const container in repoMetadata.containers) {
      const lockExists = await this.checkForLock(container);
      if (lockExists[0] && lockExists[2]) {
        return ResponseFactory.error(
          `The container [${container}] is locked unable and cannot perform creates, updates or deletes on objects.`,
          lockExists,
          503
        );
      }
    }

    // Lock containers
    for (const container in repoMetadata.containers) {
      const locked = await this.lockContainer(container);
      if (!locked[0]) {
        return ResponseFactory.error(
          `Unable to lock [${container}] and cannot perform creates, updates or deletes on objects.`,
          locked,
          503
        );
      }
      repoMetadata.containers[container].lockSha = locked[2].content.sha;
    }

    // Create branch
    const branchCreated = await createBranchFn();
    if (!branchCreated[0]) {
      return ResponseFactory.error(
        'Unable to create new branch',
        branchCreated,
        503
      );
    }
    
    // Extract branch ref (remove 'refs/heads/' prefix)
    const branchRef = branchCreated[2].ref.replace('refs/heads/', '');
    
    repoMetadata.branch = {
      name: branchRef,
      sha: branchCreated[2].object.sha
    };

    // Read objects
    for (const container in repoMetadata.containers) {
      const readResponse = await readObjectsFn(container);
      if (!readResponse[0]) {
        return ResponseFactory.error(
          `Unable to read the source objects [${container}/${objectFiles[container]}].`,
          readResponse,
          503
        );
      }
      
      repoMetadata.containers[container].objectSha = readResponse[2].sha;
      repoMetadata.containers[container].objects = readResponse[2].mrJson;
    }

    return ResponseFactory.success(
      `${Object.keys(repoMetadata.containers).length} containers are ready for use.`,
      repoMetadata,
      200
    );
  }

  /**
   * Releases containers (unlocks them and merges changes)
   * @param {Object} repoMetadata - Container metadata object
   * @param {Function} mergeBranchFn - Function to merge branch to main
   * @returns {Promise<Array>} ResponseFactory result
   */
  async releaseContainers(repoMetadata, mergeBranchFn) {
    // Merge branch to main
    const mergeResponse = await mergeBranchFn(
      repoMetadata.branch.name, 
      repoMetadata.branch.sha
    );
    
    if (!mergeResponse[0]) {
      return ResponseFactory.error(
        'Unable to merge the branch to main.',
        mergeResponse,
        503
      );
    }

    // Unlock containers
    for (const container in repoMetadata.containers) {
      // Unlock branch
      const branchUnlocked = await this.unlockContainer(
        container, 
        repoMetadata.containers[container].lockSha,
        repoMetadata.branch.name
      );
      if (!branchUnlocked[0]) {
        return ResponseFactory.error(
          `Unable to unlock the container, objects may have been written please check [${container}] for objects and the lock file.`,
          branchUnlocked,
          503
        );
      }
      
      // Unlock main
      const mainUnlocked = await this.unlockContainer(
        container, 
        repoMetadata.containers[container].lockSha,
        this.mainBranchName
      );
      if (!mainUnlocked[0]) {
        return ResponseFactory.error(
          `Unable to unlock the container, objects may have been written please check [${container}] for objects and the lock file.`,
          mainUnlocked,
          503
        );
      }
    }

    // Return success
    return ResponseFactory.success(
      `Released [${Object.keys(repoMetadata.containers).length}] containers.`,
      null,
      200
    );
  }
}

export default ContainerOperations;