com.google.copybara.git.ChangeReader.java Source code

Java tutorial

Introduction

Here is the source code for com.google.copybara.git.ChangeReader.java

Source

/*
 * Copyright (C) 2016 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.copybara.git;

import static com.google.common.base.Preconditions.checkNotNull;

import com.google.common.base.Preconditions;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.copybara.Change;
import com.google.copybara.LabelFinder;
import com.google.copybara.RepoException;
import com.google.copybara.authoring.Author;
import com.google.copybara.authoring.AuthorParser;
import com.google.copybara.authoring.Authoring;
import com.google.copybara.authoring.InvalidAuthorException;
import com.google.copybara.util.Glob;
import com.google.copybara.util.console.Console;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;

/**
 * Utility class to introspect the log of a Git repository.
 */
class ChangeReader {

    @Nullable
    private final Authoring authoring;
    private final GitRepository repository;
    private final Console console;
    private final boolean verbose;
    private final int limit;
    private final ImmutableList<String> roots;
    private final boolean includeBranchCommitLogs;

    private ChangeReader(@Nullable Authoring authoring, GitRepository repository, Console console, boolean verbose,
            int limit, Iterable<String> roots, boolean includeBranchCommitLogs) {
        this.authoring = authoring;
        this.repository = checkNotNull(repository, "repository");
        this.console = checkNotNull(console, "console");
        this.verbose = verbose;
        this.limit = limit;
        this.roots = ImmutableList.copyOf(roots);
        this.includeBranchCommitLogs = includeBranchCommitLogs;
    }

    private String runLog(Iterable<String> params) throws RepoException {
        List<String> fullParams = new ArrayList<>(Arrays.asList("log", "--no-color", "--date=iso-strict"));
        Iterables.addAll(fullParams, params);
        if (!roots.get(0).isEmpty()) {
            fullParams.add("--");
            fullParams.addAll(roots);
        }
        return repository.simpleCommand(fullParams.toArray(new String[0])).getStdout();
    }

    ImmutableList<GitChange> run(String refExpression) throws RepoException {
        List<String> params = new ArrayList<>();

        if (limit != -1) {
            params.add("-" + limit);
        }

        params.add("--parents");
        params.add("--first-parent");

        params.add(refExpression);

        return parseChanges(runLog(params));
    }

    static final String BRANCH_COMMIT_LOG_HEADING = "-- Branch commit log --";

    private CharSequence branchCommitLog(GitReference ref, List<GitReference> parents) throws RepoException {
        if (parents.size() <= 1) {
            // Not a merge commit, so don't bother showing full log of branch commits. This would only
            // contain the raw commit of 'ref', which will be redundant.
            return "";
        }
        if (!includeBranchCommitLogs) {
            return "";
        }

        return new StringBuilder().append("\n").append(BRANCH_COMMIT_LOG_HEADING).append("\n")
                .append(runLog(ImmutableList.of(parents.get(0) + ".." + ref)));
    }

    private ImmutableList<GitChange> parseChanges(String log) throws RepoException {
        // No changes. We cannot know until we run git log since fromRef can be null (HEAD)
        if (log.isEmpty()) {
            return ImmutableList.of();
        }

        Iterator<String> rawLines = Splitter.on('\n').split(log).iterator();
        ImmutableList.Builder<GitChange> builder = ImmutableList.builder();

        while (rawLines.hasNext()) {
            String rawCommitLine = rawLines.next();
            Iterator<String> commitReferences = Splitter.on(" ").split(removePrefix(log, rawCommitLine, "commit"))
                    .iterator();

            GitReference ref = repository.createReferenceFromCompleteSha1(commitReferences.next());
            ArrayList<GitReference> parents = new ArrayList<>();
            while (commitReferences.hasNext()) {
                parents.add(repository.createReferenceFromCompleteSha1(commitReferences.next()));
            }
            String line = rawLines.next();
            Author author = null;
            ZonedDateTime dateTime = null;
            while (!line.isEmpty()) {
                if (line.startsWith("Author: ")) {
                    String authorStr = line.substring("Author: ".length()).trim();
                    Author parsedUser;
                    try {
                        parsedUser = AuthorParser.parse(authorStr);
                    } catch (InvalidAuthorException e) {
                        throw new RepoException("Invalid author found in Git history.", e);
                    }
                    if (authoring == null || authoring.useAuthor(parsedUser.getEmail())) {
                        author = parsedUser;
                    } else {
                        author = authoring.getDefaultAuthor();
                    }
                } else if (line.startsWith("Date: ")) {
                    dateTime = ZonedDateTime.parse(line.substring("Date: ".length()).trim());
                }
                line = rawLines.next();
            }
            Preconditions.checkState(author != null || dateTime != null,
                    "Could not find author and/or date for commitReferences %s in log\n:%s", rawCommitLine, log);
            StringBuilder message = new StringBuilder();
            // Maintain labels in order just in case we print them back in the destination.
            Map<String, String> labels = new LinkedHashMap<>();
            while (rawLines.hasNext()) {
                String s = rawLines.next();
                if (!s.startsWith(GitOrigin.GIT_LOG_COMMENT_PREFIX)) {
                    break;
                }
                LabelFinder labelFinder = new LabelFinder(s.substring(GitOrigin.GIT_LOG_COMMENT_PREFIX.length()));
                if (labelFinder.isLabel()) {
                    String previous = labels.put(labelFinder.getName(), labelFinder.getValue());
                    if (previous != null && verbose) {
                        console.warn(String.format(
                                "Possible duplicate label '%s' happening multiple times"
                                        + " in commit. Keeping only the last value: '%s'\n  Discarded value: '%s'",
                                labelFinder.getName(), labelFinder.getValue(), previous));
                    }
                }
                message.append(s, GitOrigin.GIT_LOG_COMMENT_PREFIX.length(), s.length()).append("\n");
            }
            message.append(branchCommitLog(ref, parents));
            Change<GitReference> change = new Change<>(ref, author, message.toString(), dateTime,
                    ImmutableMap.copyOf(labels));
            builder.add(new GitChange(change, parents));
        }
        // Return older commit first.
        return builder.build().reverse();
    }

    private String removePrefix(String log, String line, String prefix) {
        Preconditions.checkState(line.startsWith(prefix), "Cannot find '%s' in:\n%s", prefix, log);
        return line.substring(prefix.length()).trim();
    }

    /**
     * An enhanced version of Change that contains the git parents.
     */
    static class GitChange {

        private final Change<GitReference> change;
        private final ImmutableList<GitReference> parents;

        GitChange(Change<GitReference> change, Iterable<GitReference> parents) {
            this.change = change;
            this.parents = ImmutableList.copyOf(parents);
        }

        public Change<GitReference> getChange() {
            return change;
        }

        public ImmutableList<GitReference> getParents() {
            return parents;
        }
    }

    /**
     * Builder for ChangeReader.
     */
    static class Builder {
        private Authoring authoring = null;
        private GitRepository repository;
        private Console console;
        private boolean verbose = false;
        private int limit = -1;
        private ImmutableList<String> roots = ImmutableList.of("");
        private boolean includeBranchCommitLogs = false;

        // TODO(matvore): Consider adding destinationFiles.
        // For ALL_FILES and where roots is [""], This will skip merges that don't affect the tree
        // For other cases, this will skip merges and commits that don't affect a subtree
        static Builder forDestination(GitRepository repository, Console console) {
            return new Builder(repository, console);
        }

        static Builder forOrigin(Authoring authoring, GitRepository repository, Console console, Glob originFiles) {
            return new Builder(repository, console).setAuthoring(authoring).setRoots(originFiles.roots());
        }

        private Builder(GitRepository repository, Console console) {
            this.repository = checkNotNull(repository, "repository");
            this.console = checkNotNull(console, "console");
        }

        Builder setLimit(int limit) {
            Preconditions.checkArgument(limit > 0);
            this.limit = limit;
            return this;
        }

        private Builder setAuthoring(Authoring authoring) {
            this.authoring = checkNotNull(authoring, "authoring");
            return this;
        }

        Builder setVerbose(boolean verbose) {
            this.verbose = verbose;
            return this;
        }

        Builder setIncludeBranchCommitLogs(boolean includeBranchCommitLogs) {
            this.includeBranchCommitLogs = includeBranchCommitLogs;
            return this;
        }

        private Builder setRoots(Iterable<String> roots) {
            this.roots = ImmutableList.copyOf(roots);
            return this;
        }

        ChangeReader build() {
            return new ChangeReader(authoring, repository, console, verbose, limit, roots, includeBranchCommitLogs);
        }
    }

}