@ 261105.4
current
import { flatten } from '../utils.js';

const COMMIT_QUERY = 'LastCommit';
const TREE_QUERY = 'TreeEntries';

export default class GitHubTree {
  constructor(cli, request, argv) {
    this.cli = cli;
    this.request = request;

    this.owner = argv.owner;
    this.repo = argv.repo;
    this.branch = argv.branch;

    if (argv.path.endsWith('/')) {
      this.path = argv.path.slice(0, argv.path - 1);
    } else {
      this.path = argv.path;
    }

    this.lastCommit = argv.commit || null;
  }

  get repoUrl() {
    const base = 'https://github.com';
    const { owner, repo } = this;
    return `${base}/${owner}/${repo}`;
  }

  async _getLastCommit() {
    const { request, owner, repo, branch, path } = this;
    const data = await request.gql(COMMIT_QUERY, {
      owner,
      repo,
      branch,
      path
    });

    const targetHistoryNodes = data.repository.ref.target.history.nodes;
    if (!targetHistoryNodes.length) {
      this.cli.stopSpinner(
        `Cannot find commit for "${path}". Please check the path name.`,
        this.cli.SPINNER_STATUS.FAILED
      );
      throw new Error(`Cannot find commit for "${path}"`);
    }
    return targetHistoryNodes[0].oid;
  }

  /**
   * @returns {string} the hash of the last commit in the tree
   */
  async getLastCommit() {
    if (this.lastCommit) {
      return this.lastCommit;
    }
    this.lastCommit = await this._getLastCommit();
    return this.lastCommit;
  }

  getPermanentUrl() {
    if (!this.lastCommit) {
      throw new Error('Call await tree.getLastCommit() first');
    }
    const commit = this.lastCommit;
    return `${this.repoUrl}/tree/${commit.slice(0, 10)}/${this.path}`;
  }

  async buffer(assetPath) {
    await this.getLastCommit();
    const url = this.getAssetUrl(assetPath);
    return this.request.buffer(url);
  }

  /**
   * Get the url of an asset. If the assetPath starts with `/`,
   * it will be treated as an absolute path and the
   * the path of the tree will not be prefixed in the url.
   * @param {string} assetPath
   */
  getAssetUrl(assetPath) {
    if (!this.lastCommit) {
      throw new Error('Call await tree.getLastCommit() first');
    }
    const base = 'https://raw.githubusercontent.com';
    const { owner, repo, lastCommit, path } = this;
    const prefix = `${base}/${owner}/${repo}/${lastCommit}`;
    if (assetPath.startsWith('/')) {  // absolute
      return `${prefix}/${assetPath}`;
    } else {
      return `${prefix}/${path}/${assetPath}`;
    }
  }

  /**
   * Get a list of files inside the tree (recursively).
   * The returned file names will be relative to the path of the tree,
   * e.g. `url/resources/data.json` in a tree with `url` as path
   * will be `resources/data.json`
   *
   * @returns {{name: string, type: string}[]}
   */
  async getFiles(path) {
    if (!path) {
      path = this.path;
    }
    let lastCommit = this.lastCommit;
    if (!lastCommit) {
      lastCommit = await this.getLastCommit();
    }
    const { request, owner, repo } = this;

    const expression = `${lastCommit}:${path}`;
    this.cli.updateSpinner(`Querying files for ${path}`);
    const data = await request.gql(TREE_QUERY, {
      owner,
      repo,
      expression
    });
    const files = data.repository.object.entries;

    const dirs = files.filter((file) => file.type === 'tree');
    const nondirs = files.filter((file) => file.type !== 'tree');

    if (dirs.length) {
      const expanded = await Promise.all(
        dirs.map((dir) =>
          this.getFiles(`${path}/${dir.name}`)
            .then(files => files.map(
              ({ name, type }) => ({ name: `${dir.name}/${name}`, type })
            ))
        )
      );
      return nondirs.concat(flatten(expanded));
    } else {
      return nondirs;
    }
  }

  getCacheKey() {
    const { branch, owner, repo, path } = this;
    return `tree-${owner}-${repo}-${branch}-${clean(path)}`;
  }
}

function clean(path) {
  if (!path) {
    return '';
  }
  return path.replace('/', '-');
}

// Uncomment this when testing to avoid extra network costs
// import Cache from '../cache.js';
// const treeCache = new Cache();

// treeCache.wrap(GitHubTree, {
//   _getLastCommit() {
//     return { key: `${this.getCacheKey()}-commit`, ext: '.json' };
//   },
//   getFiles(path) {
//     return {
//       key: `${this.getCacheKey()}-${clean(path)}-files`,
//       ext: '.json'
//     };
//   },
//   text(assetPath) {
//     return { key: `${this.getCacheKey()}-${clean(assetPath)}`, ext: '.txt' };
//   }
// });
// treeCache.enable();