koraktor/mavanagaiata

View on GitHub
src/main/java/com/github/koraktor/mavanagaiata/git/MailMap.java

Summary

Maintainability
B
4 hrs
Test Coverage
A
94%
/*
 * This code is free software; you can redistribute it and/or modify it under
 * the terms of the new BSD License.
 *
 * Copyright (c) 2014-2018, Sebastian Staudt
 */

package com.github.koraktor.mavanagaiata.git;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.AbstractMap;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * An implementation of Git's {@code .mailmap} functionality
 *
 * @author Sebastian Staudt
 */
public class MailMap {

    static final String MAILMAP_FILE = ".mailmap";
    private static final Pattern MAIL_TO_MAIL_PATTERN;
    private static final Pattern MAIL_TO_NAME_PATTERN;
    private static final Pattern MAIL_TO_NAME_AND_MAIL_PATTERN;
    private static final Pattern NAME_AND_MAIL_TO_NAME_AND_MAIL_PATTERN;

    static {
        MAIL_TO_MAIL_PATTERN = Pattern.compile("^<(\\S+)>\\s+<(\\S+)>$");
        MAIL_TO_NAME_PATTERN = Pattern.compile("^(\\S.*?)\\s+<(\\S+)>$");
        MAIL_TO_NAME_AND_MAIL_PATTERN = Pattern.compile("^(\\S.*?)\\s+<(\\S+)>\\s+<(\\S+)>$");
        NAME_AND_MAIL_TO_NAME_AND_MAIL_PATTERN = Pattern.compile("^(\\S.*?)\\s+<(\\S+)>\\s+(\\S.*?)\\s+<(\\S+)>$");
    }

    boolean exists;

    Map<String, String> mailToMailMap;

    Map<String, String> mailToNameMap;

    Map<String, Map.Entry<String, String>> mailToNameAndMailMap;

    Map<Map.Entry<String, String>, Map.Entry<String, String>> nameAndMailToNameAndMailMap;

    GitRepository repository;

    /**
     * Creates a new mail map instance
     *
     * @param repository The Git repository to parse the mail map for
     * @see #parseMailMap
     */
    MailMap(GitRepository repository) {
        this.repository = repository;

        mailToMailMap = new HashMap<>();
        mailToNameMap = new HashMap<>();
        mailToNameAndMailMap = new HashMap<>();
        nameAndMailToNameAndMailMap = new HashMap<>();
    }

    /**
     * Returns whether a mail map has been found for the repository
     *
     * @return {@code true} if the mail map has been parsed from an existing
     *         {@code .mailmap} file
     * @see #parseMailMap
     */
    public boolean exists() {
        return exists;
    }

    /**
     * Returns the canonical email address for the given name and email address
     * pair
     *
     * @param name The actual name from a commit
     * @param mail The actual email address from a commit
     * @return The email address matching a mapping in the mail map or the
     *         initial email address
     */
    String getCanonicalMail(String name, String mail) {
        if (mailToMailMap.containsKey(mail)) {
            return mailToMailMap.get(mail);
        }

        if (mailToNameAndMailMap.containsKey(mail)) {
            return mailToNameAndMailMap.get(mail).getValue();
        }

        Map.Entry<String, String> nameAndMail = new AbstractMap.SimpleEntry<>(name, mail);
        if (nameAndMailToNameAndMailMap.containsKey(nameAndMail)) {
            return nameAndMailToNameAndMailMap.get(nameAndMail).getValue();
        }

        return mail;
    }

    /**
     * Returns the canonical name for the given name and email address pair
     *
     * @param name The actual name from a commit
     * @param mail The actual email address from a commit
     * @return The name matching a mapping in the mail map or the initial name
     */
    String getCanonicalName(String name, String mail) {
        if (mailToNameMap.containsKey(mail)) {
            return mailToNameMap.get(mail);
        }

        if (mailToNameAndMailMap.containsKey(mail)) {
            return mailToNameAndMailMap.get(mail).getKey();
        }

        Map.Entry<String, String> nameAndMail = new AbstractMap.SimpleEntry<>(name, mail);
        if (nameAndMailToNameAndMailMap.containsKey(nameAndMail)) {
            return nameAndMailToNameAndMailMap.get(nameAndMail).getKey();
        }

        return name;
    }

    /**
     * Returns the canonical email address of the author of the given commit
     * object
     *
     * @param commit The commit object to get the email address from
     * @return The canonical email address of the author
     * @see #getCanonicalMail(String, String)
     */
    public String getCanonicalAuthorEmailAddress(GitCommit commit) {
        return getCanonicalMail(commit.getAuthorName(), commit.getAuthorEmailAddress());
    }

    /**
     * Returns the canonical name of the author of the given commit
     * object
     *
     * @param commit The commit object to get the name from
     * @return The canonical name of the author
     * @see #getCanonicalName(String, String)
     */
    public String getCanonicalAuthorName(GitCommit commit) {
        return getCanonicalName(commit.getAuthorName(), commit.getAuthorEmailAddress());
    }

    /**
     * Returns the canonical email address of the committer of the given commit
     * object
     *
     * @param commit The commit object to get the email address from
     * @return The canonical email address of the author
     * @see #getCanonicalMail(String, String)
     */
    public String getCanonicalCommitterEmailAddress(GitCommit commit) {
        return getCanonicalMail(commit.getCommitterName(), commit.getCommitterEmailAddress());
    }

    /**
     * Returns the canonical name of the committer of the given commit
     * object
     *
     * @param commit The commit object to get the name from
     * @return The canonical name of the author
     * @see #getCanonicalName(String, String)
     */
    public String getCanonicalCommitterName(GitCommit commit) {
        return getCanonicalName(commit.getCommitterName(), commit.getCommitterEmailAddress());
    }

    /**
     * Tries to parse the {@code .mailmap} file from the worktree of the given
     * repository.
     * <br>
     * If the file exists and contains valid content {@link #exists()} will
     * return {@code true}.
     *
     * @see #parseMailMap(File)
     * @throws GitRepositoryException if the {@code .mailmap} file cannot be
     *         read
     */
    void parseMailMap() throws GitRepositoryException {
        File mailMap = new File(repository.getWorkTree(), MAILMAP_FILE);

        try {
            parseMailMap(mailMap);
            exists = !(mailToMailMap.isEmpty() &&
                    mailToNameMap.isEmpty() &&
                    mailToNameAndMailMap.isEmpty() &&
                    nameAndMailToNameAndMailMap.isEmpty());
        } catch (FileNotFoundException ignored) {
            // Ignore non-existant .mailmap
        } catch (IOException e) {
            throw new GitRepositoryException("Error while parsing the .mailmap.", e);
        }
    }

    /**
     * Tries to parse the given file using the rules from
     * <a href="http://git-scm.com/docs/git-shortlog">git-shortlog</a>.
     * <br>
     * Lines not matching one of the valid formats are silently ignored.
     *
     * @param mailMap The {@code .mailmap} file to parse
     * @throws FileNotFoundException if the {@code .mailmap} file does not
     *         exist
     * @throws IOException if the {@code .mailmap} file cannot be read
     */
    void parseMailMap(File mailMap) throws IOException {
        try (BufferedReader mailMapReader = new BufferedReader(new FileReader(mailMap))) {
            String line;
            while ((line = mailMapReader.readLine()) != null) {
                line = line.trim();

                if (line.isEmpty() || line.charAt(0) == '#') {
                    continue;
                }

                Matcher lineMatcher = NAME_AND_MAIL_TO_NAME_AND_MAIL_PATTERN.matcher(line);
                if (lineMatcher.matches()) {
                    Map.Entry<String, String> properNameAndMail = new AbstractMap.SimpleEntry<>(lineMatcher.group(1), lineMatcher.group(2));
                    Map.Entry<String, String> commitNameAndMail = new AbstractMap.SimpleEntry<>(lineMatcher.group(3), lineMatcher.group(4));
                    nameAndMailToNameAndMailMap.put(commitNameAndMail, properNameAndMail);
                    continue;
                }

                lineMatcher = MAIL_TO_NAME_AND_MAIL_PATTERN.matcher(line);
                if (lineMatcher.matches()) {
                    Map.Entry<String, String> properNameAndMail = new AbstractMap.SimpleEntry<>(lineMatcher.group(1), lineMatcher.group(2));
                    mailToNameAndMailMap.put(lineMatcher.group(3), properNameAndMail);
                    continue;
                }

                lineMatcher = MAIL_TO_MAIL_PATTERN.matcher(line);
                if (lineMatcher.matches()) {
                    mailToMailMap.put(lineMatcher.group(2), lineMatcher.group(1));
                    continue;
                }

                lineMatcher = MAIL_TO_NAME_PATTERN.matcher(line);
                if (lineMatcher.matches()) {
                    mailToNameMap.put(lineMatcher.group(2), lineMatcher.group(1));
                }
            }
        }
    }

}