"use strict";
/*
 * Copyright © 2020 Atomist, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.fileContent = exports.GitHubLazyProjectLoader = void 0;
const configuration_1 = require("@atomist/automation-client/lib/configuration");
const base64_1 = require("@atomist/automation-client/lib/internal/util/base64");
const GitHubRepoRef_1 = require("@atomist/automation-client/lib/operations/common/GitHubRepoRef");
const NodeFsLocalFile_1 = require("@atomist/automation-client/lib/project/local/NodeFsLocalFile");
const InMemoryFile_1 = require("@atomist/automation-client/lib/project/mem/InMemoryFile");
const AbstractProject_1 = require("@atomist/automation-client/lib/project/support/AbstractProject");
const httpClient_1 = require("@atomist/automation-client/lib/spi/http/httpClient");
const logger_1 = require("@atomist/automation-client/lib/util/logger");
const fg = require("fast-glob");
const stream = require("stream");
const CachingProjectLoader_1 = require("./CachingProjectLoader");
/**
 * Create a lazy view of the given project GitHub, which will materialize
 * the remote project (usually by cloning) only if needed.
 */
class GitHubLazyProjectLoader {
    constructor(delegate) {
        this.delegate = delegate;
    }
    doWithProject(params, action) {
        const lazyProject = new GitHubLazyProject(params.id, this.delegate, params);
        return action(lazyProject);
    }
    get isLazy() {
        return true;
    }
}
exports.GitHubLazyProjectLoader = GitHubLazyProjectLoader;
/**
 * Lazy project that loads remote GitHub project and forwards to it only if necessary.
 */
class GitHubLazyProject extends AbstractProject_1.AbstractProject {
    constructor(id, delegate, params) {
        super(id);
        this.delegate = delegate;
        this.params = params;
        this.release = () => Promise.resolve();
        this.branch = this.id.branch;
        this.newRepo = false;
        this.remote = this.id.url;
    }
    materializing() {
        return !!this.projectPromise;
    }
    materialized() {
        return !!this.projectPromise && !!this.projectPromise.result();
    }
    materialize() {
        this.materializeIfNecessary("materialize");
        return this.projectPromise.then(mp => mp.gitStatus());
    }
    get provenance() {
        return this.materialized() ? this.projectPromise.result().provenance : "unavailable";
    }
    get baseDir() {
        if (!this.materialized()) {
            throw new Error("baseDir not supported until materialized");
        }
        return this.projectPromise.result().baseDir;
    }
    addDirectory(path) {
        this.materializeIfNecessary(`addDirectory${path}`);
        return this.projectPromise.then(mp => mp.addDirectory(path));
    }
    hasDirectory(path) {
        this.materializeIfNecessary(`hasDirectory${path}`);
        return this.projectPromise.then(mp => mp.hasDirectory(path));
    }
    addFile(path, content) {
        this.materializeIfNecessary(`addFile(${path})`);
        return this.projectPromise.then(mp => mp.addFile(path, content));
    }
    addFileSync(path, content) {
        throw new Error("sync methods not supported");
    }
    deleteDirectory(path) {
        this.materializeIfNecessary(`deleteDirectory(${path})`);
        return this.projectPromise.then(mp => mp.deleteDirectory(path));
    }
    deleteDirectorySync(path) {
        throw new Error("sync methods not supported");
    }
    deleteFile(path) {
        this.materializeIfNecessary(`deleteFile(${path})`);
        return this.projectPromise.then(mp => mp.deleteFile(path));
    }
    deleteFileSync(path) {
        throw new Error("sync methods not supported");
    }
    directoryExistsSync(path) {
        throw new Error("sync methods not supported");
    }
    fileExistsSync(path) {
        throw new Error("sync methods not supported");
    }
    async findFile(path) {
        const file = await this.getFile(path);
        if (!file) {
            throw new Error(`No file found: '${path}'`);
        }
        return file;
    }
    findFileSync(path) {
        throw new Error("sync methods not supported");
    }
    async getFile(path) {
        if (this.materializing()) {
            return this.projectPromise.then(mp => mp.getFile(path));
        }
        if (GitHubRepoRef_1.isGitHubRepoRef(this.id)) {
            const content = await fileContent(this.params.credentials.token, this.id, path);
            return !!content ? new InMemoryFile_1.InMemoryFile(path, content) : undefined;
        }
        this.materializeIfNecessary(`getFile(${path})`);
        return this.projectPromise.then(mp => mp.getFile(path));
    }
    makeExecutable(path) {
        this.materializeIfNecessary(`makeExecutable(${path})`);
        return this.projectPromise.then(mp => mp.makeExecutable(path));
    }
    makeExecutableSync(path) {
        throw new Error("sync methods not supported");
    }
    streamFilesRaw(globPatterns, opts) {
        const resultStream = new stream.Transform({ objectMode: true });
        resultStream._transform = function (chunk, encoding, done) {
            this.push(chunk);
            done();
        };
        this.materializeIfNecessary(`streamFilesRaw`)
            .then(async () => {
            const underlyingStream = await this.projectPromise.then(mp => mp.streamFilesRaw(globPatterns, opts));
            // tslint:disable-next-line:no-floating-promises
            underlyingStream.pipe(resultStream);
        });
        return resultStream;
    }
    checkout(sha) {
        this.materializeIfNecessary(`checkout(${sha})`);
        return this.projectPromise.then(mp => mp.checkout(sha));
    }
    commit(message) {
        this.materializeIfNecessary(`commit(${message})`);
        return this.projectPromise.then(mp => mp.commit(message));
    }
    configureFromRemote() {
        this.materializeIfNecessary("configureFromRemote");
        return this.projectPromise.then(mp => mp.configureFromRemote());
    }
    createAndSetRemote(gid, description, visibility) {
        this.materializeIfNecessary("createAndSetRemote");
        return this.projectPromise.then(mp => mp.createAndSetRemote(gid, description, visibility));
    }
    createBranch(name) {
        this.materializeIfNecessary(`createBranch(${name})`);
        return this.projectPromise.then(mp => mp.createBranch(name));
    }
    gitStatus() {
        this.materializeIfNecessary("gitStatus");
        return this.projectPromise.then(mp => mp.gitStatus());
    }
    hasBranch(name) {
        this.materializeIfNecessary(`hasBranch(${name})`);
        return this.projectPromise.then(mp => mp.hasBranch(name));
    }
    init() {
        this.materializeIfNecessary("init");
        return this.projectPromise.then(mp => mp.init());
    }
    isClean() {
        this.materializeIfNecessary("isClean");
        return this.projectPromise.then(mp => mp.isClean());
    }
    push(options) {
        this.materializeIfNecessary("push");
        return this.projectPromise.then(mp => mp.push(options));
    }
    raisePullRequest(title, body, targetBranch) {
        this.materializeIfNecessary("raisePullRequest");
        return this.projectPromise.then(mp => mp.raisePullRequest(title, body, targetBranch));
    }
    revert() {
        this.materializeIfNecessary("revert");
        return this.projectPromise.then(mp => mp.revert());
    }
    setRemote(remote) {
        this.materializeIfNecessary("setRemote");
        return this.projectPromise.then(mp => mp.setRemote(remote));
    }
    setUserConfig(user, email) {
        this.materializeIfNecessary("setUserConfig");
        return this.projectPromise.then(mp => mp.setUserConfig(user, email));
    }
    async getFilesInternal(globPatterns) {
        await this.materializeIfNecessary("getFilesInternal");
        const optsToUse = {
            cwd: this.baseDir,
            dot: true,
            onlyFiles: true,
        };
        const paths = await fg(globPatterns, optsToUse);
        const files = paths.map(path => new NodeFsLocalFile_1.NodeFsLocalFile(this.baseDir, path));
        return files;
    }
    materializeIfNecessary(why) {
        if (!this.materializing()) {
            logger_1.logger.debug("Materializing project %j because of %s", this.id, why);
            this.projectPromise = makeQueryablePromise(CachingProjectLoader_1.save(this.delegate, this.params));
        }
        return this.projectPromise;
    }
}
/**
 * Based on https://ourcodeworld.com/articles/read/317/how-to-check-if-a-javascript-promise-has-been-fulfilled-rejected-or-resolved
 * This function allow you to modify a JS Promise by adding some status properties.
 * Based on: http://stackoverflow.com/questions/21485545/is-there-a-way-to-tell-if-an-es6-promise-is-fulfilled-rejected-resolved
 * But modified according to the specs of promises : https://promisesaplus.com/
 */
function makeQueryablePromise(ppromise) {
    const promise = ppromise;
    // Don't modify any promise that has been already modified.
    if (promise.isResolved) {
        return promise;
    }
    // Set initial state
    let isPending = true;
    let isRejected = false;
    let isFulfilled = false;
    let result;
    // Observe the promise, saving the fulfillment in a closure scope.
    const qp = promise.then(v => {
        isFulfilled = true;
        isPending = false;
        result = v;
        return v;
    }, e => {
        isRejected = true;
        isPending = false;
        throw e;
    });
    qp.isFulfilled = () => {
        return isFulfilled;
    };
    qp.isPending = () => {
        return isPending;
    };
    qp.isRejected = () => {
        return isRejected;
    };
    qp.result = () => {
        return result;
    };
    return qp;
}
async function fileContent(token, rr, path) {
    try {
        const result = await filePromise(token, rr, path);
        return base64_1.decode(result.body.content);
    }
    catch (e) {
        logger_1.logger.debug(`File at '${path}' not available`);
        return undefined;
    }
}
exports.fileContent = fileContent;
async function filePromise(token, rr, path) {
    const url = `${rr.scheme}${rr.apiBase}/repos/${rr.owner}/${rr.repo}/contents/${path}?ref=${rr.branch || "master"}`;
    logger_1.logger.debug(`Requesting file from GitHub at '${url}'`);
    const httpClient = configuration_1.configurationValue("http.client.factory", httpClient_1.defaultHttpClientFactory());
    return httpClient.create(url).exchange(url, {
        method: httpClient_1.HttpMethod.Get,
        headers: {
            Authorization: `token ${token}`,
        },
        retry: {
            retries: 0,
        },
    });
}
//# sourceMappingURL=GitHubLazyProjectLoader.js.map