cksource/mgit2

View on GitHub
lib/commands/status.js

Summary

Maintainability
D
1 day
Test Coverage
/**
 * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md.
 */

'use strict';

const chalk = require( 'chalk' );
const Table = require( 'cli-table' );
const gitStatusParser = require( '../utils/gitstatusparser' );
const { addRootRepositorySuffix } = require( '../utils/rootrepositoryutils' );

module.exports = {
    name: 'status',

    get helpMessage() {
        const {
            underline: u
        } = chalk;

        return `
    ${ u( 'Description:' ) }
        Prints a useful table that contains status of every repository. It displays:

            * current branch,
            * whether current branch is equal to specified in "mrgit.json" file,
            * whether current branch is behind or ahead with the remote,
            * current commit short hash,
            * how many files is staged, modified and untracked.
        `;
    },

    beforeExecute() {
        console.log( chalk.blue( 'Collecting statuses...' ) );
    },

    /**
     * @param {CommandData} data
     * @returns {Promise}
     */
    async execute( data ) {
        const execCommand = require( './exec' );

        let latestTag = null;
        let currentTag = null;
        let packageName = data.packageName;

        const hashResponse = await execCommand.execute( getExecData( 'git rev-parse HEAD' ) );
        const currentBranchStatusResponse = await execCommand.execute( getExecData( 'git status --branch --porcelain' ) );
        const latestTagStatusResponse = await execCommand.execute( getExecData( 'git log --tags --simplify-by-decoration --pretty="%S"' ) );

        if ( latestTagStatusResponse.logs.info.length ) {
            const currentTagStatusResponse = await execCommand.execute( getExecData( 'git describe --abbrev=0 --tags' ) );

            latestTag = latestTagStatusResponse.logs.info[ 0 ].trim().split( '\n' ).shift();
            currentTag = currentTagStatusResponse.logs.info[ 0 ];
        }

        for ( const packagePrefix of data.toolOptions.packagesPrefix ) {
            packageName = packageName.replace( new RegExp( '^' + packagePrefix ), '' );
        }

        if ( data.isRootRepository ) {
            packageName = addRootRepositorySuffix( packageName, { bold: true } );
        }

        const commandResponse = {
            packageName,
            isRootRepository: data.isRootRepository,
            status: gitStatusParser( currentBranchStatusResponse.logs.info[ 0 ], currentTag ),
            commit: hashResponse.logs.info[ 0 ].slice( 0, 7 ), // Short version of the commit hash.
            mrgitBranch: data.repository.branch,
            mrgitTag: data.repository.tag,
            latestTag
        };

        return { response: commandResponse };

        function getExecData( command ) {
            return Object.assign( {}, data, {
                arguments: [ command ]
            } );
        }
    },

    /**
     * Saves collected hashes to configuration file.
     *
     * @param {Set} processedPackages Collection of processed packages.
     * @param {Set} commandResponses Results of executed command for each package.
     */
    afterExecute( processedPackages, commandResponses ) {
        if ( !processedPackages.size || !commandResponses.size ) {
            return;
        }

        let shouldDisplayLatestHint = false;
        let shouldDisplaySyncHint = false;

        const table = new Table( {
            head: [ 'Package', 'Branch/Tag', 'Commit', 'Status' ],
            style: {
                compact: true
            }
        } );

        const packagesResponses = Array.from( commandResponses.values() )
            .sort( ( a, b ) => a.packageName.localeCompare( b.packageName ) )
            // The root package should be at the top.
            .sort( ( a, b ) => b.isRootRepository - a.isRootRepository );

        for ( const singleResponse of packagesResponses ) {
            table.push( createSingleRow( singleResponse ) );
        }

        console.log( table.toString() );
        displayLegend();
        displayHints( shouldDisplayLatestHint, shouldDisplaySyncHint );

        function createSingleRow( data ) {
            const { packageName, status, commit, mrgitBranch, mrgitTag, latestTag } = data;
            const statusColumn = [];

            const shouldUseTag = mrgitTag !== undefined;
            const shouldUseLatestTag = mrgitTag === 'latest';
            let branchOrTag = !status.detachedHead ? status.branch : status.tag;

            // Unmerged files are also modified so we should print the number of them out.
            const modifiedFiles = [ status.modified, status.unmerged ]
                .reduce( ( sum, item ) => sum + item.length, 0 );

            if ( shouldUseTag && shouldUseLatestTag && status.detachedHead && status.tag === latestTag ) {
                branchOrTag = `${ chalk.green( 'L' ) } ${ branchOrTag }`;
                shouldDisplayLatestHint = true;
            }

            if ( shouldUseTag && shouldUseLatestTag && ( !status.detachedHead || status.tag !== latestTag ) ) {
                branchOrTag = `${ chalk.cyan( '!' ) } ${ branchOrTag }`;
                shouldDisplaySyncHint = true;
            }

            if ( shouldUseTag && !shouldUseLatestTag && status.tag !== mrgitTag ) {
                branchOrTag = `${ chalk.cyan( '!' ) } ${ branchOrTag }`;
                shouldDisplaySyncHint = true;
            }

            if ( !shouldUseTag && status.branch !== mrgitBranch && commit !== mrgitBranch ) {
                branchOrTag = `${ chalk.cyan( '!' ) } ${ branchOrTag }`;
                shouldDisplaySyncHint = true;
            }

            if ( !shouldUseTag && mrgitBranch === commit ) {
                branchOrTag = 'Using saved commit →';
            }

            if ( status.ahead ) {
                branchOrTag += chalk.yellow( ` ↑${ status.ahead }` );
            }

            if ( status.behind ) {
                branchOrTag += chalk.yellow( ` ↓${ status.behind }` );
            }

            if ( status.staged.length ) {
                statusColumn.push( chalk.green( `+${ status.staged.length }` ) );
            }

            if ( modifiedFiles ) {
                statusColumn.push( chalk.red( `M${ modifiedFiles }` ) );
            }

            if ( status.untracked.length ) {
                statusColumn.push( chalk.blue( `?${ status.untracked.length }` ) );
            }

            return [
                packageName,
                branchOrTag,
                commit,
                statusColumn.join( ' ' )
            ];
        }

        function displayLegend() {
            const legend = [
                `${ chalk.yellow( '↑' ) } branch is ahead ${ chalk.yellow( '↓' ) } or behind`,
                `${ chalk.green( '+' ) } staged files`,
                `${ chalk.red( 'M' ) } modified files`,
                `${ chalk.blue( '?' ) } untracked files`,
                `\n${ chalk.green( 'L' ) } latest tag`,
                `${ chalk.cyan( '!' ) } not on a branch or a tag specified in "mrgit.json"`
            ];

            console.log( `${ chalk.bold( 'Legend:' ) }\n${ legend.join( ', ' ) }.` );
        }

        function displayHints( shouldDisplayLatestHint, shouldDisplaySyncHint ) {
            const hints = [];

            if ( shouldDisplayLatestHint ) {
                hints.push( [
                    chalk.green( 'L' ),
                    'This is the latest local tag. To ensure having latest remote tag, execute',
                    chalk.blue( 'mrgit fetch' ),
                    'before checking status.'
                ].join( ' ' ) );
            }

            if ( shouldDisplaySyncHint ) {
                hints.push( [
                    chalk.cyan( '!' ),
                    'In order to bring your repositories up to date, execute',
                    chalk.blue( 'mrgit sync' )
                ].join( ' ' ) + '.' );
            }

            if ( !hints.length ) {
                return;
            }

            console.log( `\n${ chalk.bold( 'Hints:' ) }\n${ hints.join( '\n' ) }` );
        }
    }
};