| /******************************************************************************* |
| * Copyright (C) 2010, 2021 Mathias Kinzler <mathias.kinzler@sap.com> and others. |
| * |
| * All rights reserved. This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License 2.0 |
| * which accompanies this distribution, and is available at |
| * https://www.eclipse.org/legal/epl-2.0/ |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| * |
| * Contributors: |
| * Andre Bossert <andre.bossert@siemens.com> - external merge and diff tools |
| *******************************************************************************/ |
| package org.eclipse.egit.ui.internal.merge; |
| |
| import java.io.IOException; |
| import java.lang.reflect.InvocationTargetException; |
| import java.net.URI; |
| import java.net.URISyntaxException; |
| import java.nio.charset.Charset; |
| import java.nio.file.Files; |
| import java.text.MessageFormat; |
| import java.time.Instant; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.IdentityHashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Objects; |
| |
| import org.eclipse.compare.CompareConfiguration; |
| import org.eclipse.compare.CompareEditorInput; |
| import org.eclipse.compare.ITypedElement; |
| import org.eclipse.compare.contentmergeviewer.ContentMergeViewer; |
| import org.eclipse.compare.rangedifferencer.RangeDifference; |
| import org.eclipse.compare.structuremergeviewer.DiffContainer; |
| import org.eclipse.compare.structuremergeviewer.DiffNode; |
| import org.eclipse.compare.structuremergeviewer.Differencer; |
| import org.eclipse.compare.structuremergeviewer.ICompareInput; |
| import org.eclipse.compare.structuremergeviewer.IDiffContainer; |
| import org.eclipse.core.resources.IFile; |
| import org.eclipse.core.resources.IResource; |
| import org.eclipse.core.runtime.CoreException; |
| import org.eclipse.core.runtime.IPath; |
| import org.eclipse.core.runtime.IProgressMonitor; |
| import org.eclipse.core.runtime.Path; |
| import org.eclipse.egit.core.RepositoryUtil; |
| import org.eclipse.egit.core.info.GitInfo; |
| import org.eclipse.egit.core.internal.CompareCoreUtils; |
| import org.eclipse.egit.core.internal.CoreText; |
| import org.eclipse.egit.core.internal.efs.EgitFileSystem; |
| import org.eclipse.egit.core.internal.storage.GitFileRevision; |
| import org.eclipse.egit.core.internal.util.ResourceUtil; |
| import org.eclipse.egit.core.util.RevCommitUtils; |
| import org.eclipse.egit.ui.Activator; |
| import org.eclipse.egit.ui.internal.CompareUtils; |
| import org.eclipse.egit.ui.internal.UIText; |
| import org.eclipse.egit.ui.internal.revision.EditableRevision; |
| import org.eclipse.egit.ui.internal.revision.FileRevisionTypedElement; |
| import org.eclipse.egit.ui.internal.revision.GitCompareFileRevisionEditorInput.EmptyTypedElement; |
| import org.eclipse.egit.ui.internal.revision.LocationEditableRevision; |
| import org.eclipse.egit.ui.internal.revision.ResourceEditableRevision; |
| import org.eclipse.egit.ui.internal.synchronize.compare.LocalNonWorkspaceTypedElement; |
| import org.eclipse.jface.action.ToolBarManager; |
| import org.eclipse.jface.operation.IRunnableContext; |
| import org.eclipse.jface.viewers.Viewer; |
| import org.eclipse.jgit.api.MergeCommand.ConflictStyle; |
| import org.eclipse.jgit.attributes.Attribute; |
| import org.eclipse.jgit.attributes.Attributes; |
| import org.eclipse.jgit.dircache.DirCache; |
| import org.eclipse.jgit.dircache.DirCacheEditor; |
| import org.eclipse.jgit.dircache.DirCacheEditor.PathEdit; |
| import org.eclipse.jgit.dircache.DirCacheEntry; |
| import org.eclipse.jgit.dircache.DirCacheIterator; |
| import org.eclipse.jgit.lib.AnyObjectId; |
| import org.eclipse.jgit.lib.ConfigConstants; |
| import org.eclipse.jgit.lib.Constants; |
| import org.eclipse.jgit.lib.ObjectId; |
| import org.eclipse.jgit.lib.Repository; |
| import org.eclipse.jgit.revwalk.RevCommit; |
| import org.eclipse.jgit.revwalk.RevWalk; |
| import org.eclipse.jgit.revwalk.filter.RevFilter; |
| import org.eclipse.jgit.treewalk.AbstractTreeIterator; |
| import org.eclipse.jgit.treewalk.FileTreeIterator; |
| import org.eclipse.jgit.treewalk.TreeWalk; |
| import org.eclipse.jgit.treewalk.filter.PathFilterGroup; |
| import org.eclipse.osgi.util.NLS; |
| import org.eclipse.team.core.history.IFileRevision; |
| import org.eclipse.team.internal.ui.synchronize.LocalResourceTypedElement; |
| import org.eclipse.ui.PlatformUI; |
| |
| /** |
| * A Git-specific {@link CompareEditorInput} for merging conflicting files. |
| */ |
| @SuppressWarnings("restriction") |
| public class GitMergeEditorInput extends AbstractGitCompareEditorInput { |
| |
| private static final String LABELPATTERN = "{0} - {1}"; //$NON-NLS-1$ |
| |
| private final MergeInputMode mode; |
| |
| private final boolean useWorkspace; |
| |
| private final boolean useOurs; |
| |
| private CompareEditorInputViewerAction toggleCurrentChanges; |
| |
| // This must be an identity map. If the built tree is post-processed and its |
| // structure is changed, hash codes of nodes may change! |
| private Map<DiffNode, String> customLabels = new IdentityHashMap<>(); |
| |
| /** |
| * Creates a new {@link GitMergeEditorInput}. |
| * |
| * @param mode |
| * defining what to use as input for the logical left side |
| * @param locations |
| * as selected by the user |
| */ |
| public GitMergeEditorInput(MergeInputMode mode, IPath... locations) { |
| super(null, locations); |
| this.useWorkspace = !MergeInputMode.STAGE_2.equals(mode); |
| this.useOurs = MergeInputMode.MERGED_OURS.equals(mode); |
| this.mode = mode; |
| CompareConfiguration config = getCompareConfiguration(); |
| config.setLeftEditable(true); |
| } |
| |
| @Override |
| public int hashCode() { |
| return 31 * super.hashCode() + Objects.hash(mode); |
| } |
| |
| @Override |
| public boolean equals(Object obj) { |
| if (!super.equals(obj)) { |
| return false; |
| } |
| GitMergeEditorInput other = (GitMergeEditorInput) obj; |
| return mode == other.mode; |
| } |
| |
| @Override |
| protected void initActions(ToolBarManager manager, Viewer newViewer, |
| ICompareInput input) { |
| super.initActions(manager, newViewer, input); |
| setToggleCurrentChangesAction(manager, newViewer, input); |
| } |
| |
| private void setToggleCurrentChangesAction(ToolBarManager manager, |
| Viewer newViewer, ICompareInput input) { |
| boolean isApplicable = newViewer instanceof ContentMergeViewer |
| && input instanceof MergeDiffNode |
| && input.getAncestor() != null; |
| setAction(manager, newViewer, isApplicable, |
| ToggleCurrentChangesAction.COMMAND_ID, |
| create -> { |
| if (toggleCurrentChanges == null && create) { |
| toggleCurrentChanges = new ToggleCurrentChangesAction( |
| UIText.GitMergeEditorInput_ToggleCurrentChangesLabel, |
| this); |
| toggleCurrentChanges |
| .setId(ToggleCurrentChangesAction.COMMAND_ID); |
| } |
| return toggleCurrentChanges; |
| }); |
| } |
| |
| @Override |
| protected void disposeActions() { |
| if (toggleCurrentChanges != null) { |
| toggleCurrentChanges.dispose(); |
| toggleCurrentChanges = null; |
| } |
| super.disposeActions(); |
| } |
| |
| /** |
| * @param monitor |
| * @return return the diff container |
| * @throws InterruptedException |
| * @throws InvocationTargetException |
| */ |
| public IDiffContainer getDiffContainer(IProgressMonitor monitor) |
| throws InvocationTargetException, InterruptedException { |
| return (IDiffContainer) prepareInput(monitor); |
| } |
| |
| @Override |
| protected GitInfo getGitInfo(IPath path) { |
| return new GitInfo() { |
| |
| @Override |
| public Repository getRepository() { |
| return GitMergeEditorInput.this.getRepository(); |
| } |
| |
| @Override |
| public String getGitPath() { |
| return path.toString(); |
| } |
| |
| @Override |
| public Source getSource() { |
| return Source.WORKING_TREE; |
| } |
| |
| @Override |
| public AnyObjectId getCommitId() { |
| return null; |
| } |
| }; |
| } |
| |
| @Override |
| protected DiffContainer buildInput(IProgressMonitor monitor) |
| throws InvocationTargetException, InterruptedException { |
| RevWalk rw = null; |
| try { |
| Repository repo = getRepository(); |
| |
| rw = new RevWalk(repo); |
| |
| // get the "right" side |
| final RevCommit rightCommit; |
| try { |
| rightCommit = RevCommitUtils.getTheirs(repo, rw); |
| } catch (IOException e) { |
| throw new InvocationTargetException(e); |
| } |
| |
| // we need the HEAD, also to determine the common |
| // ancestor |
| final RevCommit headCommit; |
| try { |
| ObjectId head = repo.resolve(Constants.HEAD); |
| if (head == null) |
| throw new IOException(NLS.bind( |
| CoreText.ValidationUtils_CanNotResolveRefMessage, |
| Constants.HEAD)); |
| headCommit = rw.parseCommit(head); |
| } catch (IOException e) { |
| throw new InvocationTargetException(e); |
| } |
| |
| final String fullBranch; |
| try { |
| fullBranch = repo.getFullBranch(); |
| } catch (IOException e) { |
| throw new InvocationTargetException(e); |
| } |
| |
| CompareConfiguration config = getCompareConfiguration(); |
| // try to obtain the common ancestor |
| RevCommit ancestorCommit = null; |
| boolean unknownAncestor = false; |
| switch (repo.getRepositoryState()) { |
| case CHERRY_PICKING: |
| case REBASING_INTERACTIVE: |
| case REBASING_MERGE: |
| if (rightCommit.getParentCount() == 1) { |
| try { |
| ancestorCommit = rw |
| .parseCommit(rightCommit.getParent(0)); |
| } catch (IOException e) { |
| unknownAncestor = true; |
| } |
| } else { |
| // Cherry-pick of a merge commit -- git doesn't record the |
| // mainline index anywhere, so we don't know which parent |
| // was taken. |
| unknownAncestor = true; |
| } |
| if (!MergeInputMode.WORKTREE.equals(mode)) { |
| // Do not suppress any changes on the left if the input is |
| // the possibly pre-merged working tree version. Conflict |
| // markers exist only on the left; they would not be shown |
| // as differences, and are then too easy to miss. |
| config.setChangeIgnored( |
| config.isMirrored() ? RangeDifference.RIGHT |
| : RangeDifference.LEFT, |
| true); |
| config.setChangeIgnored(RangeDifference.ANCESTOR, true); |
| } |
| break; |
| default: |
| List<RevCommit> startPoints = new ArrayList<>(); |
| rw.setRevFilter(RevFilter.MERGE_BASE); |
| startPoints.add(rightCommit); |
| startPoints.add(headCommit); |
| try { |
| rw.markStart(startPoints); |
| ancestorCommit = rw.next(); |
| } catch (Exception e) { |
| // Ignore; ancestor remains null |
| } |
| break; |
| } |
| |
| if (monitor.isCanceled()) { |
| throw new InterruptedException(); |
| } |
| // set the labels |
| config.setRightLabel(NLS.bind(LABELPATTERN, rightCommit |
| .getShortMessage(), CompareUtils.truncatedRevision(rightCommit.name()))); |
| |
| if (!useWorkspace) { |
| config.setLeftLabel(NLS.bind(LABELPATTERN, headCommit |
| .getShortMessage(), CompareUtils.truncatedRevision(headCommit.name()))); |
| } else if (useOurs) { |
| config.setLeftLabel( |
| UIText.GitMergeEditorInput_WorkspaceOursHeader); |
| } else { |
| config.setLeftLabel(UIText.GitMergeEditorInput_WorkspaceHeader); |
| } |
| if (ancestorCommit != null) { |
| config.setAncestorLabel(NLS.bind(LABELPATTERN, ancestorCommit |
| .getShortMessage(), CompareUtils.truncatedRevision(ancestorCommit.name()))); |
| } else if (unknownAncestor) { |
| config.setAncestorLabel(NLS.bind( |
| UIText.GitMergeEditorInput_AncestorUnknownHeader, |
| CompareUtils.truncatedRevision(rightCommit.name()))); |
| } |
| // set title and icon |
| setTitle(NLS.bind(UIText.GitMergeEditorInput_MergeEditorTitle, |
| new Object[] { |
| RepositoryUtil.INSTANCE.getRepositoryName(repo), |
| rightCommit.getShortMessage(), fullBranch })); |
| |
| // build the nodes |
| try { |
| return buildDiffContainer(repo, headCommit, ancestorCommit, rw, |
| monitor); |
| } catch (IOException e) { |
| throw new InvocationTargetException(e); |
| } |
| } finally { |
| if (rw != null) { |
| rw.dispose(); |
| } |
| } |
| } |
| |
| @Override |
| protected void inputBuilt(DiffContainer root) { |
| super.inputBuilt(root); |
| customLabels.forEach((node, name) -> setLeftLabel(node, name, false)); |
| customLabels.clear(); |
| } |
| |
| private DiffContainer buildDiffContainer(Repository repository, |
| RevCommit headCommit, RevCommit ancestorCommit, RevWalk rw, |
| IProgressMonitor monitor) |
| throws IOException, InterruptedException { |
| |
| monitor.setTaskName(UIText.GitMergeEditorInput_CalculatingDiffTaskName); |
| DiffContainer result = new DiffNode(Differencer.CONFLICTING); |
| |
| ConflictStyle style = null; |
| try (TreeWalk tw = new TreeWalk(repository)) { |
| int dirCacheIndex = tw.addTree(new DirCacheIterator(repository |
| .readDirCache())); |
| FileTreeIterator fIter = new FileTreeIterator(repository); |
| int fileTreeIndex = tw.addTree(fIter); |
| fIter.setDirCacheIterator(tw, dirCacheIndex); |
| int repositoryTreeIndex = tw.addTree(rw.parseTree(repository |
| .resolve(Constants.HEAD))); |
| |
| // filter by selected resources |
| Collection<String> filterPaths = getFilterPaths(); |
| if (!filterPaths.isEmpty()) { |
| if (filterPaths.size() > 1) { |
| tw.setFilter( |
| PathFilterGroup.createFromStrings(filterPaths)); |
| } else { |
| String path = filterPaths.iterator().next(); |
| if (!path.isEmpty()) { |
| tw.setFilter(PathFilterGroup.createFromStrings(path)); |
| } |
| } |
| } |
| tw.setRecursive(true); |
| |
| while (tw.next()) { |
| if (monitor.isCanceled()) |
| throw new InterruptedException(); |
| String gitPath = tw.getPathString(); |
| monitor.setTaskName(gitPath); |
| |
| FileTreeIterator fit = tw.getTree(fileTreeIndex, |
| FileTreeIterator.class); |
| if (fit == null) |
| continue; |
| |
| DirCacheIterator dit = tw.getTree(dirCacheIndex, |
| DirCacheIterator.class); |
| |
| final DirCacheEntry dirCacheEntry = dit == null ? null : dit |
| .getDirCacheEntry(); |
| |
| boolean conflicting = dirCacheEntry != null |
| && dirCacheEntry.getStage() > 0; |
| |
| AbstractTreeIterator rt = tw.getTree(repositoryTreeIndex, |
| AbstractTreeIterator.class); |
| |
| // compare local file against HEAD to see if it was modified |
| boolean modified = rt != null |
| && !fit.getEntryObjectId() |
| .equals(rt.getEntryObjectId()); |
| |
| // if this is neither conflicting nor changed, we skip it |
| if (!conflicting && !modified) |
| continue; |
| |
| ITypedElement right; |
| String encoding = null; |
| if (conflicting) { |
| GitFileRevision revision = GitFileRevision.inIndex( |
| repository, gitPath, DirCacheEntry.STAGE_3); |
| encoding = CompareCoreUtils.getResourceEncoding(repository, |
| gitPath); |
| right = new FileRevisionTypedElement(revision, encoding); |
| } else { |
| right = CompareUtils.getFileRevisionTypedElement(gitPath, |
| headCommit, repository); |
| } |
| // can this really happen? |
| if (right instanceof EmptyTypedElement) { |
| continue; |
| } |
| ITypedElement left; |
| IFileRevision rev; |
| // if the file is not conflicting (as it was auto-merged) |
| // we will show the auto-merged (local) version |
| |
| Path repositoryPath = new Path(repository.getWorkTree() |
| .getAbsolutePath()); |
| IPath location = repositoryPath.append(gitPath); |
| assert location != null; |
| IFile file = ResourceUtil.getFileForLocation(location, false); |
| boolean useWorkingTree = !conflicting || useWorkspace; |
| boolean useCustomLabel = false; |
| if (!useWorkingTree && conflicting && dirCacheEntry != null) { |
| // Normal conflict stages have a zero timestamp. If it's not |
| // zero, we marked it below when the content was saved to |
| // the working tree file in an earlier merge editor. |
| useWorkingTree = !Instant.EPOCH |
| .equals(dirCacheEntry.getLastModifiedInstant()); |
| useCustomLabel = useWorkingTree; |
| } |
| boolean isSymLink = Files |
| .isSymbolicLink(location.toFile().toPath()); |
| if (useWorkingTree) { |
| boolean useOursFilter = conflicting && useOurs; |
| if (isSymLink && useOursFilter) { |
| useOursFilter = false; |
| useCustomLabel = true; |
| } |
| int conflictMarkerSize = 7; // Git default |
| if (useOursFilter) { |
| Attributes attributes = tw.getAttributes(); |
| useOursFilter = attributes.canBeContentMerged(); |
| if (useOursFilter) { |
| Attribute markerSize = attributes |
| .get("conflict-marker-size"); //$NON-NLS-1$ |
| if (markerSize != null && Attribute.State.CUSTOM |
| .equals(markerSize.getState())) { |
| try { |
| conflictMarkerSize = Integer |
| .parseUnsignedInt( |
| markerSize.getValue()); |
| } catch (NumberFormatException e) { |
| // Ignore |
| } |
| } |
| } |
| } |
| LocalResourceTypedElement item; |
| if (useOursFilter) { |
| if (style == null) { |
| style = repository.getConfig().getEnum( |
| ConfigConstants.CONFIG_MERGE_SECTION, null, |
| ConfigConstants.CONFIG_KEY_CONFLICTSTYLE, |
| ConflictStyle.MERGE); |
| } |
| boolean useDiff3Style = ConflictStyle.DIFF3 |
| .equals(style); |
| String filter = (useDiff3Style ? 'O' : 'o') |
| + Integer.toString(conflictMarkerSize); |
| URI uri = EgitFileSystem.createURI(repository, gitPath, |
| "WORKTREE:" + filter); //$NON-NLS-1$ |
| Charset rscEncoding = null; |
| if (file != null) { |
| if (encoding == null) { |
| encoding = CompareCoreUtils |
| .getResourceEncoding(file); |
| } |
| try { |
| rscEncoding = Charset.forName(encoding); |
| } catch (IllegalArgumentException e) { |
| // Ignore here; use default. |
| } |
| } |
| item = createWithHiddenResource(uri, repository, |
| gitPath, tw.getNameString(), file, rscEncoding); |
| if (file != null) { |
| item.setSharedDocumentListener( |
| new LocalResourceSaver(item) { |
| |
| @Override |
| protected void save() |
| throws CoreException { |
| super.save(); |
| file.refreshLocal( |
| IResource.DEPTH_ZERO, null); |
| } |
| }); |
| } else { |
| item.setSharedDocumentListener( |
| new LocalResourceSaver(item)); |
| } |
| } else { |
| if (!isSymLink && file != null) { |
| item = new LocalResourceTypedElement(file); |
| } else { |
| item = new LocalNonWorkspaceTypedElement(repository, |
| location); |
| if (isSymLink) { |
| item.addContentChangeListener(source -> { |
| try { |
| item.commit(null); |
| } catch (CoreException e) { |
| Activator.handleStatus(e.getStatus(), |
| true); |
| } |
| }); |
| } |
| } |
| item.setSharedDocumentListener( |
| new LocalResourceSaver(item)); |
| } |
| left = item; |
| } else { |
| // Stage 2 from index with backing IResource |
| rev = GitFileRevision.inIndex(repository, gitPath, |
| DirCacheEntry.STAGE_2); |
| IRunnableContext runnableContext = getContainer(); |
| if (runnableContext == null) { |
| runnableContext = PlatformUI.getWorkbench() |
| .getProgressService(); |
| assert runnableContext != null; |
| } |
| if (isSymLink) { |
| left = new LocationEditableRevision(rev, location, |
| runnableContext); |
| } else { |
| IFile rsc = file != null ? file |
| : createHiddenResource( |
| location.toFile().toURI(), |
| tw.getNameString(), null); |
| assert rsc != null; |
| left = new ResourceEditableRevision(rev, rsc, |
| runnableContext); |
| } |
| // 'left' saves to the working tree. Update the index entry |
| // with the current time. Normal conflict stages have a |
| // timestamp of zero, so this is a non-invasive fully |
| // compatible way to mark this conflict stage so that the |
| // next time we do take the file contents. |
| ((EditableRevision) left).addContentChangeListener( |
| source -> updateIndexTimestamp(repository, |
| gitPath)); |
| // make sure we don't need a round trip later |
| try { |
| ((EditableRevision) left).cacheContents(monitor); |
| } catch (CoreException e) { |
| throw new IOException(e.getMessage(), e); |
| } |
| } |
| |
| int kind = Differencer.NO_CHANGE; |
| if (conflicting) { |
| kind = Differencer.CONFLICTING + Differencer.CHANGE; |
| } else if (modified) { |
| kind = Differencer.LEFT + Differencer.ADDITION; |
| } |
| IDiffContainer fileParent = getFileParent(result, |
| repositoryPath, file, location); |
| |
| ITypedElement ancestor = null; |
| if (ancestorCommit != null) { |
| ancestor = CompareUtils.getFileRevisionTypedElement(gitPath, |
| ancestorCommit, repository); |
| // we get an ugly black icon if we have an EmptyTypedElement |
| // instead of null |
| if (ancestor instanceof EmptyTypedElement) { |
| ancestor = null; |
| } |
| } else if (conflicting) { |
| GitFileRevision revision = GitFileRevision.inIndex( |
| repository, gitPath, DirCacheEntry.STAGE_1); |
| if (encoding == null) { |
| encoding = CompareCoreUtils |
| .getResourceEncoding(repository, gitPath); |
| } |
| ancestor = new FileRevisionTypedElement(revision, encoding); |
| } |
| // create the node as child |
| DiffNode node = new MergeDiffNode(fileParent, kind, ancestor, |
| left, right); |
| if (useCustomLabel) { |
| customLabels.put(node, tw.getNameString()); |
| } else if (left instanceof EditableRevision) { |
| String name = tw.getNameString(); |
| ((EditableRevision) left).addContentChangeListener( |
| source -> setLeftLabel(node, name, true)); |
| } |
| } |
| return result; |
| } catch (URISyntaxException e) { |
| throw new IOException(e.getMessage(), e); |
| } |
| } |
| |
| private void updateIndexTimestamp(Repository repository, String gitPath) { |
| DirCache cache = null; |
| try { |
| cache = repository.lockDirCache(); |
| DirCacheEditor editor = cache.editor(); |
| editor.add(new PathEdit(gitPath) { |
| |
| private boolean done; |
| |
| @Override |
| public void apply(DirCacheEntry ent) { |
| if (!done && ent.getStage() > 0) { |
| ent.setLastModified(Instant.now()); |
| done = true; |
| } |
| } |
| }); |
| editor.commit(); |
| } catch (IOException e) { |
| Activator.logError(MessageFormat.format( |
| UIText.GitMergeEditorInput_ErrorUpdatingIndex, gitPath), e); |
| } finally { |
| if (cache != null) { |
| cache.unlock(); |
| } |
| } |
| } |
| |
| private void setLeftLabel(DiffNode node, String name, boolean fireChange) { |
| GitCompareLabelProvider labels = new GitCompareLabelProvider() { |
| |
| @Override |
| public String getLeftLabel(Object input) { |
| return MessageFormat.format( |
| UIText.GitCompareFileRevisionEditorInput_LocalLabel, |
| name); |
| } |
| |
| @Override |
| public String getRightLabel(Object input) { |
| return null; // Use default |
| } |
| |
| @Override |
| public String getAncestorLabel(Object input) { |
| return null; // Use default |
| } |
| }; |
| getCompareConfiguration().setLabelProvider(node, labels); |
| if (fireChange) { |
| labels.fireNodeLabelChanged(node); |
| } |
| } |
| } |