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

com.capitalone.dashboard.common.CommonCodeReview Maven / Gradle / Ivy

package com.capitalone.dashboard.common;

import com.capitalone.dashboard.ApiSettings;
import com.capitalone.dashboard.model.CodeAction;
import com.capitalone.dashboard.model.CodeActionType;
import com.capitalone.dashboard.model.CollectorItem;
import com.capitalone.dashboard.model.Comment;
import com.capitalone.dashboard.model.Commit;
import com.capitalone.dashboard.model.CommitStatus;
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.CommitRepository;
import com.capitalone.dashboard.repository.ServiceAccountRepository;
import com.capitalone.dashboard.response.AuditReviewResponse;
import com.capitalone.dashboard.response.CodeReviewAuditResponse;
import com.capitalone.dashboard.response.CodeReviewAuditResponseV2;
import com.capitalone.dashboard.status.CodeReviewAuditStatus;
import com.capitalone.dashboard.util.GitHubParsedUrl;
import com.google.common.collect.Sets;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ldap.support.LdapUtils;

import javax.naming.InvalidNameException;
import javax.naming.ldap.LdapName;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

public class CommonCodeReview {

    private static final Logger LOGGER = LoggerFactory.getLogger(CommonCodeReview.class);

    /**
     * Calculates the peer review status for a given pull request
     *
     * @param pr                  - pull request
     * @param auditReviewResponse - audit review response
     * @return boolean fail or pass
     */
    public static boolean computePeerReviewStatus(GitRequest pr, ApiSettings settings,
                                                  AuditReviewResponse auditReviewResponse,
                                                  List commits,
                                                  CommitRepository commitRepository, ServiceAccountRepository serviceAccountRepository) {
        List reviews = pr.getReviews();

        List statuses = pr.getCommitStatuses();

        Map actors = getActors(pr);

        List serviceAccounts = (List) serviceAccountRepository.findAll();
        Map accounts =  serviceAccounts.stream().collect(Collectors.toMap(ServiceAccount :: getServiceAccountName, ServiceAccount::getFileNames));

        /**
         * Native Github Reviews take Higher priority so check for GHR, if not found check for LGTM.
         */

        if (!CollectionUtils.isEmpty(reviews)) {
            for (Review review : reviews) {
                if (StringUtils.equalsIgnoreCase("approved", review.getState())) {
                    if (!StringUtils.isEmpty(review.getAuthorLDAPDN()) && checkForServiceAccount(review.getAuthorLDAPDN(), settings,accounts,review.getAuthor(),null,false,auditReviewResponse)) {
                        auditReviewResponse.addAuditStatus(CodeReviewAuditStatus.PEER_REVIEW_BY_SERVICEACCOUNT);
                    }
                    //review done using GitHub Review workflow
                    auditReviewResponse.addAuditStatus(CodeReviewAuditStatus.PEER_REVIEW_GHR);
                    if (!CollectionUtils.isEmpty(auditReviewResponse.getAuditStatuses()) &&
                            !isPRReviewedInTimeScale(pr, auditReviewResponse, commits, commitRepository)) {
                        auditReviewResponse.addAuditStatus(CodeReviewAuditStatus.PEER_REVIEW_GHR_SELF_APPROVAL);
                        return Boolean.FALSE;
                    }
                    return Boolean.TRUE;
                }
            }
        }

        /**
         * If there are no Native Github reviews, Check for LGTM.
         */

        if (!CollectionUtils.isEmpty(statuses)) {
            String contextString = settings.getPeerReviewContexts();
            Set prContexts = StringUtils.isEmpty(contextString) ? new HashSet<>() : Sets.newHashSet(contextString.trim().split(","));

            boolean lgtmAttempted = false;
            boolean lgtmStateResult = false;

            for (CommitStatus status : statuses) {
                if (status.getContext() != null && prContexts.contains(status.getContext())) {
                    //review done using LGTM workflow assuming its in the settings peerReviewContexts
                    lgtmAttempted = true;
                    String stateString = (status.getState() != null) ? status.getState().toLowerCase() : "unknown";
                    switch (stateString) {
                        case "pending":
                            auditReviewResponse.addAuditStatus(CodeReviewAuditStatus.PEER_REVIEW_LGTM_PENDING);
                            break;

                        case "error":
                            auditReviewResponse.addAuditStatus(CodeReviewAuditStatus.PEER_REVIEW_LGTM_PENDING);
                            break;

                        case "success":
                            lgtmStateResult = true;
                            auditReviewResponse.addAuditStatus(CodeReviewAuditStatus.PEER_REVIEW_LGTM_SUCCESS);

                            String description = status.getDescription();
                            if (!CollectionUtils.isEmpty(settings.getServiceAccountOU()) && !StringUtils.isEmpty(settings.getPeerReviewApprovalText()) && !StringUtils.isEmpty(description) &&
                                    description.startsWith(settings.getPeerReviewApprovalText())) {
                                String user = description.replace(settings.getPeerReviewApprovalText(), "").trim();
                                if (!StringUtils.isEmpty(actors.get(user)) && checkForServiceAccount(actors.get(user), settings,accounts,user,null,false,auditReviewResponse)) {
                                    auditReviewResponse.addAuditStatus(CodeReviewAuditStatus.PEER_REVIEW_BY_SERVICEACCOUNT);
                                }
                            }
                            break;

                        default:
                            auditReviewResponse.addAuditStatus(CodeReviewAuditStatus.PEER_REVIEW_LGTM_UNKNOWN);
                            break;
                    }
                }
            }

            if (lgtmAttempted) {
                //if lgtm self-review, then no peer-review was done unless someone else looked at it
                if (!CollectionUtils.isEmpty(auditReviewResponse.getAuditStatuses()) &&
                        !isPRReviewedInTimeScale(pr, auditReviewResponse, commits, commitRepository)) {
                    auditReviewResponse.addAuditStatus(CodeReviewAuditStatus.PEER_REVIEW_LGTM_SELF_APPROVAL);
                    return false;
                }
                return lgtmStateResult;
            }
        }


        return false;
    }

    /**
     * Check if the passed in account is a Service Account or not by comparing
     * against list of valid ServiceAccountOU in ApiSettings.
     *
     * @param userLdapDN
     * @param settings
     * @return
     */
    public static boolean checkForServiceAccount(String userLdapDN, ApiSettings settings,Map allowedUsers,String author,List commitFiles,boolean isCommit,AuditReviewResponse auditReviewResponse) {
        List serviceAccountOU = settings.getServiceAccountOU();
        boolean isValid = false;
        if(!MapUtils.isEmpty(allowedUsers) && isCommit){
            isValid = isValidServiceAccount(author,allowedUsers,commitFiles);
            if(isValid){
                auditReviewResponse.addAuditStatus(CodeReviewAuditStatus.DIRECT_COMMIT_CHANGE_WHITELISTED_ACCOUNT);
            }
        }
        if (!CollectionUtils.isEmpty(serviceAccountOU) && StringUtils.isNotBlank(userLdapDN) && !isValid) {
            try {
                String userLdapDNParsed = LdapUtils.getStringValue(new LdapName(userLdapDN), "OU");
                List matches = serviceAccountOU.stream().filter(it -> it.contains(userLdapDNParsed)).collect(Collectors.toList());
                isValid =  CollectionUtils.isNotEmpty(matches);
            } catch (InvalidNameException e) {
                LOGGER.error("Error parsing LDAP DN:" + userLdapDN);
            }
        }
        else {
            LOGGER.info("API Settings missing service account RDN");
        }
        return isValid;
    }

    private static boolean isValidServiceAccount(String author, Map allowedServiceAccounts,List commitFiles) {

        boolean isValidServiceAccount = false;
        if (MapUtils.isEmpty(allowedServiceAccounts)) return Boolean.FALSE;
        for (String serviceAccount:allowedServiceAccounts.keySet()) {
            String fileNames = allowedServiceAccounts.get(serviceAccount);
            for (String s : fileNames.split(",")) {
                if (serviceAccount.equalsIgnoreCase(author) && findFileMatch(s, commitFiles)){
                    isValidServiceAccount = true;
                }
            }
        }
        return isValidServiceAccount;
    }

    private static boolean findFileMatch(String fileName, List files){
      if(fileName.contains("*")){
          Optional extension = getExtensionByStringHandling(fileName);
            String EXT_PATTERN = "([^\\s]+(\\.(?i)("+extension.get()+"))$)";
            java.util.function.Predicate fileFilter = Pattern.compile(EXT_PATTERN).asPredicate();
            List filesFound = files.stream().filter(fileFilter).collect(Collectors.toList());
            return filesFound.size()>0;
        }else return files.parallelStream().anyMatch(file -> file.contains(fileName));
    }

    public static Optional getExtensionByStringHandling(String filename) {
        return Optional.ofNullable(filename)
                .filter(f -> f.contains("."))
                .map(f -> f.substring(filename.lastIndexOf(".") + 1));
    }


    /**
     * Get all the actors associated with this user.s
     *
     * @param pr
     * @return
     */
    private static Map getActors(GitRequest pr) {
        Map actors = new HashMap<>();
        if (!StringUtils.isEmpty(pr.getMergeAuthorLDAPDN())) {
            actors.put(pr.getMergeAuthor(), pr.getMergeAuthorLDAPDN());
        }
        Optional.ofNullable(pr.getCommits()).orElse(Collections.emptyList()).stream().filter(c -> !StringUtils.isEmpty(c.getScmAuthorLDAPDN())).forEach(c -> actors.put(c.getScmAuthor(), c.getScmAuthorLDAPDN()));
        Optional.ofNullable(pr.getComments()).orElse(Collections.emptyList()).stream().filter(c -> !StringUtils.isEmpty(c.getUserLDAPDN())).forEach(c -> actors.put(c.getUser(), c.getUserLDAPDN()));
        Optional.ofNullable(pr.getReviews()).orElse(Collections.emptyList()).stream().filter(r -> !StringUtils.isEmpty(r.getAuthorLDAPDN())).forEach(r -> actors.put(r.getAuthor(), r.getAuthorLDAPDN()));
        return actors;
    }

    /**
     * Calculates if the PR was looked at by a peer
     *
     * @param pr
     * @return true if PR was looked at by at least one peer
     */
    private static boolean isPRLookedAtByPeer(GitRequest pr) {
        Set commentUsers = pr.getComments() != null ? pr.getComments().stream().map(Comment::getUser).collect(Collectors.toCollection(HashSet::new)) : new HashSet<>();
        Set reviewAuthors = pr.getReviews() != null ? pr.getReviews().stream().map(Review::getAuthor).collect(Collectors.toCollection(HashSet::new)) : new HashSet<>();
        reviewAuthors.remove("unknown");

        Set prCommitAuthors = pr.getCommits() != null ? pr.getCommits().stream().map(Commit::getScmAuthorLogin).collect(Collectors.toCollection(HashSet::new)) : new HashSet<>();
        prCommitAuthors.add(pr.getUserId());
        prCommitAuthors.remove("unknown");

        commentUsers.removeAll(prCommitAuthors);
        reviewAuthors.removeAll(prCommitAuthors);

        return (commentUsers.size() > 0) || (reviewAuthors.size() > 0);
    }


    private static boolean isPRReviewedInTimeScale(GitRequest pr,
                                                   AuditReviewResponse auditReviewResponse,
                                                   List commits, CommitRepository commitRepository) {
        List filteredPrCommits = new ArrayList<>();
        pr.getCommits().forEach(prC -> {
            Optional cOptionalCommit = commits.stream().filter(c -> Objects.equals(c.getScmRevisionNumber(), prC.getScmRevisionNumber())).findFirst();
            Commit cCommit = cOptionalCommit.orElse(null);

            //If not found in the list, it must be a commit in the PR from time beyond the evaluation time window.
            //In this case, look up from repository.
            if (cCommit == null) {
                cCommit =  commitRepository.findByCollectorItemIdAndScmRevisionNumber(pr.getCollectorItemId(), prC.getScmRevisionNumber());
            }

            if (cCommit != null
                    && !CollectionUtils.isEmpty(cCommit.getScmParentRevisionNumbers())
                    && cCommit.getScmParentRevisionNumbers().size() > 1) {
                //exclude commits with multiple parents ie. merge commits
            } else {
                String mergeCommitLog = String.format("Merge branch '%s' into %s", pr.getScmBranch(), pr.getSourceBranch());
                if (!prC.getScmCommitLog().contains(mergeCommitLog)) {
                    filteredPrCommits.add(prC);
                }
            }
        });

        List codeActionList = new ArrayList<>();
        if (!CollectionUtils.isEmpty(filteredPrCommits)) {
            codeActionList.addAll(filteredPrCommits.stream().map(c -> new CodeAction(CodeActionType.Commit, c.getScmCommitTimestamp(),
                    "unknown".equalsIgnoreCase(c.getScmAuthorLogin()) ? pr.getUserId() : c.getScmAuthorLogin(),
                    c.getScmAuthorLDAPDN() != null ? c.getScmAuthorLDAPDN() : "unknown", c.getScmCommitLog())).collect(Collectors.toList()));
        }
        if (!CollectionUtils.isEmpty(pr.getReviews())) {
            codeActionList.addAll(pr.getReviews().stream().map(r -> new CodeAction(CodeActionType.Review, r.getUpdatedAt(),
                    r.getAuthor(), r.getAuthorLDAPDN() != null ? r.getAuthorLDAPDN() : "unknown", r.getBody())).collect(Collectors.toList()));
        }
        if (!CollectionUtils.isEmpty(pr.getComments())) {
            codeActionList.addAll(pr.getComments().stream().map(r -> new CodeAction(CodeActionType.Review, r.getUpdatedAt(),
                    r.getUser(), r.getUserLDAPDN() != null ? r.getUserLDAPDN() : "unknown", r.getBody())).collect(Collectors.toList()));
        }

        codeActionList.add(new CodeAction(CodeActionType.PRMerge, pr.getMergedAt(), pr.getMergeAuthor(), pr.getMergeAuthorLDAPDN(), "merged"));
        codeActionList.add(new CodeAction(CodeActionType.PRCreate, pr.getCreatedAt(), pr.getUserId(), "unknown", "create"));

        codeActionList.sort(Comparator.comparing(CodeAction::getTimestamp));

        codeActionList.stream().forEach(c -> LOGGER.debug(new DateTime(c.getTimestamp()).toString("yyyy-MM-dd hh:mm:ss.SSa")
                + " " + c.getType() + " " + c.getActor() + " " + c.getMessage()));

        List clonedCodeActions = codeActionList.stream().map(CodeAction::new).collect(Collectors.toList());
        if (auditReviewResponse instanceof CodeReviewAuditResponse) {
            ((CodeReviewAuditResponse) auditReviewResponse).setCodeActions(clonedCodeActions);
        } else if (auditReviewResponse instanceof CodeReviewAuditResponseV2.PullRequestAudit) {
            ((CodeReviewAuditResponseV2.PullRequestAudit) auditReviewResponse).setCodeActions(clonedCodeActions);
        }

        Set reviewedList = new HashSet<>();
        codeActionList.stream().filter(as -> as.getType() == CodeActionType.Review).map(as -> getReviewedActions(codeActionList, as)).forEach(reviewedList::addAll);
        codeActionList.removeAll(reviewedList);
        return codeActionList.stream().noneMatch(as -> as.getType() == CodeActionType.Commit);
    }


    private static List getReviewedActions(List codeActionList, CodeAction reviewAction) {
        return codeActionList.stream()
                .filter(cal -> (cal.getType() == CodeActionType.Commit) && !reviewAction.getActor().equalsIgnoreCase(cal.getActor()))
                .collect(Collectors.toList());
    }


    public static Set getCodeAuthors(List repoItems, long beginDate,
                                             long endDate, CommitRepository commitRepository) {
        Set authors = new HashSet<>();
        //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?
            List commits = commitRepository.findByCollectorItemIdAndScmCommitTimestampIsBetween(repoItem.getId(), beginDate - 1, endDate + 1);
            authors.addAll(commits.stream().map(SCM::getScmAuthor).collect(Collectors.toCollection(HashSet::new)));
        });
        return authors;
    }

    public static boolean matchIncrementVersionTag(String commitMessage,ApiSettings settings) {
        if (StringUtils.isEmpty(settings.getCommitLogIgnoreAuditRegEx())) return false;
        Pattern pattern = Pattern.compile(settings.getCommitLogIgnoreAuditRegEx());
        return pattern.matcher(commitMessage).matches();
    }


}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy