GitUtils.java

/*
 * Copyright (c) 2002-2021, City of Paris
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 *  1. Redistributions of source code must retain the above copyright notice
 *     and the following disclaimer.
 *
 *  2. Redistributions in binary form must reproduce the above copyright notice
 *     and the following disclaimer in the documentation and/or other materials
 *     provided with the distribution.
 *
 *  3. Neither the name of 'Mairie de Paris' nor 'Lutece' nor the names of its
 *     contributors may be used to endorse or promote products derived from
 *     this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 *
 * License 1.0
 */
package fr.paris.lutece.plugins.releaser.util.github;

import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang3.StringUtils;
import org.eclipse.jgit.api.CloneCommand;
import org.eclipse.jgit.api.CreateBranchCommand.SetupUpstreamMode;
import org.eclipse.jgit.api.ListBranchCommand.ListMode;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.MergeResult;
import org.eclipse.jgit.api.PullResult;
import org.eclipse.jgit.api.errors.CanceledException;
import org.eclipse.jgit.api.errors.CheckoutConflictException;
import org.eclipse.jgit.api.errors.DetachedHeadException;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.api.errors.InvalidConfigurationException;
import org.eclipse.jgit.api.errors.InvalidRefNameException;
import org.eclipse.jgit.api.errors.InvalidRemoteException;
import org.eclipse.jgit.api.errors.NoHeadException;
import org.eclipse.jgit.api.errors.RefAlreadyExistsException;
import org.eclipse.jgit.api.errors.RefNotFoundException;
import org.eclipse.jgit.api.errors.TransportException;
import org.eclipse.jgit.api.errors.WrongRepositoryStateException;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
import org.eclipse.jgit.transport.CredentialsProvider;
import org.eclipse.jgit.transport.RefSpec;
import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;

import fr.paris.lutece.plugins.releaser.util.CommandResult;
import fr.paris.lutece.plugins.releaser.util.ConstanteUtils;
import fr.paris.lutece.plugins.releaser.util.MapperJsonUtil;
import fr.paris.lutece.plugins.releaser.util.ReleaserUtils;
import fr.paris.lutece.portal.service.util.AppLogService;
import fr.paris.lutece.portal.service.util.AppPropertiesService;
import fr.paris.lutece.util.httpaccess.HttpAccess;
import fr.paris.lutece.util.httpaccess.HttpAccessException;
import fr.paris.lutece.util.signrequest.BasicAuthorizationAuthenticator;

// TODO: Auto-generated Javadoc
/**
 * The Class GitUtils.
 */
public class GitUtils
{

    /** The Constant MASTER_BRANCH. */
    public static final String MASTER_BRANCH = "master";

    /** The Constant DEVELOP_BRANCH. */
    public static final String DEFAULT_RELEASE_BRANCH = "develop";

    /** The Constant CONSTANTE_REF_TAG. */
    private static final String CONSTANTE_REF_TAG = "refs/tags/";

    /**
     * Clone repo.
     *
     * @param sClonePath
     *            the s clone path
     * @param sRepoURL
     *            the s repo URL
     * @param commandResult
     *            the command result
     * @param strGitHubUserLogin
     *            the str git hub user login
     * @param strUserName
     *            the str user name
     * @param strPassword
     *            the str password
     * @return the git
     */
    public static Git cloneRepo( String sClonePath, String sRepoURL, CommandResult commandResult, String strGitHubUserLogin, String strUserName,
            String strPassword )
    {
        Git git = null;
        Repository repository = null;
        try
        {
            FileRepositoryBuilder builder = new FileRepositoryBuilder( );
            File fGitDir = new File( sClonePath );

            CloneCommand clone = Git.cloneRepository( ).setCredentialsProvider( new UsernamePasswordCredentialsProvider( strUserName, strPassword ) )
                    .setBare( false ).setCloneAllBranches( true ).setDirectory( fGitDir ).setURI( getRepoUrl( sRepoURL ) );

            git = clone.call( );

            repository = builder.setGitDir( fGitDir ).readEnvironment( ).findGitDir( ).build( );
            repository.getConfig( ).setString( "user", null, "name", strGitHubUserLogin );
            repository.getConfig( ).setString( "user", null, "email", strGitHubUserLogin + "@users.noreply.github.com" );
            repository.getConfig( ).save( );

        }
        catch( InvalidRemoteException e )
        {

            ReleaserUtils.addTechnicalError( commandResult, e.getMessage( ), e );

        }
        catch( TransportException e )
        {
            ReleaserUtils.addTechnicalError( commandResult, e.getMessage( ), e );

        }
        catch( IOException e )
        {
            ReleaserUtils.addTechnicalError( commandResult, e.getMessage( ), e );
        }
        catch( GitAPIException e )
        {

            ReleaserUtils.addTechnicalError( commandResult, e.getMessage( ), e );
        }
        finally
        {
            if ( repository != null )
            {
                repository.close( );
            }
        }
        return git;

    }

    /**
     * Checkout repo branch.
     *
     * @param git
     *            the git
     * @param sBranchName
     *            the s branch name
     * @param commandResult
     *            the command result
     */
    public static void checkoutRepoBranch( Git git, String sBranchName, CommandResult commandResult )
    {
        try
        {
            git.checkout( ).setName( sBranchName ).call( );

        }
        catch( InvalidRemoteException e )
        {

            ReleaserUtils.addTechnicalError( commandResult, e.getMessage( ), e );

        }
        catch( TransportException e )
        {
            ReleaserUtils.addTechnicalError( commandResult, e.getMessage( ), e );

        }

        catch( GitAPIException e )
        {
            ReleaserUtils.addTechnicalError( commandResult, e.getMessage( ), e );
        }

    }

    /**
     * Creates the local branch.
     *
     * @param git
     *            the git
     * @param sBranchName
     *            the s branch name
     * @param commandResult
     *            the command result
     */
    public static void createLocalBranch( Git git, String sBranchName, CommandResult commandResult )
    {
        try
        {
            git.branchCreate( ).setName( sBranchName ).setUpstreamMode( SetupUpstreamMode.SET_UPSTREAM ).setStartPoint( "origin/" + sBranchName )
                    .setForce( true ).call( );
        }
        catch( InvalidRemoteException e )
        {

            ReleaserUtils.addTechnicalError( commandResult, e.getMessage( ), e );

        }
        catch( TransportException e )
        {
            ReleaserUtils.addTechnicalError( commandResult, e.getMessage( ), e );

        }

        catch( GitAPIException e )
        {
            ReleaserUtils.addTechnicalError( commandResult, e.getMessage( ), e );
        }

    }

    /**
     * Gets the ref branch.
     *
     * @param git
     *            the git
     * @param sBranchName
     *            the s branch name
     * @param commandResult
     *            the command result
     * @return the ref branch
     */
    public static String getRefBranch( Git git, String sBranchName, CommandResult commandResult )
    {

        String refLastCommit = null;
        try
        {
            git.checkout( ).setName( sBranchName ).call( );
            refLastCommit = getLastCommitId( git );
        }

        catch( RefAlreadyExistsException e )
        {
            ReleaserUtils.addTechnicalError( commandResult, e.getMessage( ), e );
        }
        catch( RefNotFoundException e )
        {
            ReleaserUtils.addTechnicalError( commandResult, e.getMessage( ), e );
        }
        catch( InvalidRefNameException e )
        {
            ReleaserUtils.addTechnicalError( commandResult, e.getMessage( ), e );
        }
        catch( CheckoutConflictException e )
        {
            ReleaserUtils.addTechnicalError( commandResult, e.getMessage( ), e );
        }
        catch( GitAPIException e )
        {
            ReleaserUtils.addTechnicalError( commandResult, e.getMessage( ), e );
        }
        return refLastCommit;
    }

    /**
     * Push force.
     *
     * @param git
     *            the git
     * @param strRefSpec
     *            the str ref spec
     * @param strUserName
     *            the str user name
     * @param strPassword
     *            the str password
     * @throws InvalidRemoteException
     *             the invalid remote exception
     * @throws TransportException
     *             the transport exception
     * @throws GitAPIException
     *             the git API exception
     */
    public static void pushForce( Git git, String strRefSpec, String strUserName, String strPassword )
            throws InvalidRemoteException, TransportException, GitAPIException
    {

        git.push( ).setRemote( "origin" ).setRefSpecs( new RefSpec( strRefSpec ) ).setForce( true )
                .setCredentialsProvider( new UsernamePasswordCredentialsProvider( strUserName, strPassword ) ).call( );

    }

    /**
     * Pull repo branch.
     *
     * @param git
     *            the git
     * @param sBranchName
     *            the s branch name
     * @param strUserName
     *            the str user name
     * @param strPassword
     *            the str password
     * @return the pull result
     * @throws IOException
     *             Signals that an I/O exception has occurred.
     * @throws WrongRepositoryStateException
     *             the wrong repository state exception
     * @throws InvalidConfigurationException
     *             the invalid configuration exception
     * @throws DetachedHeadException
     *             the detached head exception
     * @throws InvalidRemoteException
     *             the invalid remote exception
     * @throws CanceledException
     *             the canceled exception
     * @throws RefNotFoundException
     *             the ref not found exception
     * @throws NoHeadException
     *             the no head exception
     * @throws TransportException
     *             the transport exception
     * @throws GitAPIException
     *             the git API exception
     */
    public static PullResult pullRepoBranch( Git git, String sBranchName, String strUserName, String strPassword )
            throws IOException, WrongRepositoryStateException, InvalidConfigurationException, DetachedHeadException, InvalidRemoteException, CanceledException,
            RefNotFoundException, NoHeadException, TransportException, GitAPIException
    {
        PullResult pPullResult = git.pull( ).setCredentialsProvider( new UsernamePasswordCredentialsProvider( strUserName, strPassword ) ).setRemote( "origin" )
                .setRemoteBranchName( sBranchName ).call( );

        return pPullResult;
    }

    /**
     * Merge repo branch.
     *
     * @param git
     *            the git
     * @param strBranchToMerge
     *            the str branch to merge
     * @return the merge result
     * @throws IOException
     *             Signals that an I/O exception has occurred.
     * @throws WrongRepositoryStateException
     *             the wrong repository state exception
     * @throws InvalidConfigurationException
     *             the invalid configuration exception
     * @throws DetachedHeadException
     *             the detached head exception
     * @throws InvalidRemoteException
     *             the invalid remote exception
     * @throws CanceledException
     *             the canceled exception
     * @throws RefNotFoundException
     *             the ref not found exception
     * @throws NoHeadException
     *             the no head exception
     * @throws TransportException
     *             the transport exception
     * @throws GitAPIException
     *             the git API exception
     */
    public static MergeResult mergeRepoBranch( Git git, String strBranchToMerge )
            throws IOException, WrongRepositoryStateException, InvalidConfigurationException, DetachedHeadException, InvalidRemoteException, CanceledException,
            RefNotFoundException, NoHeadException, TransportException, GitAPIException
    {
        List<Ref> call = git.branchList( ).call( );
        Ref mergedBranchRef = null;
        for ( Ref ref : call )
        {
            if ( ref.getName( ).equals( "refs/heads/" + strBranchToMerge ) )
            {
                mergedBranchRef = ref;
                break;
            }
        }
        MergeResult mergeResult = git.merge( ).include( mergedBranchRef ).call( );
        return mergeResult;
    }

    /**
     * Gets the last log.
     *
     * @param git
     *            the git
     * @param nMaxCommit
     *            the n max commit
     * @return the last log
     * @throws NoHeadException
     *             the no head exception
     * @throws GitAPIException
     *             the git API exception
     */
    public static String getLastLog( Git git, int nMaxCommit ) throws NoHeadException, GitAPIException
    {
        Iterable<RevCommit> logList = git.log( ).setMaxCount( 1 ).call( );
        Iterator i = logList.iterator( );
        String sCommitMessages = "";
        while ( i.hasNext( ) )
        {
            RevCommit revCommit = (RevCommit) i.next( );
            sCommitMessages += revCommit.getFullMessage( );
            sCommitMessages += "\n";
            sCommitMessages += revCommit.getCommitterIdent( );
        }
        return sCommitMessages;
    }

    /**
     * Gets the last commit id.
     *
     * @param git
     *            the git
     * @return the last commit id
     * @throws NoHeadException
     *             the no head exception
     * @throws GitAPIException
     *             the git API exception
     */
    public static String getLastCommitId( Git git ) throws NoHeadException, GitAPIException
    {
        Iterable<RevCommit> logList = git.log( ).setMaxCount( 1 ).call( );
        Iterator i = logList.iterator( );
        String strCommitId = null;
        while ( i.hasNext( ) )
        {
            RevCommit revCommit = (RevCommit) i.next( );
            strCommitId = revCommit.getName( );

        }
        return strCommitId;
    }

    /**
     * Merge back.
     *
     * @param git
     *            the git
     * @param strUserName
     *            the str user name
     * @param strPassword
     *            the str password
     * @param commandResult
     *            the command result
     * @return the merge result
     * @throws IOException
     *             Signals that an I/O exception has occurred.
     * @throws GitAPIException
     *             the git API exception
     */
    public static MergeResult mergeBack( Git git, String strUserName, String strPassword, CommandResult commandResult ) throws IOException, GitAPIException
    {

        Ref tag = getTagLinkedToLastRelease( git );

        git.checkout( ).setName( MASTER_BRANCH ).call( );
        List<Ref> call = git.branchList( ).call( );

        Ref mergedBranchRef = null;
        for ( Ref ref : call )
        {
            if ( ref.getName( ).equals( "refs/heads/" + DEFAULT_RELEASE_BRANCH ) )
            {
                mergedBranchRef = ref;
                break;
            }
        }

        if ( tag != null )
        {
            mergedBranchRef = tag;
        }
        MergeResult mergeResult = git.merge( ).include( mergedBranchRef ).call( );
        if ( mergeResult.getMergeStatus( ).equals( MergeResult.MergeStatus.CHECKOUT_CONFLICT )
                || mergeResult.getMergeStatus( ).equals( MergeResult.MergeStatus.CONFLICTING )
                || mergeResult.getMergeStatus( ).equals( MergeResult.MergeStatus.FAILED )
                || mergeResult.getMergeStatus( ).equals( MergeResult.MergeStatus.NOT_SUPPORTED ) )
        {

            ReleaserUtils.addTechnicalError( commandResult,
                    mergeResult.getMergeStatus( ).toString( ) + "\nPlease merge manually master into" + DEFAULT_RELEASE_BRANCH + "branch." );
        }
        else
        {
            git.push( ).setCredentialsProvider( new UsernamePasswordCredentialsProvider( strUserName, strPassword ) ).call( );
            commandResult.getLog( ).append( mergeResult.getMergeStatus( ) );
        }
        return mergeResult;

    }

    /**
     * Search repo.
     *
     * @param strSearch
     *            the str search
     * @param strOrganization
     *            the str organization
     * @param strUserName
     *            the str user name
     * @param strPassword
     *            the str password
     * @return the github search result
     */
    public static GithubSearchResult searchRepo( String strSearch, String strOrganization, String strUserName, String strPassword )
    {
        HttpAccess httpAccess = new HttpAccess( );

        GithubSearchResult searchResult = null;

        String strUrl = null;
        try
        {
            strUrl = MessageFormat.format( AppPropertiesService.getProperty( ConstanteUtils.PROPERTY_GITHUB_SEARCH_REPO_API ),
                    URLEncoder.encode( strSearch, "UTF-8" ), strOrganization );
        }
        catch( UnsupportedEncodingException e1 )
        {
            AppLogService.error( e1 );
        }

        String strResponse = "";

        try
        {

            String strApiToken = AppPropertiesService.getProperty( ConstanteUtils.PROPERTY_GITHUB_SEARCH_REPO_API_TOKEN );
            Map<String, String> mapHeaderToken = new HashMap<String, String>( );
            mapHeaderToken.put( "Authorization", "token " + strApiToken );
            strResponse = httpAccess.doGet( strUrl, null, null, mapHeaderToken );

            if ( !StringUtils.isEmpty( strResponse ) )
            {
                searchResult = MapperJsonUtil.parse( strResponse, GithubSearchResult.class );

            }

        }
        catch( HttpAccessException ex )
        {
            AppLogService.error( ex );
        }
        catch( IOException e )
        {
            AppLogService.error( e );
        }

        return searchResult;
    }

    /**
     * Gets the file content.
     *
     * @param strFullName
     *            the str full name
     * @param strPathFile
     *            the str path file
     * @param strBranch
     *            the str branch
     * @param strUserName
     *            the str user name
     * @param strPassword
     *            the str password
     * @return the file content
     */
    public static String getFileContent( String strFullName, String strPathFile, String strBranch, String strUserName, String strPassword )
    {
        HttpAccess httpAccess = new HttpAccess( );
        String strUrl = "https://raw.githubusercontent.com/" + strFullName + "/" + strBranch + "/" + strPathFile;
        // Map<String,String> hashHeader=new HashMap<>( );
        // hashHeader.put( "accept", "application/vnd.github.VERSION.raw" );
        String strResponse = "";

        try
        {

            strResponse = httpAccess.doGet( strUrl, new BasicAuthorizationAuthenticator( strUserName, strPassword ), null );

        }
        catch( HttpAccessException ex )
        {
            AppLogService.error( ex );
        }

        return strResponse;
    }

    /**
     * Gets the tag linked to last release.
     *
     * @param git
     *            the git
     * @return the tag linked to last release
     * @throws GitAPIException
     *             the git API exception
     */
    private static Ref getTagLinkedToLastRelease( Git git ) throws GitAPIException
    {
        final String TOKEN = "[maven-release-plugin] prepare release ";
        Ref res = null;
        String sTagName = null;

        Iterable<RevCommit> logList = git.log( ).setMaxCount( 10 ).call( );
        Iterator i = logList.iterator( );
        String sCommitMessages = "";
        while ( i.hasNext( ) )
        {
            RevCommit revCommit = (RevCommit) i.next( );

            sCommitMessages = revCommit.getFullMessage( );
            int index = sCommitMessages.indexOf( TOKEN );
            if ( index >= 0 )
            {
                sTagName = sCommitMessages.replace( TOKEN, "" );
                break;
            }
        }

        if ( ( sTagName != null ) && ( !( sTagName.trim( ).equals( "" ) ) ) )
        {
            List<Ref> tags = git.tagList( ).call( );
            for ( int j = 0; j < tags.size( ); j++ )
            {
                Ref tag = tags.get( tags.size( ) - 1 - j );
                String tagName = tag.getName( );
                if ( ( "refs/tags/" + sTagName ).startsWith( tag.getName( ) ) )
                {
                    res = tag;
                    break;
                }
            }
        }

        return res;
    }

    /**
     * Gets the tag name list.
     *
     * @param git
     *            the git
     * @return the tag name list
     */
    public static List<String> getTagNameList( Git git )
    {
        List<String> listTagName = null;
        if ( git != null )
        {
            listTagName = new ArrayList<>( );
            Collection<Ref> colTags = git.getRepository( ).getTags( ).values( );
            for ( Ref ref : colTags )
            {
                listTagName.add( ref.getName( ).replace( CONSTANTE_REF_TAG, "" ) );
            }
        }

        return listTagName;
    }

    /**
     * Gets the repo url.
     *
     * @param strRepoUrl
     *            the str repo url
     * @return the repo url
     */
    public static String getRepoUrl( String strRepoUrl )
    {

        if ( strRepoUrl != null && strRepoUrl.startsWith( "scm:git:" ) )
        {
            strRepoUrl = strRepoUrl.substring( 8 );

        }

        return strRepoUrl;

    }

    /**
     * Gets the git.
     *
     * @param strClonePath
     *            the str clone path
     * @return the git
     */
    public static Git getGit( String strClonePath )
    {
        Git git = null;
        Repository repository = null;

        File fGitDir = new File( strClonePath + "/.git" );

        if ( !fGitDir.exists( ) )
        {
            return null;
        }

        try
        {
            FileRepositoryBuilder builder = new FileRepositoryBuilder( );
            repository = builder.setGitDir( fGitDir ).readEnvironment( ).findGitDir( ).build( );

            git = new Git( repository );
        }
        catch( IOException e )
        {
            AppLogService.error( e.getMessage( ), e );
        }

        return git;
    }

    public static List<String> getBranchList( String repoUrl, File localRepo, CommandResult commandResult, String login, String pwd )
    {
        Git git = null;
        List<String> branchNameList = null;

        try
        {
            CredentialsProvider credential = new UsernamePasswordCredentialsProvider( login, pwd );

            git = Git.cloneRepository( ).setCredentialsProvider( credential ).setURI( repoUrl ).setDirectory( localRepo ).setCloneAllBranches( true ).call( );

            branchNameList = new ArrayList<String>( );

            List<Ref> branchList = git.branchList( ).setListMode( ListMode.ALL ).call( );
            if ( !branchList.isEmpty( ) )
            {
                for ( Ref ref : branchList )
                {
                    String [ ] refSplit = ref.getName( ).split( "/" );

                    if ( refSplit [1].equals( "remotes" ) && refSplit [2].equals( "origin" ) )
                    {
                        branchNameList.add( refSplit [3] );
                    }
                }
            }
        }
        catch( InvalidRemoteException e )
        {
            ReleaserUtils.addTechnicalError( commandResult, e.getMessage( ), e );
            branchNameList.add( "InvalidRemoteException" );
        }
        catch( TransportException e )
        {
            ReleaserUtils.addTechnicalError( commandResult, e.getMessage( ), e );
            branchNameList.add( "TransportException" );
        }
        catch( GitAPIException e )
        {
            ReleaserUtils.addTechnicalError( commandResult, e.getMessage( ), e );
            branchNameList.add( "GitAPIException)" );
        }
        finally
        {
            if ( git != null )
            {
                git.close( );
            }
        }

        return branchNameList;
    }
}