Source: data.js

'use strict';


// user defined modules
const { Client } = require('./client.js');
const { Clone } = require('./clone.js');
const { parallelAnalysis } = require('./parallel.js');
const utils = require('./utils.js');
const mkLogger = require('./log.js');
const logger = mkLogger({label: __filename});


/**
 *  @class The Data class is used as the central point where all raw
 *  data is fetched from. It holds a client connection to the GitHub project
 *  and a cloned instance of the Git repository.
 */
class Data {
    static async init(url, db, options) {
        const clone = await Clone.init(url);
        return new Data({
            url,
            db: await db,
            clone,
            options,
        });
    }

    /**
     *  Construct a {@link Data} object, create a [clone]{@link Clone} from the
     *  Git repository asynchronously, and initialize
     *  [GitHub API]{@link https://developer.github.com/v4/} [client]{@link Client}
     *
     *  @param {string} url - GitHub project URL
     *  @param {object} options - Configuration arguments
     *  @param {string} options.auth_token - The GitHub authentication token
     *  @param {boolean} options.noClone - Flag on whether to clone the Git project or not
     */
    constructor({url, db, clone, options} = {}) {
        // Parse the GitHub URL into project owner and project name
        const {owner, name} = utils.parseURL(url);
        this.client = new Client({
            owner,
            name,
            auth_token: (options || {}).auth_token,
        });
        this.clone = clone
        this.owner = owner;
        this.name = name;
        this.db = db;
    }

    async analyse(options) {
        // ensure repo is in database
        const repo_id = await this.db.getRepoId({
            owner: this.owner, 
            name: this.name
        });

        // get last commit in database
        const lastCommit = await this.db.getLastCommit(repo_id);

        // get new commits 
        const newCommits = await this.clone.commitsAfter(lastCommit ? lastCommit.commit_date : null, options.quick);

        // get the meta analysis for project
        const newMeta = this.client.getMetaAnalysis(
            newCommits.map(c => ({
                commit_id: c.id().tostrS(),
                commit_date: c.date(),
            }))
        );

        // begin static analysis of new commits
        // analyse in parallel if many commits
        const newStatic = newCommits.length < 10 ?
            this.clone.analyseCommits({commits: newCommits}) :
            parallelAnalysis(this, newCommits.map(c => c.id().tostrS()));

        // merge static and meta analysis
        const newAnalyses = Promise.all([newStatic, newMeta])
            .then(([static_, meta]) => {
                // new analyses is a list of objects
                // with keys: commit_id, commit_date, and valuesByExt
                // where valuesByExt is an object with different file extensions as keys
                // and objects of metric types as keys and metric values as values

                let results = [];

                // convert it into a flat list of objects
                // where each object key corresponds to a column in the table MetricValues
                for (const {commit_id, commit_date, valuesByExt} of static_) {
                    for (const [ext, staticValues] of Object.entries(valuesByExt)) {

                        // merge metrics
                        const metricValues = {...staticValues, ...meta[commit_id]};
                        
                        // expand into separate rows
                        for (const [type, value] of Object.entries(metricValues)) {
                            // convert date string into UNIX timestamp
                            const timestamp = Date.parse(commit_date);
                            results.push({
                                repo_id: repo_id,
                                commit_id: commit_id,
                                commit_date: timestamp,
                                file_extension: ext,
                                metric_type: type,
                                metric_value: value,
                            });
                        }
                    }
                }

                return results;
            });

        // insert new analysis results into database
        if (!options.quick) {
            newAnalyses.then(values => this.db.insertValues(values))
                .then(() => logger.debug('Finished inserting values to database'));
        }

        // get already analysed commits if present, otherwise empty list
        const oldAnalyses = this.db.getValuesUntil(repo_id, lastCommit);

        // merge new and old analyses
        const results = await Promise.all([oldAnalyses, newAnalyses])
            .then(([oldAnalyses, newAnalyses]) => {
                // old analyses serves as the start for our results
                // after we can append the new results to it
                let results = [...oldAnalyses, ...newAnalyses];
                console.log(`# of old results: ${oldAnalyses.length}`);
                console.log(`# of new results: ${newAnalyses.length}`);
                return results;
            });

        const points = utils.rows2points(results);

        return points;
    }
}

module.exports = {
    Data: Data
};