com.google.devtools.moe.client.codebase.CodebaseMerger.java Source code

Java tutorial

Introduction

Here is the source code for com.google.devtools.moe.client.codebase.CodebaseMerger.java

Source

/*
 * Copyright (c) 2011 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.devtools.moe.client.codebase;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.google.devtools.moe.client.CommandRunner;
import com.google.devtools.moe.client.CommandRunner.CommandException;
import com.google.devtools.moe.client.FileSystem;
import com.google.devtools.moe.client.MoeProblem;
import com.google.devtools.moe.client.Ui;
import com.google.devtools.moe.client.parser.RepositoryExpression;
import com.google.devtools.moe.client.parser.Term;
import com.google.devtools.moe.client.tools.FileDifference.FileDiffer;

import java.io.File;
import java.io.IOException;
import java.util.Set;

/**
 * Merges all changes that lead from {@code originalCodebase} to {@code modifiedCodebase} into
 * destinationCodebase when its {@link #merge()} method is invoked.
 *
 * <p>Here is a description of the UNIX merge(1) tool from its man page:
 * <pre>{@code
 *
 *   merge [ options ] file1 file2 file3
 *
 *   merge incorporates all changes that lead from file2 to file3 into file1.  The result
 *   ordinarily goes into file1.  merge is useful for combining separate changes to an original.
 *   Suppose file2 is the original, and both file1 and file3 are modifications of file2.  Then
 *   merge combines both changes.
 *
 * }</pre>
 *
 * <p>{@link CodebaseMerger#merge()} performs this type of merge on each file in the three
 * codebases. In {@link CodebaseMerger#merge}, {@code originalCodebase} is analogous to
 * {@code file2}, {@code modifiedCodebase} is analogous to {@code file3}, and
 * {@code destinationCodebase} is analogous to {@code file1}. The output of
 * {@link CodebaseMerger#merge()} is a codebase that incorporates the changes that both
 * {@code modifiedCodebase} and {@code destinationCodebase} made on the {@code originalCodebase}.
 * The differences between {@code modifiedCodebase} and the {@code originalCodebase} are brought
 * into a copy of {@code destinationCodebase}. The result is the merged codebase.
 *
 * <p>This is useful when bringing changes to the public repository into the internal repository.
 * For example, say you run:
 *
 * <pre>{@code
 *    merge_codebases --originalCodebase "publicrepo(revision=142)"
 *                    --modifiedCodebase "publicrepo(revision=143)"
 *                    --destinationCodebase "internalrepo(revision=74)"
 * }</pre>
 *
 * <p>Let internalrepo(revision=74) be in equivalence with publicrepo(revision=142). That is, let
 * publicrepo(revision=142) represent the same state of the code as internalrepo(revision=74)
 * minus any confidential code that may have been scrubbed during translation. That means that
 * publicrepo(revision=143) is a change to the public repository which has yet to be brought to
 * the internal repository. By running the above merge_codebases, the changes from the public
 * revision 142 to 143 will be merged into a copy of internal revision 74. The result is an
 * internal revision 75 which has the new public changes and still has the confidential code that
 * a public revision wouldn't have. Thus, internal revision 75 would be equivalent with public
 * revision 143 assuming there were no conflicts when merging.
 */
// TODO(cgruber) AutoFactory or split out a MergeResult object with metadata/reporting.
public class CodebaseMerger {
    private final Ui ui;
    private final FileSystem filesystem;
    private final CommandRunner cmd;
    private final FileDiffer differ;
    private final Codebase originalCodebase, destinationCodebase, modifiedCodebase, mergedCodebase;
    private final Set<String> mergedFiles, failedToMergeFiles;

    public CodebaseMerger(Ui ui, FileSystem filesystem, CommandRunner cmd, FileDiffer differ,
            Codebase originalCodebase, Codebase modifiedCodebase, Codebase destinationCodebase) {
        this.ui = ui;
        this.filesystem = filesystem;
        this.cmd = cmd;
        this.differ = differ;
        this.originalCodebase = originalCodebase;
        this.modifiedCodebase = modifiedCodebase;
        this.destinationCodebase = destinationCodebase;

        File mergedDir = filesystem.getTemporaryDirectory("merged_codebase_");
        RepositoryExpression mergedExpression = new RepositoryExpression(
                new Term("merged", ImmutableMap.<String, String>of()));
        this.mergedCodebase = new Codebase(filesystem, mergedDir, "merged", mergedExpression);

        mergedFiles = Sets.newHashSet();
        failedToMergeFiles = Sets.newHashSet();
    }

    public Set<String> getMergedFiles() {
        return ImmutableSet.copyOf(mergedFiles);
    }

    public Set<String> getFailedToMergeFiles() {
        return ImmutableSet.copyOf(failedToMergeFiles);
    }

    /**
     * For each file in the union of the modified and destination codebases, run
     * generateMergedFile(...) and then report() the results.
     *
     * @return the merged Codebase
     */
    public Codebase merge() {
        Set<String> filesToMerge = Sets.union(destinationCodebase.getRelativeFilenames(),
                modifiedCodebase.getRelativeFilenames());
        for (String filename : filesToMerge) {
            this.generateMergedFile(filename);
        }
        this.report();
        return mergedCodebase;
    }

    /**
     * Print the results of a merge to the UI.
     */
    public void report() {
        ui.message("Merged codebase generated at: %s", mergedCodebase.getPath().getAbsolutePath());
        if (failedToMergeFiles.isEmpty()) {
            ui.message("%d files merged successfully. No merge conflicts.", mergedFiles.size());
        } else {
            ui.message(
                    "%d files merged successfully.\n%d files have merge "
                            + "conflicts. Edit the following files to resolve conflicts:\n%s",
                    mergedFiles.size(), failedToMergeFiles.size(), failedToMergeFiles);
        }
    }

    private boolean areDifferent(String filename, File x, File y) {
        return differ.diffFiles(filename, x, y).isDifferent();
    }

    /**
     * Copy the destFile into the merged codebase. This is where the output of merge will be
     * written to.
     */
    private File copyToMergedCodebase(String filename, File destFile) {
        File mergedFile = mergedCodebase.getFile(filename);
        try {
            filesystem.makeDirsForFile(mergedFile);
            filesystem.copyFile(destFile, mergedFile);
            return mergedFile;
        } catch (IOException e) {
            throw new MoeProblem(e.getMessage());
        }
    }

    /**
     * Given a filename, this method finds the file with that name in each of the three codebases.
     * Using the UNIX merge(1) tool, those three files are merged and the result is placed in the
     * merged codebase. Any conflicts that occurred during merging will appear in the merged codebase
     * file for the user to resolve.
     *
     * <p>In the case where the file specified by the given filename exists in the original codebase
     * and in either the modified codebase or the destination codebase (but not both) and if the file
     * is unchanged between those codebases, then a file in the merged codebase will NOT be created
     * and this method will return leaving the merged codebase unchanged.
     *
     * @param filename the name of the file to merge
     */
    public void generateMergedFile(String filename) {
        File origFile = originalCodebase.getFile(filename);
        boolean origExists = filesystem.exists(origFile);

        File destFile = destinationCodebase.getFile(filename);
        boolean destExists = filesystem.exists(destFile);

        File modFile = modifiedCodebase.getFile(filename);
        boolean modExists = filesystem.exists(modFile);

        if (!destExists && !modExists) {
            // This should never be thrown since generateMergedFile(...) is only called on filesToMerge
            // from merge() which is the union of the files in the destination and modified codebases.
            throw new MoeProblem("%s doesn't exist in either %s nor %s. This should not be possible.", filename,
                    destinationCodebase, modifiedCodebase);

        } else if (origExists && modExists && !destExists) {
            if (areDifferent(filename, origFile, modFile)) {
                // Proceed and merge in /dev/null, which should produce a merge conflict (incoming edit on
                // delete).
                destFile = new File("/dev/null");
            } else {
                // Defer to deletion in destination codebase.
                return;
            }

        } else if (origExists && !modExists && destExists) {
            // Blindly follow deletion of the original file by not copying it into the merged codebase.
            return;

        } else if (!origExists && !(modExists && destExists)) {
            // File exists only in modified or destination codebase, so just copy it over.
            File existingFile = (modExists ? modFile : destFile);
            copyToMergedCodebase(filename, existingFile);
            return;

        } else if (!origExists && modExists && destExists) {
            // Merge both new files (conflict expected).
            origFile = new File("/dev/null");
        }

        File mergedFile = copyToMergedCodebase(filename, destFile);

        try {
            // Merges the changes that lead from origFile to modFile into mergedFile (which is a copy
            // of destFile). After, mergedFile will have the combined changes of modFile and destFile.
            cmd.runCommand("merge", ImmutableList.of(mergedFile.getAbsolutePath(), origFile.getAbsolutePath(),
                    modFile.getAbsolutePath()), this.mergedCodebase.getPath().getAbsolutePath());
            // Return status was 0 and the merge was successful. Note it.
            mergedFiles.add(mergedFile.getAbsolutePath());
        } catch (CommandException e) {
            // If merge fails with exit status 1, then a conflict occurred. Make a note of the filepath.
            if (e.returnStatus == 1) {
                failedToMergeFiles.add(mergedFile.getAbsolutePath());
            } else {
                throw new MoeProblem(
                        "Merge returned with unexpected status %d when trying to run \"merge -p %s %s %s\"",
                        e.returnStatus, destFile.getAbsolutePath(), origFile.getAbsolutePath(),
                        modFile.getAbsolutePath());
            }
        }
    }
}