/* * Copyright 2000-2009 JetBrains s.r.o. * * 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 org.community.intellij.plugins.communitycase.merge; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.project.Project; import com.intellij.openapi.vcs.FilePath; import com.intellij.openapi.vcs.VcsException; import com.intellij.openapi.vcs.merge.MergeData; import com.intellij.openapi.vcs.merge.MergeProvider2; import com.intellij.openapi.vcs.merge.MergeSession; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.util.ui.ColumnInfo; import com.intellij.vcsUtil.VcsRunnable; import com.intellij.vcsUtil.VcsUtil; import org.community.intellij.plugins.communitycase.FileRevision; import org.community.intellij.plugins.communitycase.Util; import org.community.intellij.plugins.communitycase.commands.Command; import org.community.intellij.plugins.communitycase.commands.FileUtils; import org.community.intellij.plugins.communitycase.commands.SimpleHandler; import org.community.intellij.plugins.communitycase.commands.StringScanner; import org.community.intellij.plugins.communitycase.history.HistoryUtils; import org.community.intellij.plugins.communitycase.i18n.Bundle; import org.jetbrains.annotations.NotNull; import java.io.IOException; import java.util.HashMap; import java.util.List; import java.util.Map; /** * Merge-changes provider for Git, used by IDEA internal 3-way merge tool */ public class MergeProvider implements MergeProvider2 { /** * the logger */ private static final Logger log = Logger.getInstance("#"+MergeProvider.class.getName()); /** * The project instance */ private final Project myProject; /** * If true the merge provider has a reverse meaning */ private final boolean myReverse; /** * The revision that designates common parent for the files during the merge */ private static final int ORIGINAL_REVISION_NUM = 1; /** * The revision that designates the file on the local branch */ private static final int YOURS_REVISION_NUM = 2; /** * The revision that designates the remote file being merged */ private static final int THEIRS_REVISION_NUM = 3; /** * A merge provider * * @param project a project for the provider */ public MergeProvider(Project project) { this(project, false); } /** * A merge provider * * @param project a project for the provider * @param reverse if true, yours and theirs take a reverse meaning */ public MergeProvider(Project project, boolean reverse) { myProject = project; myReverse = reverse; } /** * {@inheritDoc} */ @NotNull public MergeData loadRevisions(final VirtualFile file) throws VcsException { final MergeData mergeData = new MergeData(); if (file == null) return mergeData; final VirtualFile root = Util.getRoot(file); final FilePath path = VcsUtil.getFilePath(file.getPath()); VcsRunnable runnable = new VcsRunnable() { @SuppressWarnings({"ConstantConditions"}) public void run() throws VcsException { FileRevision original = new FileRevision(myProject, path,HistoryUtils.createUnvalidatedRevisionNumber(":" + ORIGINAL_REVISION_NUM) ); FileRevision current = new FileRevision(myProject, path,HistoryUtils.createUnvalidatedRevisionNumber(":" + yoursRevision()) ); FileRevision last = new FileRevision(myProject, path,HistoryUtils.createUnvalidatedRevisionNumber(":" + theirsRevision()) ); try { try { mergeData.ORIGINAL = original.getContent(); } catch (Exception ex) { /// unable to load original revision, use the current instead /// This could happen in case if rebasing. mergeData.ORIGINAL = file.contentsToByteArray(); } mergeData.CURRENT = current.getContent(); mergeData.LAST = last.getContent(); mergeData.LAST_REVISION_NUMBER=HistoryUtils.createUnvalidatedRevisionNumber(myReverse?"HEAD":"MERGE_HEAD"); } catch (IOException e) { throw new IllegalStateException("Failed to load file content", e); } } }; VcsUtil.runVcsProcessWithProgress(runnable, Bundle.message("merge.load.files"), false, myProject); return mergeData; } /** * @return number for "yours" revision (taking {@code revsere} flag in account) */ private int yoursRevision() { return myReverse ? THEIRS_REVISION_NUM : YOURS_REVISION_NUM; } /** * @return number for "theirs" revision (taking {@code revsere} flag in account) */ private int theirsRevision() { return myReverse ? YOURS_REVISION_NUM : THEIRS_REVISION_NUM; } /** * {@inheritDoc} */ public void conflictResolvedForFile(VirtualFile file) { if (file == null) return; try { FileUtils.addFiles(myProject, Util.getRoot(file), file); } catch (VcsException e) { log.error("Confirming conflict resolution failed", e); } } /** * {@inheritDoc} */ public boolean isBinary(VirtualFile file) { return file.getFileType().isBinary(); } @NotNull public MergeSession createMergeSession(List<VirtualFile> files) { return new MyMergeSession(files); } /** * The conflict descriptor */ private static class Conflict { /** * the file in the conflict */ VirtualFile myFile; /** * the root for the file */ VirtualFile myRoot; /** * the status of theirs revision */ Status myStatusTheirs; /** * the status */ Status myStatusYours; /** * @return true if the merge operation can be applied */ boolean isMergeable() { return myStatusTheirs == Conflict.Status.MODIFIED && myStatusYours == Conflict.Status.MODIFIED; } /** * The conflict status */ enum Status { /** * the file was modified on the branch */ MODIFIED, /** * the file was deleted on the branch */ DELETED, } } /** * The merge session, it queries conflict information . */ private class MyMergeSession implements MergeSession { /** * the map with conflicts */ Map<VirtualFile, Conflict> myConflicts = new HashMap<VirtualFile, Conflict>(); /** * A constructor from list of the files * * @param filesToMerge the files to process using merge dialog. */ MyMergeSession(List<VirtualFile> filesToMerge) { // get conflict type by the file try { for (Map.Entry<VirtualFile, List<VirtualFile>> e : Util.sortFilesByRoot(filesToMerge).entrySet()) { Map<String, Conflict> cs = new HashMap<String, Conflict>(); VirtualFile root = e.getKey(); List<VirtualFile> files = e.getValue(); SimpleHandler h = new SimpleHandler(myProject, root, Command.LS_FILES); h.setRemote(true); h.setStdoutSuppressed(true); h.addParameters("--exclude-standard", "--unmerged", "-t", "-z"); h.endOptions(); String output = h.run(); StringScanner s = new StringScanner(output); while (s.hasMoreData()) { if (!"M".equals(s.spaceToken())) { s.boundedToken('\u0000'); continue; } s.spaceToken(); // permissions s.spaceToken(); // commit hash int source = Integer.parseInt(s.tabToken()); String file = s.boundedToken('\u0000'); Conflict c = cs.get(file); if (c == null) { c = new Conflict(); c.myRoot = root; cs.put(file, c); } if (source == theirsRevision()) { c.myStatusTheirs = Conflict.Status.MODIFIED; } else if (source == yoursRevision()) { c.myStatusYours = Conflict.Status.MODIFIED; } else if (source != ORIGINAL_REVISION_NUM) { throw new IllegalStateException("Unknown revision " + source + " for the file: " + file); } } for (VirtualFile f : files) { String path = Util.relativePath(root, f); Conflict c = cs.get(path); assert c != null : "The conflict not found for the file: " + f.getPath() + "(" + path + ")"; c.myFile = f; if (c.myStatusTheirs == null) { c.myStatusTheirs = Conflict.Status.DELETED; } if (c.myStatusYours == null) { c.myStatusYours = Conflict.Status.DELETED; } myConflicts.put(f, c); } } } catch (VcsException ex) { throw new IllegalStateException("The git operation should not fail in this context", ex); } } /** * {@inheritDoc} */ public ColumnInfo[] getMergeInfoColumns() { return new ColumnInfo[]{new StatusColumn(false), new StatusColumn(true)}; } /** * {@inheritDoc} */ public boolean canMerge(VirtualFile file) { Conflict c = myConflicts.get(file); return c != null && c.isMergeable(); } /** * {@inheritDoc} */ public void conflictResolvedForFile(VirtualFile file, Resolution resolution) { Conflict c = myConflicts.get(file); assert c != null : "Conflict was not loaded for the file: " + file.getPath(); try { if (c.isMergeable()) { FileUtils.addFiles(myProject, c.myRoot, file); } else { Conflict.Status status; switch (resolution) { case AcceptedTheirs: status = c.myStatusTheirs; break; case AcceptedYours: status = c.myStatusYours; break; case Merged: default: throw new IllegalArgumentException("Unsupported resolution for unmergable files(" + file.getPath() + "): " + resolution); } switch (status) { case MODIFIED: FileUtils.addFiles(myProject, c.myRoot, file); break; case DELETED: FileUtils.deleteFiles(myProject, c.myRoot, file); break; default: throw new IllegalArgumentException("Unsupported status(" + file.getPath() + "): " + status); } } } catch (VcsException e) { log.error("Unexpected exception during the git operation (" + file.getPath() + ")", e); } } /** * The status column, the column shows either "yours" or "theirs" status */ class StatusColumn extends ColumnInfo<VirtualFile, String> { /** * if false, "yours" status is displayed, otherwise "theirs" */ private final boolean myIsTheirs; /** * The constructor * * @param isTheirs if true columns represents status in 'theirs' revision, if false in 'ours' */ public StatusColumn(boolean isTheirs) { super(isTheirs ? Bundle.message("merge.tool.column.theirs.status") : Bundle.message("merge.tool.column.yours.status")); myIsTheirs = isTheirs; } /** * {@inheritDoc} */ public String valueOf(VirtualFile file) { Conflict c = myConflicts.get(file); assert c != null : "No conflict for the file " + file; Conflict.Status s = myIsTheirs ? c.myStatusTheirs : c.myStatusYours; switch (s) { case MODIFIED: return Bundle.message("merge.tool.column.status.modified"); case DELETED: return Bundle.message("merge.tool.column.status.deleted"); default: throw new IllegalStateException("Unknown status " + s + " for file " + file.getPath()); } } } } }