Java tutorial
/* * The MIT License (MIT) * * Copyright (c) 2016 Alcemir R. Santos * * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and * associated documentation files (the "Software"), to deal in the Software without restriction, * including without limitation the rights to use, copy, modify, merge, publish, distribute, * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all copies or * substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ package br.com.riselabs.cotonet.builder; import java.io.File; import java.io.IOException; import java.sql.Timestamp; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import org.eclipse.jgit.api.CheckoutCommand; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.MergeCommand; import org.eclipse.jgit.api.MergeResult; import org.eclipse.jgit.api.ResetCommand.ResetType; import org.eclipse.jgit.api.errors.CheckoutConflictException; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.api.errors.JGitInternalException; import org.eclipse.jgit.errors.NoMergeBaseException; import org.eclipse.jgit.merge.MergeStrategy; import org.eclipse.jgit.merge.ThreeWayMerger; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; import br.com.riselabs.cotonet.builder.commands.ExternalGitCommand; import br.com.riselabs.cotonet.builder.commands.ExternalGitCommand.CommandType; import br.com.riselabs.cotonet.model.beans.CommandLineBlameResult; import br.com.riselabs.cotonet.model.beans.ConflictBasedNetwork; import br.com.riselabs.cotonet.model.beans.ConflictChunk; import br.com.riselabs.cotonet.model.beans.DeveloperEdge; import br.com.riselabs.cotonet.model.beans.DeveloperNode; import br.com.riselabs.cotonet.model.beans.MergeScenario; import br.com.riselabs.cotonet.model.beans.Project; import br.com.riselabs.cotonet.model.db.DBWritter; import br.com.riselabs.cotonet.model.enums.MergeCommitSide; import br.com.riselabs.cotonet.model.enums.NetworkType; import br.com.riselabs.cotonet.model.exceptions.BlameException; import br.com.riselabs.cotonet.util.Logger; /** * @author Alcemir R. Santos * @param <T> * @param <T> * */ public class NetworkBuilder<T> { protected NetworkType type; protected Project project; protected File log; public NetworkBuilder(Project project, NetworkType type) { setProject(project); setType(type); } public Project getProject() { return project; } public void setProject(Project project) { this.project = project; } public NetworkType getType() { return type; } public void setType(NetworkType type) { this.type = type; } public void setLogFile(File log) { this.log = log; } /** * Builds the conflict based network considering the previously network type * set and the repository information provided. In case the type was not set * yet, this method used the <i>default</i> type (<i>i.e.,</i> the * chunk-based {@code NetworkType.CHUNK_BASED}). * * OBS: You should set the repository first, otherwise this method will * return <code>null</code> * * @return {@code aNetwork} * * -<code>null</code> when the repository is not set. * @throws Exception */ public void build() throws IOException, CheckoutConflictException, GitAPIException, InterruptedException { Logger.log(log, "[" + project.getName() + "] Network building start."); List<MergeScenario> conflictingScenarios = getMergeScenarios(); for (MergeScenario scenario : conflictingScenarios) { ConflictBasedNetwork connet = getConflictNetwork(scenario); if (connet != null) { project.add(scenario, connet); } } Logger.log(log, "[" + project.getName() + "] Network building finished."); } /** * Triggers the persistence of the networks built for this project. */ public void persist() { Logger.log(log, "[" + project.getName() + "] Project persistence start."); DBWritter.INSTANCE.setLogFile(log); DBWritter.INSTANCE.persist(project); Logger.log(log, "[" + project.getName() + "] Project persistence finished."); } /** * Returns the conflicting merge scenarios * * @return - a list of merge scenarios. it may be empty in case of no * conflict. * @throws IOException */ private List<MergeScenario> getMergeScenarios() throws IOException { List<MergeScenario> result = new ArrayList<MergeScenario>(); List<RevCommit> mergeCommits = new ArrayList<RevCommit>(); Iterable<RevCommit> gitlog; try { Git git = Git.wrap(getProject().getRepository()); gitlog = git.log().call(); for (RevCommit commit : gitlog) { if (commit.getParentCount() == 2) { mergeCommits.add(commit); // collecting merge commits // we know there is only to parents RevCommit leftParent = commit.getParent(0); RevCommit rightParent = commit.getParent(1); ThreeWayMerger merger = MergeStrategy.RECURSIVE.newMerger(getProject().getRepository(), true); // selecting the conflicting ones boolean noConflicts = false; try { noConflicts = merger.merge(leftParent, rightParent); } catch (NoMergeBaseException e) { StringBuilder sb = new StringBuilder(); sb.append("[" + project.getName() + ":" + project.getUrl() + "] " + "Skipping merge scenario due to '" + e.getMessage() + "'\n"); sb.append("---> Skipped scenario:\n"); sb.append("::Base (<several>): \n"); sb.append("::Left (" + leftParent.getAuthorIdent().getWhen().toString() + "):" + leftParent.getName() + "\n"); sb.append("::Right (" + rightParent.getAuthorIdent().getWhen().toString() + "):" + rightParent.getName() + "\n"); Logger.log(log, sb.toString()); Logger.logStackTrace(log, e); continue; } if (noConflicts) { continue; } RevWalk walk = new RevWalk(getProject().getRepository()); // for merges without a base commit if (merger.getBaseCommitId() == null) continue; RevCommit baseCommit = walk.lookupCommit(merger.getBaseCommitId()); walk.close(); Timestamp mergeDate = new Timestamp(commit.getAuthorIdent().getWhen().getTime()); result.add(new MergeScenario(baseCommit, leftParent, rightParent, commit, mergeDate)); } } } catch (GitAPIException e) { Logger.logStackTrace(log, e); } return result; } /** * Returns the conflicting files of the given scenario. * * @param scenario * @return * @throws CheckoutConflictException * @throws GitAPIException */ private List<File> getConflictingFiles(MergeScenario scenario) throws CheckoutConflictException, GitAPIException { Git git = Git.wrap(getProject().getRepository()); // this is for the cases of restarting after exception in a conflict // scenario analysis try { git.reset().setRef(scenario.getLeft().getName()).setMode(ResetType.HARD).call(); } catch (JGitInternalException e) { Logger.log(log, "[" + project.getName() + "] JGit Reset Command ended with exception." + " Trying external reset command."); ExternalGitCommand egit = new ExternalGitCommand(); try { egit.setType(CommandType.RESET).setDirectory(project.getRepository().getDirectory().getParentFile()) .call(); } catch (BlameException e1) { Logger.logStackTrace(log, e1); return null; } } CheckoutCommand ckoutCmd = git.checkout(); ckoutCmd.setName(scenario.getLeft().getName()); ckoutCmd.setStartPoint(scenario.getLeft()); ckoutCmd.call(); MergeCommand mergeCmd = git.merge(); mergeCmd.setCommit(false); mergeCmd.setStrategy(MergeStrategy.RECURSIVE); mergeCmd.include(scenario.getRight()); Set<String> conflictingPaths; try { // dealing with MissingObjectException MergeResult mResult = mergeCmd.call(); // dealing with Ghosts conflicts conflictingPaths = mResult.getConflicts().keySet(); } catch (NullPointerException | JGitInternalException e) { StringBuilder sb = new StringBuilder(); sb.append("[" + project.getName() + ":" + project.getUrl() + "] " + "Skipping merge scenario due to '" + e.getMessage() + "'\n"); sb.append("--> Exception: " + e.getClass()); sb.append("--> Skipped scenario:\n"); sb.append("::Base:" + scenario.getBase().getName() + "\n"); sb.append("::Left:" + scenario.getLeft().getName() + "\n"); sb.append("::Right:" + scenario.getRight().getName() + "\n"); Logger.log(log, sb.toString()); return null; } List<File> result = new ArrayList<File>(); for (String path : conflictingPaths) { result.add(new File(getProject().getRepository().getDirectory().getParent(), path)); } return result; } private ConflictBasedNetwork getConflictNetwork(MergeScenario scenario) throws IOException, GitAPIException, InterruptedException { List<File> files = getConflictingFiles(scenario); if (files == null) { return null; // dealing with ghost scenarios or fail to hard reset. } List<DeveloperNode> nodes = new ArrayList<DeveloperNode>(); List<DeveloperEdge> edges = new ArrayList<DeveloperEdge>(); for (File file : files) { List<ConflictChunk<CommandLineBlameResult>> cchunks; try { cchunks = getConflictChunks(scenario, file); } catch (BlameException e) { Logger.log(log, "[" + project.getName() + "]" + e.getMessage()); continue; } HashMap<String, List<DeveloperNode>> fNodes = null; List<DeveloperEdge> fEdges = null; /* * iterates in each chunk of the file */ for (ConflictChunk<CommandLineBlameResult> cChunk : cchunks) { fNodes = (HashMap<String, List<DeveloperNode>>) (getDeveloperNodes(scenario, cChunk)); // if program type chunk-based get developer edges that // contribute to the conflict if (type == NetworkType.CHUNK_BASED) { fEdges = getDeveloperEdges(fNodes, cChunk); // else (chunk-based full or File-based get the full // developer edges at chunk level) } else { fEdges = getFullDeveloperEdges(fNodes, cChunk); } if (fNodes != null && fEdges != null) { Iterator<List<DeveloperNode>> igroups = fNodes.values().iterator(); nodes.addAll(igroups.next()); nodes.addAll(igroups.next()); edges.addAll(fEdges); } } // case file-based, get developer nodes that contribute to some // chunk in the target file and // make the previous graph full if (type == NetworkType.FILE_BASED) { edges = getDeveloperFileEdges(nodes, file.getAbsolutePath(), edges); } } if (nodes.isEmpty() || edges.isEmpty()) { return null; } return new ConflictBasedNetwork(project, scenario, nodes, edges, type); } private List<DeveloperEdge> getDeveloperFileEdges(List<DeveloperNode> nodes, String filePath, List<DeveloperEdge> oldEdges) { // if there is only one developer, create loop if (nodes.size() == 1) { return oldEdges; } // create a conflict file graph -> Edge's weight 2 or 3 for (DeveloperNode from : nodes) { for (DeveloperNode to : nodes) { if (from.equals(to)) { continue; } DeveloperEdge newEdge; // create edge with weight 2 to developers in which contribute // in the same side if (from.getSideCommitComesFrom().equals(to.getSideCommitComesFrom())) { newEdge = new DeveloperEdge(from, to, 2, "-", filePath); } else { newEdge = new DeveloperEdge(from, to, 3, "-", filePath); } if (!oldEdges.contains(newEdge)) { oldEdges.add(newEdge); } } } return oldEdges; } private List<DeveloperEdge> getDeveloperEdges(Map<String, List<DeveloperNode>> nodes, ConflictChunk<CommandLineBlameResult> cChunk) { List<DeveloperEdge> edges = new ArrayList<DeveloperEdge>(); Iterator<List<DeveloperNode>> ilist = nodes.values().iterator(); List<DeveloperNode> groupA = ilist.next(); List<DeveloperNode> groupB = ilist.next(); // create a conflict chunk graph -> Edge's weight 1 for (DeveloperNode from : groupA) { for (DeveloperNode to : groupB) { if (from.equals(to)) { continue; } DeveloperEdge newEdge; newEdge = new DeveloperEdge(from, to, 1, cChunk.getChunkRange(), cChunk.getPath().toString()); if (!edges.contains(newEdge)) { edges.add(newEdge); } } } return edges; } private List<DeveloperEdge> getFullDeveloperEdges(Map<String, List<DeveloperNode>> nodes, ConflictChunk<CommandLineBlameResult> cChunk) { List<DeveloperEdge> edges = new ArrayList<DeveloperEdge>(); Iterator<List<DeveloperNode>> ilist = nodes.values().iterator(); List<DeveloperNode> groupA = ilist.next(); List<DeveloperNode> groupB = ilist.next(); /* * Get each Chunk */ // create a fully connected graph -> Edge's weight 0 for (DeveloperNode from : groupA) { for (DeveloperNode to : groupA) { if (from.equals(to)) { continue; } DeveloperEdge newEdge; newEdge = new DeveloperEdge(from, to, 0, cChunk.getChunkRange(), cChunk.getPath().toString()); if (!edges.contains(newEdge)) { edges.add(newEdge); } } } // create a fully connected graph -> Edge's weight 0 for (DeveloperNode from : groupB) { for (DeveloperNode to : groupB) { if (from.equals(to)) { continue; } DeveloperEdge newEdge; newEdge = new DeveloperEdge(from, to, 0, cChunk.getChunkRange(), cChunk.getPath().toString()); if (!edges.contains(newEdge)) { edges.add(newEdge); } } } // create a conflict chunk graph -> Edge's weight 1 for (DeveloperNode from : groupA) { for (DeveloperNode to : groupB) { if (from.equals(to)) { continue; } DeveloperEdge newEdge; newEdge = new DeveloperEdge(from, to, 1, cChunk.getChunkRange(), cChunk.getPath().toString()); if (!edges.contains(newEdge)) { edges.add(newEdge); } } } return edges; } /** * Returns the conflict chunks of a given file. * * @param scenario * - the scenario that as merged * @param file * - a file with conflicts */ private List<ConflictChunk<CommandLineBlameResult>> getConflictChunks(MergeScenario scenario, File file) throws BlameException { ExternalGitCommand egit = new ExternalGitCommand(); List<ConflictChunk<CommandLineBlameResult>> blames = null; blames = egit.setMergeScenario(scenario).setDirectory(file).setType(CommandType.BLAME).call(); return blames; } /** * Creates the nodes for a given file. * * @param cChunk * @return */ private Map<String, List<DeveloperNode>> getDeveloperNodes(MergeScenario scenario, ConflictChunk<CommandLineBlameResult> cChunk) { Map<String, List<DeveloperNode>> result = new HashMap<>(); // getting nodes from the upper part of the conflict CommandLineBlameResult leftResult = cChunk.getLeft().getResult(); result.put(scenario.getLeft().getName(), extractNodes(scenario.getBase(), scenario.getLeft(), leftResult, MergeCommitSide.LEFT)); // getting nodes from the bottom part of the conflict CommandLineBlameResult rightResult = cChunk.getRight().getResult(); result.put(scenario.getRight().getName(), extractNodes(scenario.getBase(), scenario.getRight(), rightResult, MergeCommitSide.RIGHT)); return result; } private List<DeveloperNode> extractNodes(RevCommit base, RevCommit side, CommandLineBlameResult aResult, MergeCommitSide mergeCommitSide) { List<DeveloperNode> result = new ArrayList<>(); for (DeveloperNode aDev : aResult.getAuthors()) { if (!getProject().getDevs().values().contains(aDev)) { // if there is no such dev in the project, then add it getProject().add(aDev); } else { // else update the reference with the project one aDev = getProject().getDevByMail(aDev.getEmail()); } if (!result.contains(aDev)) { for (int line : aResult.getLineAuthorsMap().keySet()) { String lineCommit = aResult.getLineCommitMap().get(line); if (aResult.getLineAuthorsMap().get(line).equals(aDev) && inRange(lineCommit, base, side)) { aDev.setSideCommitComesFrom(mergeCommitSide); result.add(aDev); break; } } } } return result; } /** * Determines whether a commit in in a specified range of commits. * * TODO: This should be done in some kind of GitHelper class or somewhere * else. */ private boolean inRange(String commit, RevCommit begin, RevCommit end) { try (RevWalk rw = new RevWalk(getProject().getRepository())) { rw.markStart(rw.parseCommit(end)); rw.markUninteresting(rw.parseCommit(begin)); for (RevCommit cur; (cur = rw.next()) != null;) { if (!(cur.getName().equals(begin.getName())) && cur.getName().equals(commit)) { return true; } } } catch (IOException e) { } return false; } }