/**
* Copyright (C) 2013-2014 Olaf Lessenich
* Copyright (C) 2014-2015 University of Passau, Germany
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*
* Contributors:
* Olaf Lessenich <lessenic@fim.uni-passau.de>
* Georg Seibt <seibt@fim.uni-passau.de>
*/
package de.fosd.jdime.strategy;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import de.fosd.jdime.artifact.file.FileArtifact;
import de.fosd.jdime.config.merge.MergeContext;
import de.fosd.jdime.config.merge.MergeScenario;
import de.fosd.jdime.operations.MergeOperation;
import de.fosd.jdime.stats.MergeScenarioStatistics;
import de.fosd.jdime.stats.Statistics;
import de.fosd.jdime.stats.parser.ParseResult;
/**
* Performs an unstructured, line based merge.
* <p>
* The current implementation uses the merge routine provided by <code>git</code>.
*
* @author Olaf Lessenich
*/
public class LinebasedStrategy extends MergeStrategy<FileArtifact> {
private static final Logger LOG = Logger.getLogger(LinebasedStrategy.class.getCanonicalName());
/**
* The command to use for merging.
*/
private static final String BASECMD = "git";
/**
* The arguments for <code>BASECMD</code>.
*/
private static final List<String> BASEARGS = Arrays.asList("merge-file", "-q", "-p");
/**
* This line-based <code>merge</code> method uses the merging routine of
* the external tool <code>git</code>.
* <p>
* Basically, the input <code>FileArtifacts</code> are passed as arguments to
* `git merge-file -q -p`.
* <p>
* In a common run, the number of processed lines of code, the number of
* conflicting situations, and the number of conflicting lines of code will
* be counted. Empty lines and comments are skipped to keep
* <code>MergeStrategies</code> comparable, as JDime does (in its current
* implementation) not respect comments.
* <p>
* In case of a performance benchmark, the output is simply ignored for the
* sake of speed, and the merge will be run the specified amount of times,
* aiming to allow the computation of a reasonable mean runtime.
*
* @param operation <code>MergeOperation</code> that is executed by this strategy
* @param context <code>MergeContext</code> that is used to retrieve environmental parameters
*/
@Override
public void merge(MergeOperation<FileArtifact> operation, MergeContext context) {
MergeScenario<FileArtifact> triple = operation.getMergeScenario();
FileArtifact target = null;
if (!context.isDiffOnly() && operation.getTarget() != null) {
target = operation.getTarget();
if (target.exists() && !target.isEmpty()) {
throw new AssertionError(String.format("Would be overwritten: %s", target));
}
}
context.resetStreams();
List<String> cmd = new ArrayList<>();
cmd.add(BASECMD);
cmd.addAll(BASEARGS);
cmd.addAll(triple.asList().stream().limit(3).map(FileArtifact::getPath).collect(Collectors.toList()));
ProcessBuilder pb = new ProcessBuilder(cmd);
LOG.fine(() -> "Running external command: " + String.join(" ", cmd));
long runtime, startTime = System.currentTimeMillis();
Process pr;
try {
pr = pb.start();
} catch (IOException e) {
throw new RuntimeException("Could not run '" + String.join(" ", cmd) + "'.", e);
}
StringBuilder processOutput = new StringBuilder();
StringBuilder processErrorOutput = new StringBuilder();
String ls = System.lineSeparator();
try (BufferedReader r = new BufferedReader(new InputStreamReader(pr.getInputStream()))) {
String line;
while ((line = r.readLine()) != null) {
processOutput.append(line).append(ls);
}
} catch (IOException e) {
LOG.log(Level.SEVERE, e, () -> "Could not fully read the process output.");
}
try (BufferedReader r = new BufferedReader(new InputStreamReader(pr.getErrorStream()))) {
String line;
while ((line = r.readLine()) != null) {
processErrorOutput.append(line).append(ls);
}
} catch (IOException e) {
LOG.log(Level.SEVERE, e, () -> "Could not fully read the process error output.");
}
context.append(processOutput.toString());
context.appendError(processErrorOutput.toString());
try {
pr.waitFor();
} catch (InterruptedException e) {
LOG.log(Level.WARNING, e, () -> "Interrupted while waiting for the external command to finish.");
}
runtime = System.currentTimeMillis() - startTime;
LOG.fine(() -> String.format("%s merge time was %d ms.", getClass().getSimpleName(), runtime));
if (context.hasErrors()) {
LOG.severe(() -> String.format("Errors occurred while calling '%s'%n%s", String.join(" ", cmd), context.getStdErr()));
}
if (!context.isPretend() && target != null) {
LOG.fine("Writing output to: " + target.getFullPath());
target.write(context.getStdIn());
}
if (context.hasStatistics()) {
Statistics statistics = context.getStatistics();
MergeScenarioStatistics scenarioStatistics = new MergeScenarioStatistics(triple);
ParseResult res = scenarioStatistics.setLineStatistics(processOutput.toString());
if (res.getConflicts() > 0) {
scenarioStatistics.getFileStatistics().incrementNumOccurInConflic();
}
scenarioStatistics.setRuntime(runtime);
statistics.addScenarioStatistics(scenarioStatistics);
}
}
}