| /* |
| * Copyright (C) 2018-2022, Andre Bossert <andre.bossert@siemens.com> |
| * Copyright (C) 2019, Tim Neumann <tim.neumann@advantest.com> |
| * |
| * This program and the accompanying materials are made available under the |
| * terms of the Eclipse Distribution License v. 1.0 which is available at |
| * https://www.eclipse.org/org/documents/edl-v10.php. |
| * |
| * SPDX-License-Identifier: BSD-3-Clause |
| */ |
| |
| package org.eclipse.jgit.pgm; |
| |
| import static org.eclipse.jgit.treewalk.TreeWalk.OperationType.CHECKOUT_OP; |
| |
| import java.io.BufferedReader; |
| import java.io.File; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.io.InputStreamReader; |
| import java.nio.charset.Charset; |
| import java.text.MessageFormat; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Optional; |
| import java.util.TreeMap; |
| |
| import org.eclipse.jgit.api.Git; |
| import org.eclipse.jgit.api.Status; |
| import org.eclipse.jgit.api.StatusCommand; |
| import org.eclipse.jgit.api.errors.GitAPIException; |
| import org.eclipse.jgit.diff.ContentSource; |
| import org.eclipse.jgit.dircache.DirCache; |
| import org.eclipse.jgit.dircache.DirCacheCheckout; |
| import org.eclipse.jgit.dircache.DirCacheCheckout.CheckoutMetadata; |
| import org.eclipse.jgit.dircache.DirCacheEntry; |
| import org.eclipse.jgit.dircache.DirCacheIterator; |
| import org.eclipse.jgit.errors.NoWorkTreeException; |
| import org.eclipse.jgit.errors.RevisionSyntaxException; |
| import org.eclipse.jgit.internal.diffmergetool.ExternalMergeTool; |
| import org.eclipse.jgit.internal.diffmergetool.FileElement; |
| import org.eclipse.jgit.internal.diffmergetool.MergeTools; |
| import org.eclipse.jgit.internal.diffmergetool.ToolException; |
| import org.eclipse.jgit.lib.Constants; |
| import org.eclipse.jgit.lib.CoreConfig.EolStreamType; |
| import org.eclipse.jgit.lib.IndexDiff.StageState; |
| import org.eclipse.jgit.lib.ObjectId; |
| import org.eclipse.jgit.lib.Repository; |
| import org.eclipse.jgit.lib.internal.BooleanTriState; |
| import org.eclipse.jgit.pgm.internal.CLIText; |
| import org.eclipse.jgit.revwalk.RevWalk; |
| import org.eclipse.jgit.treewalk.TreeWalk; |
| import org.eclipse.jgit.treewalk.WorkingTreeOptions; |
| import org.eclipse.jgit.treewalk.filter.PathFilterGroup; |
| import org.eclipse.jgit.util.FS.ExecutionResult; |
| import org.eclipse.jgit.util.SystemReader; |
| import org.kohsuke.args4j.Argument; |
| import org.kohsuke.args4j.Option; |
| import org.kohsuke.args4j.spi.RestOfArgumentsHandler; |
| |
| @Command(name = "mergetool", common = true, usage = "usage_MergeTool") |
| class MergeTool extends TextBuiltin { |
| private MergeTools mergeTools; |
| |
| private Optional<String> toolName = Optional.empty(); |
| |
| @Option(name = "--tool", aliases = { |
| "-t" }, metaVar = "metaVar_tool", usage = "usage_ToolForMerge") |
| void setToolName(String name) { |
| toolName = Optional.of(name); |
| } |
| |
| private BooleanTriState prompt = BooleanTriState.UNSET; |
| |
| @Option(name = "--prompt", usage = "usage_prompt") |
| void setPrompt(@SuppressWarnings("unused") boolean on) { |
| prompt = BooleanTriState.TRUE; |
| } |
| |
| @Option(name = "--no-prompt", aliases = { "-y" }, usage = "usage_noPrompt") |
| void noPrompt(@SuppressWarnings("unused") boolean on) { |
| prompt = BooleanTriState.FALSE; |
| } |
| |
| @Option(name = "--tool-help", usage = "usage_toolHelp") |
| private boolean toolHelp; |
| |
| private boolean gui = false; |
| |
| @Option(name = "--gui", aliases = { "-g" }, usage = "usage_MergeGuiTool") |
| void setGui(@SuppressWarnings("unused") boolean on) { |
| gui = true; |
| } |
| |
| @Option(name = "--no-gui", usage = "usage_noGui") |
| void noGui(@SuppressWarnings("unused") boolean on) { |
| gui = false; |
| } |
| |
| @Argument(required = false, index = 0, metaVar = "metaVar_paths") |
| @Option(name = "--", metaVar = "metaVar_paths", handler = RestOfArgumentsHandler.class) |
| protected List<String> filterPaths; |
| |
| private BufferedReader inputReader; |
| |
| @Override |
| protected void init(Repository repository, String gitDir) { |
| super.init(repository, gitDir); |
| mergeTools = new MergeTools(repository); |
| inputReader = new BufferedReader( |
| new InputStreamReader(ins, |
| SystemReader.getInstance().getDefaultCharset())); |
| } |
| |
| enum MergeResult { |
| SUCCESSFUL, FAILED, ABORTED |
| } |
| |
| @Override |
| protected void run() { |
| try { |
| if (toolHelp) { |
| showToolHelp(); |
| } else { |
| // get the changed files |
| Map<String, StageState> files = getFiles(); |
| if (files.size() > 0) { |
| merge(files); |
| } else { |
| outw.println(CLIText.get().mergeToolNoFiles); |
| } |
| } |
| outw.flush(); |
| } catch (Exception e) { |
| throw die(e.getMessage(), e); |
| } |
| } |
| |
| private void informUserNoTool(List<String> tools) { |
| try { |
| StringBuilder toolNames = new StringBuilder(); |
| for (String name : tools) { |
| toolNames.append(name + " "); //$NON-NLS-1$ |
| } |
| outw.println(MessageFormat |
| .format(CLIText.get().mergeToolPromptToolName, toolNames)); |
| outw.flush(); |
| } catch (IOException e) { |
| throw new IllegalStateException("Cannot output text", e); //$NON-NLS-1$ |
| } |
| } |
| |
| private void merge(Map<String, StageState> files) throws Exception { |
| // sort file names |
| List<String> mergedFilePaths = new ArrayList<>(files.keySet()); |
| Collections.sort(mergedFilePaths); |
| // show the files |
| StringBuilder mergedFiles = new StringBuilder(); |
| for (String mergedFilePath : mergedFilePaths) { |
| mergedFiles.append(MessageFormat.format("{0}\n", mergedFilePath)); //$NON-NLS-1$ |
| } |
| outw.println(MessageFormat.format(CLIText.get().mergeToolMerging, |
| mergedFiles)); |
| outw.flush(); |
| boolean showPrompt = mergeTools.isInteractive(); |
| if (prompt != BooleanTriState.UNSET) { |
| showPrompt = prompt == BooleanTriState.TRUE; |
| } |
| // merge the files |
| MergeResult mergeResult = MergeResult.SUCCESSFUL; |
| for (String mergedFilePath : mergedFilePaths) { |
| // if last merge failed... |
| if (mergeResult == MergeResult.FAILED) { |
| // check if user wants to continue |
| if (showPrompt && !isContinueUnresolvedPaths()) { |
| mergeResult = MergeResult.ABORTED; |
| } |
| } |
| // aborted ? |
| if (mergeResult == MergeResult.ABORTED) { |
| break; |
| } |
| // get file stage state and merge |
| StageState fileState = files.get(mergedFilePath); |
| if (fileState == StageState.BOTH_MODIFIED) { |
| mergeResult = mergeModified(mergedFilePath, showPrompt); |
| } else if ((fileState == StageState.DELETED_BY_US) |
| || (fileState == StageState.DELETED_BY_THEM)) { |
| mergeResult = mergeDeleted(mergedFilePath, |
| fileState == StageState.DELETED_BY_US); |
| } else { |
| outw.println(MessageFormat.format( |
| CLIText.get().mergeToolUnknownConflict, |
| mergedFilePath)); |
| mergeResult = MergeResult.ABORTED; |
| } |
| } |
| } |
| |
| private MergeResult mergeModified(String mergedFilePath, boolean showPrompt) |
| throws Exception { |
| outw.println(MessageFormat.format(CLIText.get().mergeToolNormalConflict, |
| mergedFilePath)); |
| outw.flush(); |
| boolean isMergeSuccessful = true; |
| ContentSource baseSource = ContentSource.create(db.newObjectReader()); |
| ContentSource localSource = ContentSource.create(db.newObjectReader()); |
| ContentSource remoteSource = ContentSource.create(db.newObjectReader()); |
| // temporary directory if mergetool.writeToTemp == true |
| File tempDir = mergeTools.createTempDirectory(); |
| // the parent directory for temp files (can be same as tempDir or just |
| // the worktree dir) |
| File tempFilesParent = tempDir != null ? tempDir : db.getWorkTree(); |
| try { |
| FileElement base = null; |
| FileElement local = null; |
| FileElement remote = null; |
| FileElement merged = new FileElement(mergedFilePath, |
| FileElement.Type.MERGED, db.getWorkTree()); |
| DirCache cache = db.readDirCache(); |
| try (RevWalk revWalk = new RevWalk(db); |
| TreeWalk treeWalk = new TreeWalk(db, |
| revWalk.getObjectReader())) { |
| treeWalk.setFilter( |
| PathFilterGroup.createFromStrings(mergedFilePath)); |
| DirCacheIterator cacheIter = new DirCacheIterator(cache); |
| treeWalk.addTree(cacheIter); |
| while (treeWalk.next()) { |
| if (treeWalk.isSubtree()) { |
| treeWalk.enterSubtree(); |
| continue; |
| } |
| final EolStreamType eolStreamType = treeWalk |
| .getEolStreamType(CHECKOUT_OP); |
| final String filterCommand = treeWalk.getFilterCommand( |
| Constants.ATTR_FILTER_TYPE_SMUDGE); |
| WorkingTreeOptions opt = db.getConfig() |
| .get(WorkingTreeOptions.KEY); |
| CheckoutMetadata checkoutMetadata = new CheckoutMetadata( |
| eolStreamType, filterCommand); |
| DirCacheEntry entry = treeWalk |
| .getTree(DirCacheIterator.class).getDirCacheEntry(); |
| if (entry == null) { |
| continue; |
| } |
| ObjectId id = entry.getObjectId(); |
| switch (entry.getStage()) { |
| case DirCacheEntry.STAGE_1: |
| base = new FileElement(mergedFilePath, |
| FileElement.Type.BASE); |
| DirCacheCheckout.getContent(db, mergedFilePath, |
| checkoutMetadata, |
| baseSource.open(mergedFilePath, id), opt, |
| new FileOutputStream( |
| base.createTempFile(tempFilesParent))); |
| break; |
| case DirCacheEntry.STAGE_2: |
| local = new FileElement(mergedFilePath, |
| FileElement.Type.LOCAL); |
| DirCacheCheckout.getContent(db, mergedFilePath, |
| checkoutMetadata, |
| localSource.open(mergedFilePath, id), opt, |
| new FileOutputStream( |
| local.createTempFile(tempFilesParent))); |
| break; |
| case DirCacheEntry.STAGE_3: |
| remote = new FileElement(mergedFilePath, |
| FileElement.Type.REMOTE); |
| DirCacheCheckout.getContent(db, mergedFilePath, |
| checkoutMetadata, |
| remoteSource.open(mergedFilePath, id), opt, |
| new FileOutputStream(remote |
| .createTempFile(tempFilesParent))); |
| break; |
| } |
| } |
| } |
| if ((local == null) || (remote == null)) { |
| throw die(MessageFormat.format(CLIText.get().mergeToolDied, |
| mergedFilePath)); |
| } |
| long modifiedBefore = merged.getFile().lastModified(); |
| try { |
| // TODO: check how to return the exit-code of the |
| // tool to jgit / java runtime ? |
| // int rc =... |
| Optional<ExecutionResult> optionalResult = mergeTools.merge( |
| local, remote, merged, base, tempDir, toolName, prompt, |
| gui, this::promptForLaunch, this::informUserNoTool); |
| if (optionalResult.isPresent()) { |
| ExecutionResult result = optionalResult.get(); |
| Charset defaultCharset = SystemReader.getInstance() |
| .getDefaultCharset(); |
| outw.println(new String(result.getStdout().toByteArray(), |
| defaultCharset)); |
| outw.flush(); |
| errw.println(new String(result.getStderr().toByteArray(), |
| defaultCharset)); |
| errw.flush(); |
| } else { |
| return MergeResult.ABORTED; |
| } |
| } catch (ToolException e) { |
| isMergeSuccessful = false; |
| outw.println(e.getResultStdout()); |
| outw.flush(); |
| errw.println(e.getMessage()); |
| errw.println(MessageFormat.format( |
| CLIText.get().mergeToolMergeFailed, mergedFilePath)); |
| errw.flush(); |
| if (e.isCommandExecutionError()) { |
| throw die(CLIText.get().mergeToolExecutionError, e); |
| } |
| } |
| // if merge was successful check file modified |
| if (isMergeSuccessful) { |
| long modifiedAfter = merged.getFile().lastModified(); |
| if (modifiedBefore == modifiedAfter) { |
| outw.println(MessageFormat.format( |
| CLIText.get().mergeToolFileUnchanged, |
| mergedFilePath)); |
| isMergeSuccessful = !showPrompt || isMergeSuccessful(); |
| } |
| } |
| // if automatically or manually successful |
| // -> add the file to the index |
| if (isMergeSuccessful) { |
| addFile(mergedFilePath); |
| } |
| } finally { |
| baseSource.close(); |
| localSource.close(); |
| remoteSource.close(); |
| } |
| return isMergeSuccessful ? MergeResult.SUCCESSFUL : MergeResult.FAILED; |
| } |
| |
| private MergeResult mergeDeleted(String mergedFilePath, boolean deletedByUs) |
| throws Exception { |
| outw.println(MessageFormat.format(CLIText.get().mergeToolFileUnchanged, |
| mergedFilePath)); |
| if (deletedByUs) { |
| outw.println(CLIText.get().mergeToolDeletedConflictByUs); |
| } else { |
| outw.println(CLIText.get().mergeToolDeletedConflictByThem); |
| } |
| int mergeDecision = getDeletedMergeDecision(); |
| if (mergeDecision == 1) { |
| // add modified file |
| addFile(mergedFilePath); |
| } else if (mergeDecision == -1) { |
| // remove deleted file |
| rmFile(mergedFilePath); |
| } else { |
| return MergeResult.ABORTED; |
| } |
| return MergeResult.SUCCESSFUL; |
| } |
| |
| private void addFile(String fileName) throws Exception { |
| try (Git git = new Git(db)) { |
| git.add().addFilepattern(fileName).call(); |
| } |
| } |
| |
| private void rmFile(String fileName) throws Exception { |
| try (Git git = new Git(db)) { |
| git.rm().addFilepattern(fileName).call(); |
| } |
| } |
| |
| private boolean hasUserAccepted(String message) throws IOException { |
| boolean yes = true; |
| outw.print(message + " "); //$NON-NLS-1$ |
| outw.flush(); |
| BufferedReader br = inputReader; |
| String line = null; |
| while ((line = br.readLine()) != null) { |
| if (line.equalsIgnoreCase("y")) { //$NON-NLS-1$ |
| yes = true; |
| break; |
| } else if (line.equalsIgnoreCase("n")) { //$NON-NLS-1$ |
| yes = false; |
| break; |
| } |
| outw.print(message); |
| outw.flush(); |
| } |
| return yes; |
| } |
| |
| private boolean isContinueUnresolvedPaths() throws IOException { |
| return hasUserAccepted(CLIText.get().mergeToolContinueUnresolvedPaths); |
| } |
| |
| private boolean isMergeSuccessful() throws IOException { |
| return hasUserAccepted(CLIText.get().mergeToolWasMergeSuccessfull); |
| } |
| |
| private boolean promptForLaunch(String toolNamePrompt) { |
| try { |
| boolean launch = true; |
| outw.print(MessageFormat.format(CLIText.get().mergeToolLaunch, |
| toolNamePrompt) + " "); //$NON-NLS-1$ |
| outw.flush(); |
| BufferedReader br = inputReader; |
| String line = null; |
| if ((line = br.readLine()) != null) { |
| if (!line.equalsIgnoreCase("y") && !line.equalsIgnoreCase("")) { //$NON-NLS-1$ //$NON-NLS-2$ |
| launch = false; |
| } |
| } |
| return launch; |
| } catch (IOException e) { |
| throw new IllegalStateException("Cannot output text", e); //$NON-NLS-1$ |
| } |
| } |
| |
| private int getDeletedMergeDecision() throws IOException { |
| int ret = 0; // abort |
| final String message = CLIText.get().mergeToolDeletedMergeDecision |
| + " "; //$NON-NLS-1$ |
| outw.print(message); |
| outw.flush(); |
| BufferedReader br = inputReader; |
| String line = null; |
| while ((line = br.readLine()) != null) { |
| if (line.equalsIgnoreCase("m")) { //$NON-NLS-1$ |
| ret = 1; // modified |
| break; |
| } else if (line.equalsIgnoreCase("d")) { //$NON-NLS-1$ |
| ret = -1; // deleted |
| break; |
| } else if (line.equalsIgnoreCase("a")) { //$NON-NLS-1$ |
| break; |
| } |
| outw.print(message); |
| outw.flush(); |
| } |
| return ret; |
| } |
| |
| private void showToolHelp() throws IOException { |
| Map<String, ExternalMergeTool> predefTools = mergeTools |
| .getPredefinedTools(true); |
| StringBuilder availableToolNames = new StringBuilder(); |
| StringBuilder notAvailableToolNames = new StringBuilder(); |
| for (String name : predefTools.keySet()) { |
| if (predefTools.get(name).isAvailable()) { |
| availableToolNames.append(MessageFormat.format("\t\t{0}\n", name)); //$NON-NLS-1$ |
| } else { |
| notAvailableToolNames.append(MessageFormat.format("\t\t{0}\n", name)); //$NON-NLS-1$ |
| } |
| } |
| StringBuilder userToolNames = new StringBuilder(); |
| Map<String, ExternalMergeTool> userTools = mergeTools |
| .getUserDefinedTools(); |
| for (String name : userTools.keySet()) { |
| userToolNames.append(MessageFormat.format("\t\t{0}.cmd {1}\n", //$NON-NLS-1$ |
| name, userTools.get(name).getCommand())); |
| } |
| outw.println(MessageFormat.format( |
| CLIText.get().mergeToolHelpSetToFollowing, availableToolNames, |
| userToolNames, notAvailableToolNames)); |
| } |
| |
| private Map<String, StageState> getFiles() throws RevisionSyntaxException, |
| NoWorkTreeException, GitAPIException { |
| Map<String, StageState> files = new TreeMap<>(); |
| try (Git git = new Git(db)) { |
| StatusCommand statusCommand = git.status(); |
| if (filterPaths != null && filterPaths.size() > 0) { |
| for (String path : filterPaths) { |
| statusCommand.addPath(path); |
| } |
| } |
| Status status = statusCommand.call(); |
| files = status.getConflictingStageState(); |
| } |
| return files; |
| } |
| |
| } |