All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.capitalone.dashboard.evaluator.CodeReviewEvaluator Maven / Gradle / Ivy

There is a newer version: 3.7.33
Show newest version
package com.capitalone.dashboard.evaluator;

import com.capitalone.dashboard.ApiSettings;
import com.capitalone.dashboard.common.CommonCodeReview;
import com.capitalone.dashboard.model.AuditException;
import com.capitalone.dashboard.model.CollectionError;
import com.capitalone.dashboard.model.Collector;
import com.capitalone.dashboard.model.CollectorItem;
import com.capitalone.dashboard.model.CollectorType;
import com.capitalone.dashboard.model.Commit;
import com.capitalone.dashboard.model.CommitType;
import com.capitalone.dashboard.model.Dashboard;
import com.capitalone.dashboard.model.GitRequest;
import com.capitalone.dashboard.model.Review;
import com.capitalone.dashboard.model.SCM;
import com.capitalone.dashboard.model.ServiceAccount;
import com.capitalone.dashboard.repository.CollectorRepository;
import com.capitalone.dashboard.repository.CommitRepository;
import com.capitalone.dashboard.repository.GitRequestRepository;
import com.capitalone.dashboard.repository.ServiceAccountRepository;
import com.capitalone.dashboard.request.ArtifactAuditRequest;
import com.capitalone.dashboard.response.CodeReviewAuditResponseV2;
import com.capitalone.dashboard.service.LdapService;
import com.capitalone.dashboard.status.CodeReviewAuditStatus;
import com.capitalone.dashboard.util.GitHubParsedUrl;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.bson.types.ObjectId;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

@Component
public class CodeReviewEvaluator extends Evaluator {
        private final CommitRepository commitRepository;
        private final GitRequestRepository gitRequestRepository;
        private final CollectorRepository collectorRepository;
        private final ServiceAccountRepository serviceAccountRepository;
        private static final Logger LOGGER = LoggerFactory.getLogger(CodeReviewEvaluator.class);
    private final LdapService ldapService;

        protected ApiSettings settings;
        private static final String BRANCH = "branch";
        private static final String REPO_URL = "url";

    @Autowired
    public CodeReviewEvaluator(CommitRepository commitRepository, GitRequestRepository gitRequestRepository,
                               CollectorRepository collectorRepository, ServiceAccountRepository serviceAccountRepository,
                               ApiSettings settings, LdapService ldapService) {
        this.commitRepository = commitRepository;
        this.gitRequestRepository = gitRequestRepository;
        this.collectorRepository = collectorRepository;
        this.settings = settings;
        this.serviceAccountRepository = serviceAccountRepository;
        this.ldapService = ldapService;
    }


    @Override
    public Collection evaluate(Dashboard dashboard, long beginDate, long endDate, Map data, String altIdentifier, String identifierName) throws AuditException {
        List responseV2s = new ArrayList<>();
        List repoItems = getCollectorItemsByAltIdentifier(dashboard, CollectorType.SCM,altIdentifier);
        if (CollectionUtils.isEmpty(repoItems)) {
            LOGGER.info("NO_COLLECTOR_ITEM_CONFIGURED for dashboard=" + dashboard.getTitle());
            throw new AuditException("No code repository configured", AuditException.NO_COLLECTOR_ITEM_CONFIGURED);
        }

        //making sure we have a goot url?
        repoItems.forEach(repoItem -> {
            String scmUrl = (String) repoItem.getOptions().get("url");
            String scmBranch = (String) repoItem.getOptions().get("branch");
            GitHubParsedUrl gitHubParsed = new GitHubParsedUrl(scmUrl);
            String parsedUrl = gitHubParsed.getUrl(); //making sure we have a goot url?
            CodeReviewAuditResponseV2 reviewResponse = null;

            if (repoItem.isPushed()) {
                Collector githubCollector = collectorRepository.findByName("GitHub");
                List collectorItemList = new ArrayList<>();
                List collectorIdList = new ArrayList<>();
                collectorIdList.add(githubCollector.getId());
                Iterable collectorItemIterable
                        = collectorItemRepository.findAllByOptionNameValueAndCollectorIdsIn(REPO_URL, parsedUrl, collectorIdList);

                for (CollectorItem ci : collectorItemIterable) {
                    if (scmBranch.equalsIgnoreCase((String) ci.getOptions().get(BRANCH))) { continue; }

                    collectorItemList.add(ci);
                }

                reviewResponse = evaluate(repoItem, collectorItemList, beginDate, endDate, null);
            } else {
                reviewResponse = evaluate(repoItem, beginDate, endDate, null);
            }

            reviewResponse.setUrl(parsedUrl);
            reviewResponse.setBranch(scmBranch);
            reviewResponse.setLastUpdated(repoItem.getLastUpdated());
            responseV2s.add(reviewResponse);
        });
        return responseV2s;
    }

    @Override
    public Collection evaluateNextGen(ArtifactAuditRequest artifactAuditRequest, Dashboard dashboard, long beginDate, long endDate, Map data) throws AuditException {
        return null;
    }


    @Override
    public CodeReviewAuditResponseV2 evaluate(CollectorItem collectorItem, long beginDate, long endDate, Map data) {
        return getPeerReviewResponses(collectorItem, new ArrayList<>(), beginDate, endDate);
    }

    protected CodeReviewAuditResponseV2 evaluate(CollectorItem collectorItem, List collectorItemList, long beginDate, long endDate, Map data) {
        return getPeerReviewResponses(collectorItem, collectorItemList, beginDate, endDate);
    }

    /**
     * Return an empty response in error situation
     *
     * @param repoItem
     * @param scmBranch
     * @param scmUrl
     * @return
     */
    protected CodeReviewAuditResponseV2 getErrorResponse(CollectorItem repoItem, String scmBranch, String scmUrl) {
        CodeReviewAuditResponseV2 noPRsCodeReviewAuditResponse = new CodeReviewAuditResponseV2();
        noPRsCodeReviewAuditResponse.addAuditStatus(CodeReviewAuditStatus.COLLECTOR_ITEM_ERROR);
        noPRsCodeReviewAuditResponse.setAuditEntity(repoItem.getOptions());
        noPRsCodeReviewAuditResponse.setLastUpdated(repoItem.getLastUpdated());
        noPRsCodeReviewAuditResponse.setBranch(scmBranch);
        noPRsCodeReviewAuditResponse.setUrl(scmUrl);
        noPRsCodeReviewAuditResponse.setErrorMessage(repoItem.getErrors() == null ? null : repoItem.getErrors().get(0).getErrorMessage());
        return noPRsCodeReviewAuditResponse;
    }

    private CodeReviewAuditResponseV2 getPeerReviewResponses(CollectorItem repoItem,
                                                             List collectorItemList,
                                                             long beginDt, long endDt) {

        CodeReviewAuditResponseV2 reviewAuditResponseV2 = new CodeReviewAuditResponseV2();

        if (repoItem == null) {
            reviewAuditResponseV2.addAuditStatus(CodeReviewAuditStatus.REPO_NOT_CONFIGURED);
            return reviewAuditResponseV2;
        }
        reviewAuditResponseV2.setAuditEntity(repoItem.getOptions());

        String scmUrl = (String) repoItem.getOptions().get("url");
        String scmBranch = (String) repoItem.getOptions().get("branch");

        GitHubParsedUrl gitHubParsed = new GitHubParsedUrl(scmUrl);

        String parsedUrl = gitHubParsed.getUrl(); //making sure we have a goot url?

        if (StringUtils.isEmpty(scmBranch) || StringUtils.isEmpty(scmUrl)) {
            return getErrorResponse(repoItem, scmBranch, parsedUrl);
        }

        //if the collector item is pending data collection
        if (repoItem.getLastUpdated() == 0) {
            reviewAuditResponseV2.addAuditStatus(CodeReviewAuditStatus.PENDING_DATA_COLLECTION);

            reviewAuditResponseV2.setLastUpdated(repoItem.getLastUpdated());
            reviewAuditResponseV2.setBranch(scmBranch);
            reviewAuditResponseV2.setUrl(scmUrl);
            return reviewAuditResponseV2;
        }

        List pullRequests = gitRequestRepository.findByCollectorItemIdAndMergedAtIsBetween(repoItem.getId(), beginDt-1, endDt+1);
        List allCommits = commitRepository.findByCollectorItemIdAndScmCommitTimestampIsBetween(repoItem.getId(), beginDt-1, endDt+1);
        //Filter empty and auto merged commits
        List commits = allCommits.stream().filter(commit -> isNeitherEmptyNorAutoMerged(commit)).collect(Collectors.toList());
        commits.sort(Comparator.comparing(Commit::getScmCommitTimestamp).reversed());
        if (settings.isEnrichCommits()) {
            commits = enrichCommits(commits);
        }
        List inputCommits = commits.stream().collect(Collectors.toList());
        pullRequests.sort(Comparator.comparing(GitRequest::getMergedAt).reversed());

        if (hasValidRepoError(repoItem, pullRequests, commits, beginDt, endDt)) {
            return getErrorResponse(repoItem, scmBranch, parsedUrl);
        }

        if (CollectionUtils.isEmpty(pullRequests)) {
            reviewAuditResponseV2.addAuditStatus(CodeReviewAuditStatus.NO_PULL_REQ_FOR_DATE_RANGE);
        }
        if (CollectionUtils.isEmpty(commits)) {
            reviewAuditResponseV2.addAuditStatus(CodeReviewAuditStatus.NO_COMMIT_FOR_DATE_RANGE);
        }
        reviewAuditResponseV2.setUrl(parsedUrl);
        reviewAuditResponseV2.setBranch(scmBranch);
        reviewAuditResponseV2.setLastCommitTime(CollectionUtils.isEmpty(commits)? 0 : commits.get(0).getScmCommitTimestamp());
        reviewAuditResponseV2.setLastPRMergeTime(CollectionUtils.isEmpty(pullRequests)? 0 : pullRequests.get(0).getMergedAt());
        reviewAuditResponseV2.setLastUpdated(repoItem.getLastUpdated());

        // make list of PR'd and nonPR'd to fix false Automerge not-peer-reviewed warnings
        List allPrCommitShas = new ArrayList<>();
        List notPeerReviewed = new ArrayList<>();
        List peerReviewed = new ArrayList<>();

        pullRequests.stream().filter(pr -> "merged".equalsIgnoreCase(pr.getState())).forEach(pr -> {
            Boolean isPeerReviewed = auditPullRequest(repoItem, pr, inputCommits, allPrCommitShas, reviewAuditResponseV2);
            boolean isPRd = isPeerReviewed ? peerReviewed.add(pr) : notPeerReviewed.add(pr);
        });


        Long startTime = System.currentTimeMillis();
        if(notPeerReviewed.size() > 0) {
            LOGGER.info(String.format("AutoMerge Check :: Reviewing %d Pull Request(s) with no peer review", notPeerReviewed.size()));
        }

        // iterate though the gitRequests that failed the PR audit and check if the PR was auto merged
        for(GitRequest noPR: notPeerReviewed){
            boolean foundCommit = false;

            // if for some reason the PR has no commit, ignore and move on
            if (noPR.getCommits().isEmpty()){continue;}

            // get the latest commit & filter out merge commit by checking non-matching scmNums
            noPR.getCommits().sort(Comparator.comparing(Commit::getScmCommitTimestamp));
            Commit lastCommit = noPR.getCommits().get(noPR.getCommits().size()-1);

            // iterate through the peerReviewed and their commits to see if the failed PR's commit exists in there
            for (GitRequest yesPR: peerReviewed.stream().filter(pr -> pr.getCreatedAt() >= noPR.getCreatedAt()).collect(Collectors.toList())) {
                for (Commit commit: yesPR.getCommits()) {
                    if (lastCommit.getScmRevisionNumber().equalsIgnoreCase(commit.getScmRevisionNumber())){

                        foundCommit = true;

                        // get the PR that has the Peer review fail
                        CodeReviewAuditResponseV2.PullRequestAudit prAudit = reviewAuditResponseV2.getPullRequests()
                                .stream().filter(pr -> pr.getPullRequest().equals(noPR)).findFirst().orElse(null);

                        // Remove the old PR in the audit response
                        reviewAuditResponseV2.getPullRequests().remove(prAudit);

                        // get audit status, remove NOT_PEER_REVIEWED, set audit status for PR
                        Set auditStatuses = prAudit.getAuditStatuses();
                        auditStatuses.remove(CodeReviewAuditStatus.PULLREQ_NOT_PEER_REVIEWED);
                        auditStatuses.add(CodeReviewAuditStatus.PULLREQ_REVIEWED_BY_PEER);
                        prAudit.setAuditStatus(auditStatuses);

                        // add PR audit back to the auditResponse
                        reviewAuditResponseV2.addPullRequest(prAudit);

                        LOGGER.info(String.format("AutoMerge Check :: Correcting auto merged PR's status (ObjectID: %s)", noPR.getId().toString()));

                        break;      // exit loop because the commit was verified in another PR
                    }
                }
                if(foundCommit){break;}     // stop iterating through PRs if commit was verified
            }
        }

        if(notPeerReviewed.size() > 0) {
            LOGGER.info(String.format("AutoMerge Check :: Completed in %d ms", System.currentTimeMillis() - startTime));
        }
        //check any commits not directly tied to pr
        commits.stream().filter(commit -> !allPrCommitShas.contains(commit.getScmRevisionNumber()) && StringUtils.isEmpty(commit.getPullNumber()) && commit.getType() == CommitType.New).forEach(reviewAuditResponseV2::addDirectCommit);

        //check any commits not directly tied to pr
        List commitsNotDirectlyTiedToPr = new ArrayList<>();
        commits.forEach(commit -> {
            // flag any commits made by an LDAP unauthenticated user
            checkCommitByLDAPUnauthUser(reviewAuditResponseV2, commit);

            if (!checkPrCommitsAndCommitType(allPrCommitShas, commit)) { return; }

            if ( isCommitEligibleForDirectCommitsForPushedRepo(repoItem, commit, collectorItemList, beginDt, endDt)
                    || isCommitEligibleForDirectCommitsForPulledRepo(repoItem, commit) ) {
                commitsNotDirectlyTiedToPr.add(commit);
                // check for service account and increment version tag for service account on direct commits.
                auditDirectCommits(reviewAuditResponseV2, commit);
            }
        });

        return reviewAuditResponseV2;
    }

    private List enrichCommits(List commits) {
        if (CollectionUtils.isEmpty(commits)) return commits;
        List result = new ArrayList<>();
        for (Commit commit : commits) {
            if (StringUtils.isEmpty(commit.getScmAuthorLDAPDN())) {
                String loginKey = commit.getScmAuthorLogin();
                commit.setScmAuthorLDAPDN(ldapService.getLdapDN(loginKey));
            }
            result.add(commit);
        }
        return result;
    }


    // Check if commit is neither empty nor auto merged
    protected boolean isNeitherEmptyNorAutoMerged(Commit commit) {
        String pullNumber = commit.getPullNumber();
        return (commit.getNumberOfChanges()>0 && (StringUtils.isNotEmpty(pullNumber) || (StringUtils.isEmpty(pullNumber) && !isCommitHasMergedPR(commit))));
    }

    private boolean hasValidRepoError(CollectorItem repoItem, List pullRequests, List commits, long beginDt, long endDt) {
        List colErrors = repoItem.getErrors();
        return (CollectionUtils.isNotEmpty(colErrors)
                && colErrors.stream().anyMatch(err -> isWithinTimeRange(err.getTimestamp(), beginDt, endDt))
                && CollectionUtils.isEmpty(pullRequests)
                && CollectionUtils.isEmpty(commits));
    }

    private boolean isWithinTimeRange(long timestamp, long beginDt, long endDt) {
        return (timestamp >= beginDt && timestamp <= endDt);
    }

    /**
     * Flag commit made by an unauthenticated user that is not by a service account and has non-code changes with an approved message
     * Adds SCM_AUTHOR_LOGIN_INVALID status at Code Review level
     */
    private void checkCommitByLDAPUnauthUser(CodeReviewAuditResponseV2 reviewAuditResponseV2, Commit commit) {
        if (StringUtils.isNotEmpty(commit.getScmAuthorType()) && settings.getLdapdnCheckIgnoredAuthorTypes().contains(commit.getScmAuthorType())) {
            return;
        }
        if (StringUtils.isEmpty(commit.getScmAuthorLDAPDN()) &&
                !CommonCodeReview.matchIncrementVersionTag(commit.getScmCommitLog(), settings)) {
            reviewAuditResponseV2.addAuditStatus(CodeReviewAuditStatus.SCM_AUTHOR_LOGIN_INVALID);
            // add commit made by unauth user to commitsByLDAPUnauthUsers list
            reviewAuditResponseV2.addCommitByLDAPUnauthUser(commit);
        }
    }

    private boolean checkPrCommitsAndCommitType(List allPrCommitShas, Commit commit) {
        if (Objects.isNull(commit)) return false;
        if (CollectionUtils.isEmpty(allPrCommitShas)) return true;
        return (commit.getType() == CommitType.New) && !allPrCommitShas.contains(commit.getScmRevisionNumber());
    }

    private boolean isCommitEligibleForDirectCommitsForPushedRepo(CollectorItem repoItem, Commit commit,
                                                                  List collectorItemList,
                                                                  long beginDt, long endDt) {
        return repoItem.isPushed()
                && !existsApprovedPROnAnotherBranch(repoItem, commit, collectorItemList, beginDt, endDt);
    }

    private boolean isCommitEligibleForDirectCommitsForPulledRepo(CollectorItem repoItem, Commit commit) {
        return !repoItem.isPushed() && StringUtils.isEmpty(commit.getPullNumber());
    }

    protected Boolean auditPullRequest(CollectorItem repoItem, GitRequest pr, List commits,
                                    List allPrCommitShas, CodeReviewAuditResponseV2 reviewAuditResponseV2) {
        Commit mergeCommit = Optional.ofNullable(commits)
                                .orElseGet(Collections::emptyList).stream()
                                .filter(c -> Objects.equals(c.getScmRevisionNumber(), pr.getScmRevisionNumber()))
                                .findFirst().orElse(null);

        if (mergeCommit == null) {
            mergeCommit = Optional.ofNullable(commits)
                            .orElseGet(Collections::emptyList).stream()
                            .filter(c -> Objects.equals(c.getScmRevisionNumber(), pr.getScmMergeEventRevisionNumber()))
                            .findFirst().orElse(null);
        }

        CodeReviewAuditResponseV2.PullRequestAudit pullRequestAudit = new CodeReviewAuditResponseV2.PullRequestAudit();
        pullRequestAudit.setPullRequest(pr);
        List allCommitsRelatedToPr = pr.getCommits();
        List commitsRelatedToPr = allCommitsRelatedToPr.stream().filter(commit -> commit.getNumberOfChanges()>0).collect(Collectors.toList());
        commitsRelatedToPr.sort(Comparator.comparing(e -> (e.getScmCommitTimestamp())));

        if (mergeCommit == null) {
            pullRequestAudit.addAuditStatus(CodeReviewAuditStatus.MERGECOMMITER_NOT_FOUND);
        } else {
            commitsRelatedToPr.add(mergeCommit);
            if(Objects.nonNull(mergeCommit.getScmAuthorLogin())){
                pullRequestAudit.addAuditStatus(pr.getUserId().equalsIgnoreCase(mergeCommit.getScmAuthorLogin()) ? CodeReviewAuditStatus.COMMITAUTHOR_EQ_MERGECOMMITER : CodeReviewAuditStatus.COMMITAUTHOR_NE_MERGECOMMITER);
            }else{
                pullRequestAudit.addAuditStatus(pr.getUserId().equalsIgnoreCase(mergeCommit.getScmCommitterLogin()) ? CodeReviewAuditStatus.COMMITAUTHOR_EQ_MERGECOMMITER : CodeReviewAuditStatus.COMMITAUTHOR_NE_MERGECOMMITER);
            }
        }
        allPrCommitShas.addAll(commitsRelatedToPr.stream().map(SCM::getScmRevisionNumber).collect(Collectors.toList()));

        // Check peer reviews
        boolean peerReviewed = CommonCodeReview.computePeerReviewStatus(pr, settings, pullRequestAudit, commits, commitRepository, serviceAccountRepository);
        pullRequestAudit.addAuditStatus(peerReviewed ? CodeReviewAuditStatus.PULLREQ_REVIEWED_BY_PEER : CodeReviewAuditStatus.PULLREQ_NOT_PEER_REVIEWED);
        String sourceRepo = pr.getSourceRepo();
        String targetRepo = pr.getTargetRepo();
        pullRequestAudit.addAuditStatus(sourceRepo == null ? CodeReviewAuditStatus.GIT_FORK_STRATEGY : sourceRepo.equalsIgnoreCase(targetRepo) ? CodeReviewAuditStatus.GIT_BRANCH_STRATEGY : CodeReviewAuditStatus.GIT_FORK_STRATEGY);
        auditCommitAfterPrMerge(reviewAuditResponseV2, pullRequestAudit, pr, commitsRelatedToPr);
        auditCommitsAfterReviews(reviewAuditResponseV2, pullRequestAudit, pr);
        reviewAuditResponseV2.addPullRequest(pullRequestAudit);

        return peerReviewed ?  Boolean.TRUE:  Boolean.FALSE;    // to track which PR's need further examining
    }

    /**
     * Flag commit(s) made after pull request merge as violation
     * Adds COMMIT_AFTER_PR_MERGE status at both PR and Code Review level
     */
    protected void auditCommitAfterPrMerge(CodeReviewAuditResponseV2 reviewAuditResponseV2, CodeReviewAuditResponseV2.PullRequestAudit pullRequestAudit, GitRequest pr, List commitsRelatedToPr) {
        if (CollectionUtils.isEmpty(commitsRelatedToPr)) { return; }
        List  commitsAfterPrMerge = commitsRelatedToPr.stream().filter(Objects::nonNull).filter(commit -> (
                commit.getScmCommitTimestamp() > pr.getMergedAt()
                )).collect(Collectors.toList());
        if (CollectionUtils.isEmpty(commitsAfterPrMerge)) { return; }

        pullRequestAudit.addAuditStatus(CodeReviewAuditStatus.COMMIT_AFTER_PR_MERGE);
        // if code review audit status doesn't already contain this status, then add it
        if (!reviewAuditResponseV2.getAuditStatuses().contains(CodeReviewAuditStatus.COMMIT_AFTER_PR_MERGE)) {
            reviewAuditResponseV2.addAuditStatus(CodeReviewAuditStatus.COMMIT_AFTER_PR_MERGE);
        }
        // add specific commit(s) made after PR merge to commitAfterPrMerge list
        commitsAfterPrMerge.forEach(reviewAuditResponseV2::addCommitAfterPrMerge);
    }

    /**
     * Flag commits made after peer reviews as violations, exclude commits that are merge commits from target branches
     */
    protected void auditCommitsAfterReviews(CodeReviewAuditResponseV2 reviewAuditResponseV2, CodeReviewAuditResponseV2.PullRequestAudit pullRequestAudit, GitRequest pr) {
        List reviewsRelatedToPr = pr.getReviews().stream().filter(review -> StringUtils.equals(review.getState(),"APPROVED")).sorted(Comparator.comparing(Review::getUpdatedAt)).collect(Collectors.toList());
        if(CollectionUtils.isEmpty(reviewsRelatedToPr)) { return; }

        List commitsRelatedToPr = pr.getCommits().stream().filter(commit -> commit.getNumberOfChanges()>0).collect(Collectors.toList());
        if(CollectionUtils.isEmpty(commitsRelatedToPr)) { return; }

        long lastReviewTimestamp = reviewsRelatedToPr.get(reviewsRelatedToPr.size() - 1).getUpdatedAt();
        List commitsAfterPrReviews = commitsRelatedToPr.stream().filter(Objects::nonNull).filter(commit -> (
                commit.getScmCommitTimestamp() > lastReviewTimestamp
                && !isMergeCommitFromTargetBranch(commit, pr))
        ).collect(Collectors.toList());

        if(CollectionUtils.isEmpty(commitsAfterPrReviews)) { return; }
        reviewAuditResponseV2.addAuditStatus(CodeReviewAuditStatus.COMMITS_AFTER_PR_REVIEWS);
        pullRequestAudit.addAuditStatus(CodeReviewAuditStatus.COMMITS_AFTER_PR_REVIEWS);
        commitsAfterPrReviews.forEach(reviewAuditResponseV2::addCommitAfterPrReviews);
    }

    /**
     * Check if a commit is a merge commit from target branches
     * this type of commits has default commit logs that look like "merge branch 'target_branch' into ..."
     * Note that this ins't the ideal way to check this as commit logs can be modified by users
     */
    public boolean isMergeCommitFromTargetBranch(Commit commit, GitRequest pr) {
        if(commit == null || pr == null) return false;
        String commitLog = commit.getScmCommitLog();
        List mergeCommitFromTargetBranchRegEx = settings.getMergeCommitFromTargetBranchRegEx();
        for (String mergeCommitRegex : mergeCommitFromTargetBranchRegEx) {
            Pattern pattern = Pattern.compile(mergeCommitRegex, Pattern.CASE_INSENSITIVE);
            Matcher matcher = pattern.matcher(commitLog);
            if (matcher.matches() && (StringUtils.equalsIgnoreCase(pr.getScmBranch(), matcher.group(2))
                    || StringUtils.equalsIgnoreCase(pr.getScmUrl(), matcher.group(2)))) {
                    // exit only if matches, else check other patterns
                    return true;
                }
            }
        return false;
    }

    protected boolean existsApprovedPROnAnotherBranch(CollectorItem repoItem, Commit commit, List collectorItemList,
                                                      long beginDt, long endDt) {
        CollectorItem collectorItem = Optional.ofNullable(collectorItemList)
                                        .orElseGet(Collections::emptyList).stream()
                                        .filter(ci -> existsApprovedPRForCollectorItem(repoItem, commit, ci, beginDt, endDt))
                                        .findFirst().orElse(null);
        return (collectorItem != null);
    }

    protected boolean existsApprovedPRForCollectorItem(CollectorItem repoItem, Commit commit, CollectorItem collectorItem,
                                                       long beginDt, long endDt) {
        List mergedPullRequests
                = gitRequestRepository.findByCollectorItemIdAndMergedAtIsBetween(collectorItem.getId(), beginDt-1, endDt+1);

        List commits
                = commitRepository.findByCollectorItemIdAndScmCommitTimestampIsBetween(collectorItem.getId(), beginDt-1, endDt+1);

        GitRequest mergedPullRequestFound
                = Optional.ofNullable(mergedPullRequests)
                .orElseGet(Collections::emptyList).stream()
                .filter(mergedPullRequest -> evaluateMergedPullRequest(repoItem, mergedPullRequest, commit, commits))
                .findFirst().orElse(null);

        return (mergedPullRequestFound != null);
    }

    private boolean evaluateMergedPullRequest (CollectorItem repoItem, GitRequest mergedPullRequest,
                                               Commit commit, List commits) {
        Commit matchingCommit = findAMatchingCommit(mergedPullRequest, commit, commits);
        if (matchingCommit == null) { return false; }

        List allPrCommitShas = new ArrayList<>();
        CodeReviewAuditResponseV2 reviewAuditResponseV2 = new CodeReviewAuditResponseV2();

        // Matching commit found, now make sure the PR for the matching commit passes all the audit checks
        auditPullRequest(repoItem, mergedPullRequest, commits, allPrCommitShas, reviewAuditResponseV2);
        CodeReviewAuditResponseV2.PullRequestAudit pullRequestAudit = reviewAuditResponseV2.getPullRequests().get(0);

        return (pullRequestAudit != null) && codeReviewAuditResponseCheck(pullRequestAudit);
    }

    protected boolean codeReviewAuditResponseCheck(CodeReviewAuditResponseV2.PullRequestAudit pullRequestAudit) {
        for (CodeReviewAuditStatus status : pullRequestAudit.getAuditStatuses()) {
            if ((status == CodeReviewAuditStatus.COMMITAUTHOR_EQ_MERGECOMMITER)
                    || (status == CodeReviewAuditStatus.PULLREQ_NOT_PEER_REVIEWED)) {
                return false;
            }
        }
        return true;
    }

    protected Commit findAMatchingCommit(GitRequest mergedPullRequest, Commit commitToBeFound, List commitsOnTheRepo) {
        List commitsRelatedToPr = mergedPullRequest.getCommits();

        // So, will find the matching commit based on the criteria below for "Merge Only" case.
        Commit matchingCommit
                = Optional.ofNullable(commitsRelatedToPr)
                    .orElseGet(Collections::emptyList).stream()
                    .filter(commitRelatedToPr -> checkIfCommitsMatch(commitRelatedToPr, commitToBeFound))
                    .findFirst().orElse(null);
        // For "Squash and Merge", or a "Rebase and Merge":
        // The merged commit will not be part of the commits in the PR.
        // The PR will only have the original commits when the PR was opened.
        // Search for the commit in the list of commits on the repo in the db
        if (matchingCommit == null) {
            String pullNumber = mergedPullRequest.getNumber();
            matchingCommit
                    = Optional.ofNullable(commitsOnTheRepo)
                        .orElseGet(Collections::emptyList).stream()
                        .filter(commitOnRepo -> Objects.equals(pullNumber, commitToBeFound.getPullNumber())
                            && checkIfCommitsMatch(commitOnRepo, commitToBeFound))
                        .findFirst().orElse(null);
        }

        return matchingCommit;
    }

    protected boolean checkIfCommitsMatch(Commit commit1, Commit commit2) {
        return Objects.equals(commit1.getScmRevisionNumber(), commit2.getScmRevisionNumber())
                && Objects.equals(commit1.getScmAuthor(), commit2.getScmAuthor())
                && Objects.equals(commit1.getScmCommitTimestamp(), commit2.getScmCommitTimestamp())
                && Objects.equals(commit1.getScmCommitLog(), commit2.getScmCommitLog());
    }

    protected void auditDirectCommits(CodeReviewAuditResponseV2 reviewAuditResponseV2, Commit commit) {
        Stream combinedStream
                = Stream.of(commit.getFilesAdded(), commit.getFilesModified(),commit.getFilesRemoved()).filter(Objects::nonNull).flatMap(Collection::stream);
        Collection collectionCombined = combinedStream.collect(Collectors.toList());
       if (CommonCodeReview.checkForServiceAccount(commit.getScmAuthorLDAPDN(), settings,getAllServiceAccounts(),commit.getScmAuthor(),collectionCombined.stream().collect(Collectors.toList()),true,reviewAuditResponseV2)) {
            reviewAuditResponseV2.addAuditStatus(CodeReviewAuditStatus.COMMITAUTHOR_EQ_SERVICEACCOUNT);
            auditIncrementVersionTag(reviewAuditResponseV2, commit, CodeReviewAuditStatus.DIRECT_COMMIT_NONCODE_CHANGE_SERVICE_ACCOUNT);
        } else  if (StringUtils.isBlank(commit.getScmAuthorLDAPDN())) {
           auditIncrementVersionTag(reviewAuditResponseV2, commit, CodeReviewAuditStatus.DIRECT_COMMIT_NONCODE_CHANGE);
        }else {
            auditIncrementVersionTag(reviewAuditResponseV2, commit, CodeReviewAuditStatus.DIRECT_COMMIT_NONCODE_CHANGE_USER_ACCOUNT);
        }
    }

    protected void auditIncrementVersionTag(CodeReviewAuditResponseV2 reviewAuditResponseV2, Commit commit, CodeReviewAuditStatus directCommitIncrementVersionTagStatus) {
        if (CommonCodeReview.matchIncrementVersionTag(commit.getScmCommitLog(), settings)) {
            reviewAuditResponseV2.addAuditStatus(directCommitIncrementVersionTagStatus);
        } else {
           addDirectCommitsToBase(reviewAuditResponseV2,commit);
        }
    }

    private void addDirectCommitsToBase(CodeReviewAuditResponseV2 reviewAuditResponseV2,Commit commit){
        if(commit.isFirstEverCommit()){
            reviewAuditResponseV2.addAuditStatus(CodeReviewAuditStatus.DIRECT_COMMITS_TO_BASE_FIRST_COMMIT );
        }else if(StringUtils.isEmpty(commit.getPullNumber()) && !isCommitHasMergedPR(commit)){
                reviewAuditResponseV2.addAuditStatus(CodeReviewAuditStatus.DIRECT_COMMITS_TO_BASE);
                reviewAuditResponseV2.addDirectCommitsToBase(commit);
        }
   }

    /**
     * Checks whether a commit is associated with any merged pull request
     * Github Default : Commit is auto merged with no PR association if it is previously merged with another PR
     * Additional check for Direct Commit
     */
    private boolean isCommitHasMergedPR(Commit commit) {
        if (Objects.nonNull(commit)) {
            List commits = commitRepository.findAllByScmRevisionNumberAndScmAuthorIgnoreCaseAndScmCommitLogAndScmCommitTimestamp(
                    commit.getScmRevisionNumber(), commit.getScmAuthor(), commit.getScmCommitLog(), commit.getScmCommitTimestamp());

            if(CollectionUtils.isNotEmpty(commits)) {
                return commits.stream().filter(c1 -> StringUtils.isNotEmpty(c1.getPullNumber())).anyMatch(c2 -> {
                    GitRequest pr = gitRequestRepository.findByCollectorItemIdAndNumber(c2.getCollectorItemId(), c2.getPullNumber());
                    return (Objects.nonNull(pr) && "merged".equalsIgnoreCase(pr.getState()));
                });
            }
        }
        return false;
    }

    public Map getAllServiceAccounts(){
        List serviceAccounts = (List) serviceAccountRepository.findAll();
        return serviceAccounts.stream().collect(Collectors.toMap(ServiceAccount :: getServiceAccountName, ServiceAccount::getFileNames));
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy