/******************************************************************************* * Copyright (c) 2015, 2016 Obeo. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Laurent Goubet <laurent.goubet@obeo.fr> - initial API and implementation * Axel Richard <axel.richard@obeo.fr> - Update #registerHandledFiles() *******************************************************************************/ package org.eclipse.emf.compare.egit.internal.merge; //CHECKSTYLE:OFF import java.io.IOException; import java.io.InputStream; import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.Set; import org.apache.log4j.Logger; import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.mapping.RemoteResourceMappingContext; import org.eclipse.core.resources.mapping.ResourceMapping; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.core.runtime.OperationCanceledException; import org.eclipse.core.runtime.SubMonitor; import org.eclipse.core.runtime.jobs.ISchedulingRule; import org.eclipse.core.runtime.jobs.Job; import org.eclipse.egit.core.Activator; import org.eclipse.egit.core.internal.job.RuleUtil; import org.eclipse.egit.core.project.RepositoryMapping; import org.eclipse.emf.compare.egit.internal.storage.AbstractGitResourceVariant; import org.eclipse.emf.compare.egit.internal.storage.TreeParserResourceVariant; import org.eclipse.emf.compare.egit.internal.wrapper.JGitProgressMonitorWrapper; import org.eclipse.jgit.diff.RawText; import org.eclipse.jgit.dircache.DirCacheBuildIterator; import org.eclipse.jgit.dircache.DirCacheBuilder; import org.eclipse.jgit.dircache.DirCacheEntry; import org.eclipse.jgit.dircache.DirCacheIterator; import org.eclipse.jgit.errors.CorruptObjectException; import org.eclipse.jgit.errors.IncorrectObjectTypeException; import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.FileMode; import org.eclipse.jgit.lib.ProgressMonitor; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.merge.MergeResult; import org.eclipse.jgit.merge.RecursiveMerger; import org.eclipse.jgit.treewalk.CanonicalTreeParser; import org.eclipse.jgit.treewalk.FileTreeIterator; import org.eclipse.jgit.treewalk.TreeWalk; import org.eclipse.jgit.treewalk.WorkingTreeIterator; import org.eclipse.jgit.treewalk.filter.PathFilterGroup; import org.eclipse.osgi.util.NLS; import org.eclipse.team.core.TeamException; import org.eclipse.team.core.diff.IDiff; import org.eclipse.team.core.mapping.IMergeContext; import org.eclipse.team.core.mapping.IResourceMappingMerger; import org.eclipse.team.core.mapping.ISynchronizationScopeManager; import org.eclipse.team.core.subscribers.Subscriber; import org.eclipse.team.core.subscribers.SubscriberMergeContext; import org.eclipse.team.core.subscribers.SubscriberResourceMappingContext; import org.eclipse.team.core.subscribers.SubscriberScopeManager; import org.eclipse.team.core.variants.IResourceVariant; /** * This extends the recursive merger in order to take into account specific mergers provided by the Team * {@link org.eclipse.core.resources.mapping.ModelProvider model providers}. * <p> * The Recursive Merger handles files one-by-one, calling file-specific merge drivers for each. On the * opposite, this strategy can handle bigger sets of files at once, delegating the merge to the files' model. * As such, file-specific merge drivers may not be called from this strategy if that file is part of a larger * model. * </p> * <p> * Any file that is <b>not</b> part of a model, which model cannot be determined, or which model does not * specify a custom merger, will be handled as it would by the RecursiveMerger. * </p> * * @author <a href="mailto:laurent.goubet@obeo.fr">Laurent Goubet</a> * @author <a href="mailto:laurent.delaigue@obeo.fr">Laurent Delaigue</a> */ @SuppressWarnings("restriction") public class RecursiveModelMerger extends RecursiveMerger { private static final Logger LOGGER = Logger.getLogger(RecursiveModelMerger.class); /** * This will be populated during the course of the RecursiveMappingMergers' executions. These files have * been cleanly merged and we should thus make sure the DirCacheBuilder takes their latest working * directory version before committing. */ private final Set<String> makeInSync = new LinkedHashSet<>(); /** * keeps track of the files we've already merged. Since we iterate one file at a time but may merge * multiple files at once when they are part of the same model, this will help us avoid merging the same * file or model twice. */ private final Set<String> handledPaths = new HashSet<>(); /** * Default recursive model merger. * * @param db * @param inCore */ public RecursiveModelMerger(Repository db, boolean inCore) { super(db, inCore); } @Override protected boolean mergeTreeWalk(TreeWalk treeWalk, boolean ignoreConflicts) throws IOException { if (LOGGER.isInfoEnabled()) { LOGGER.info("STARTING Recursive model merge."); //$NON-NLS-1$ } final TreeWalkResourceVariantTreeProvider variantTreeProvider = new TreeWalkResourceVariantTreeProvider( getRepository(), treeWalk, T_BASE, T_OURS, T_THEIRS); final GitResourceVariantTreeSubscriber subscriber = new GitResourceVariantTreeSubscriber( variantTreeProvider); final RemoteResourceMappingContext remoteMappingContext = new SubscriberResourceMappingContext( subscriber, true); try { refreshRoots(subscriber.roots()); } catch (CoreException e) { // We cannot be sure that Team and/or the merger implementations // will properly handle unrefreshed files. Fall back to merging // without workspace awareness. Activator.logError(MergeText.RecursiveModelMerger_RefreshError, e); return super.mergeTreeWalk(treeWalk, ignoreConflicts); } monitor.beginTask(MergeText.RecursiveModelMerger_BuildLogicalModels, ProgressMonitor.UNKNOWN); // Eager lookup for the logical models to avoid issues in case we // iterate over a file that does not exist locally before the rest of // its logical model. final LogicalModels logicalModels = new LogicalModels(); logicalModels.build(variantTreeProvider.getKnownResources(), remoteMappingContext); monitor.endTask(); if (monitor.isCancelled()) { throw new OperationCanceledException(); } // We are done with the setup. We can now iterate over the tree walk and // either delegate to the logical model's merger if any or fall back to // standard git merging. Basically, any file that is not a part of a // logical model that defines its own specific merger will be handled as // it would by the RecursiveMerger. while (treeWalk.next()) { final int modeBase = treeWalk.getRawMode(T_BASE); final int modeOurs = treeWalk.getRawMode(T_OURS); final int modeTheirs = treeWalk.getRawMode(T_THEIRS); if (modeBase == 0 && modeOurs == 0 && modeTheirs == 0) { // untracked continue; } final String path = treeWalk.getPathString(); if (handledPaths.contains(path)) { // This one has been handled as a result of a previous model // merge. Simply make sure we use its latest content if it is // not in conflict. if (treeWalk.isSubtree() && enterSubtree) { treeWalk.enterSubtree(); } if (!unmergedPaths.contains(path)) { registerMergedPath(path); if (LOGGER.isDebugEnabled()) { LOGGER.debug("Ignoring previously handled file: " + path); //$NON-NLS-1$ } } continue; } final int nonZeroMode = modeBase != 0 ? modeBase : modeOurs != 0 ? modeOurs : modeTheirs; final IResource resource = variantTreeProvider.getResourceHandleForLocation(getRepository(), path, FileMode.fromBits(nonZeroMode) == FileMode.TREE); Set<IResource> logicalModel = logicalModels.getModel(resource); IResourceMappingMerger modelMerger = null; if (logicalModel != null) { try { // We need to refresh because new resources may have been added refreshRoots(subscriber.roots()); modelMerger = getResourceMappingMerger(logicalModel); } catch (CoreException e) { Activator.logError(MergeText.RecursiveModelMerger_AdaptError, e); // ignore this model and fall back to default if (!fallBackToDefaultMerge(treeWalk, ignoreConflicts)) { cleanUp(); if (LOGGER.isInfoEnabled()) { LOGGER.info( "FAILED - Recursive model merge, could not find appropriate merger and default merge failed."); //$NON-NLS-1$ } return false; } } } if (modelMerger != null) { enterSubtree = true; boolean success = new ModelMerge(this, subscriber, remoteMappingContext, path, logicalModel, modelMerger).run(new JGitProgressMonitorWrapper(monitor)); if (!success) { if (LOGGER.isInfoEnabled()) { LOGGER.info("FAILED - Recursive model merge."); //$NON-NLS-1$ } return false; } else if (!unmergedPaths.contains(path)) { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Merged model file: " + path); //$NON-NLS-1$ } registerMergedPath(path); } if (treeWalk.isSubtree()) { enterSubtree = true; } } else if (!fallBackToDefaultMerge(treeWalk, ignoreConflicts)) { cleanUp(); if (LOGGER.isInfoEnabled()) { LOGGER.info("FAILED - Recursive model merge, default merge failed."); //$NON-NLS-1$ } return false; } if (treeWalk.isSubtree() && enterSubtree) { treeWalk.enterSubtree(); } } if (!makeInSync.isEmpty()) { indexModelMergedFiles(); } if (LOGGER.isInfoEnabled()) { LOGGER.info("SUCCESS - Recursive model merge."); //$NON-NLS-1$ } return true; } protected IResourceMappingMerger getResourceMappingMerger(Set<IResource> logicalModel) throws CoreException { return LogicalModels.findAdapter(logicalModel, IResourceMappingMerger.class); } private boolean fallBackToDefaultMerge(TreeWalk treeWalk, boolean ignoreConflicts) throws MissingObjectException, IncorrectObjectTypeException, CorruptObjectException, IOException { boolean hasWorkingTreeIterator = tw.getTreeCount() > T_FILE; return processEntry(treeWalk.getTree(T_BASE, CanonicalTreeParser.class), treeWalk.getTree(T_OURS, CanonicalTreeParser.class), treeWalk.getTree(T_THEIRS, CanonicalTreeParser.class), treeWalk.getTree(T_INDEX, DirCacheBuildIterator.class), hasWorkingTreeIterator ? treeWalk.getTree(T_FILE, WorkingTreeIterator.class) : null, ignoreConflicts); } /** * Add files modified by model mergers to the index. * * @throws CorruptObjectException * @throws MissingObjectException * @throws IncorrectObjectTypeException * @throws IOException */ private void indexModelMergedFiles() throws CorruptObjectException, MissingObjectException, IncorrectObjectTypeException, IOException { try (TreeWalk syncingTreeWalk = new TreeWalk(getRepository())) { syncingTreeWalk.addTree(new DirCacheIterator(dircache)); syncingTreeWalk.addTree(new FileTreeIterator(getRepository())); syncingTreeWalk.setRecursive(true); syncingTreeWalk.setFilter(PathFilterGroup.createFromStrings(makeInSync)); String lastAdded = null; while (syncingTreeWalk.next()) { String path = syncingTreeWalk.getPathString(); if (path.equals(lastAdded)) { continue; } WorkingTreeIterator workingTree = syncingTreeWalk.getTree(1, WorkingTreeIterator.class); DirCacheIterator dirCache = syncingTreeWalk.getTree(0, DirCacheIterator.class); if (dirCache == null && workingTree != null && workingTree.isEntryIgnored()) { // nothing to do on this one } else if (workingTree != null) { if (dirCache == null || dirCache.getDirCacheEntry() == null || !dirCache.getDirCacheEntry().isAssumeValid()) { final DirCacheEntry dce = new DirCacheEntry(path); final FileMode mode = workingTree.getIndexFileMode(dirCache); dce.setFileMode(mode); if (FileMode.GITLINK != mode) { dce.setLength(workingTree.getEntryLength()); dce.setLastModified(workingTree.getEntryLastModified()); try (InputStream is = workingTree.openEntryStream()) { dce.setObjectId(getObjectInserter().insert(Constants.OBJ_BLOB, workingTree.getEntryContentLength(), is)); } } else { dce.setObjectId(workingTree.getEntryObjectId()); } builder.add(dce); lastAdded = path; } else { builder.add(dirCache.getDirCacheEntry()); } } else if (dirCache != null && FileMode.GITLINK == dirCache.getEntryFileMode()) { builder.add(dirCache.getDirCacheEntry()); } } } } private static String getRepoRelativePath(IResource file) { final RepositoryMapping mapping = RepositoryMapping.getMapping(file); if (mapping != null) { return mapping.getRepoRelativePath(file); } return null; } /** * On many aspects, team relies on the refreshed state of the workspace files, notably to determine if a * file is in sync or not. Since we could have been called for a rebase, rebase that checked out a new * commit without refreshing the workspace afterwards, team could see "in-sync" files even though they no * longer exist in the workspace. This should be called before any merging takes place to make sure all * files concerned by this merge operation are refreshed beforehand. * * @param resources * The set of resource roots to refresh. * @throws CoreException * Thrown whenever we fail at refreshing one of the resources or its children. */ private void refreshRoots(IResource[] resources) throws CoreException { for (IResource root : resources) { if (root.isAccessible()) { root.refreshLocal(IResource.DEPTH_INFINITE, new NullProgressMonitor()); } } } private void markConflict(String filePath, DirCacheBuilder cacheBuilder, TreeParserResourceVariant baseVariant, TreeParserResourceVariant ourVariant, TreeParserResourceVariant theirVariant) { add(filePath, cacheBuilder, baseVariant, DirCacheEntry.STAGE_1); add(filePath, cacheBuilder, ourVariant, DirCacheEntry.STAGE_2); add(filePath, cacheBuilder, theirVariant, DirCacheEntry.STAGE_3); } private void add(String path, DirCacheBuilder cacheBuilder, TreeParserResourceVariant variant, int stage) { if (variant != null && !FileMode.TREE.equals(variant.getRawMode())) { DirCacheEntry e = new DirCacheEntry(path, stage); e.setFileMode(FileMode.fromBits(variant.getRawMode())); e.setObjectId(variant.getObjectId()); e.setLastModified(0); e.setLength(0); cacheBuilder.add(e); } } private static class ModelMerge { private final RecursiveModelMerger merger; private final GitResourceVariantTreeSubscriber subscriber; private final RemoteResourceMappingContext remoteMappingContext; private final String path; private final Set<IResource> logicalModel; private final IResourceMappingMerger modelMerger; public ModelMerge(RecursiveModelMerger merger, GitResourceVariantTreeSubscriber subscriber, RemoteResourceMappingContext remoteMappingContext, String path, Set<IResource> logicalModel, IResourceMappingMerger modelMerger) { this.merger = merger; this.subscriber = subscriber; this.remoteMappingContext = remoteMappingContext; this.path = path; this.logicalModel = logicalModel; this.modelMerger = modelMerger; } private boolean run(IProgressMonitor monitor) throws CorruptObjectException, IOException { SubMonitor progress = SubMonitor.convert(monitor, 1); try { final IMergeContext mergeContext = prepareMergeContext(); final IStatus status = modelMerger.merge(mergeContext, progress.newChild(1)); registerHandledFiles(mergeContext, status); } catch (CoreException e) { Activator.logError(e.getMessage(), e); merger.cleanUp(); return false; } catch (OperationCanceledException e) { final String message = NLS.bind(MergeText.RecursiveModelMerger_ScopeInitializationInterrupted, path); Activator.logError(message, e); merger.cleanUp(); return false; } return true; } private void registerHandledFiles(final IMergeContext mergeContext, final IStatus status) throws TeamException, CoreException { for (IResource handledFile : logicalModel) { String filePath = getRepoRelativePath(handledFile); if (filePath == null) { IResourceVariant sourceVariant = subscriber.getSourceTree() .getResourceVariant(handledFile); IResourceVariant remoteVariant = subscriber.getRemoteTree() .getResourceVariant(handledFile); IResourceVariant baseVariant = subscriber.getBaseTree().getResourceVariant(handledFile); if (sourceVariant instanceof AbstractGitResourceVariant) { // In the case of a conflict, the file may not exist // So don't check for file existence filePath = ((AbstractGitResourceVariant)sourceVariant).getPath(); } if (filePath == null && remoteVariant instanceof AbstractGitResourceVariant) { // In the case of a conflict, the file may not exist // So don't check for file existence filePath = ((AbstractGitResourceVariant)remoteVariant).getPath(); } if (filePath == null && baseVariant instanceof AbstractGitResourceVariant) { // In the case of a conflict, the file may not exist // So don't check for file existence filePath = ((AbstractGitResourceVariant)baseVariant).getPath(); } } if (LOGGER.isDebugEnabled()) { LOGGER.debug("Registering handled file " + filePath); //$NON-NLS-1$ } if (filePath != null) { merger.modifiedFiles.add(filePath); merger.handledPaths.add(filePath); } else if (LOGGER.isDebugEnabled()) { LOGGER.debug("Impossible to compute a repo-relative filePath for file " //$NON-NLS-1$ + handledFile); } // The merge failed. If some parts of the model were // auto-mergeable, the model merger told us so through // GitMergeContext#markAsMerged() (stored within #makeInSync). // All other components of the logical model should be marked as // conflicts. if (mergeContext.getDiffTree().getDiff(handledFile) == null) { // If no diff, the model merger does... nothing // Make sure this file will be added to the index. merger.registerMergedPath(filePath); if (LOGGER.isDebugEnabled()) { LOGGER.debug("Merged non-modified file: " + filePath); //$NON-NLS-1$ } } else if (filePath != null && status.getSeverity() != IStatus.OK && !merger.makeInSync.contains(filePath)) { merger.unmergedPaths.add(filePath); merger.mergeResults.put(filePath, new MergeResult<>(Collections.<RawText> emptyList())); final TreeParserResourceVariant baseVariant = (TreeParserResourceVariant)subscriber .getBaseTree().getResourceVariant(handledFile); final TreeParserResourceVariant ourVariant = (TreeParserResourceVariant)subscriber .getSourceTree().getResourceVariant(handledFile); final TreeParserResourceVariant theirVariant = (TreeParserResourceVariant)subscriber .getRemoteTree().getResourceVariant(handledFile); merger.markConflict(filePath, merger.builder, baseVariant, ourVariant, theirVariant); if (LOGGER.isDebugEnabled()) { LOGGER.debug("Marking conflict on " + filePath); //$NON-NLS-1$ } } } } /** * Create and initialize the merge context for the given model. * * @return An initialized merge context for the given model. * @throws CoreException * Thrown if we cannot initialize the scope for this merge context. * @throws OperationCanceledException * Thrown if the user cancelled the initialization. */ private IMergeContext prepareMergeContext() throws CoreException, OperationCanceledException { final Set<ResourceMapping> allMappings = LogicalModels.getResourceMappings(logicalModel, remoteMappingContext); final ResourceMapping[] mappings = allMappings.toArray(new ResourceMapping[allMappings.size()]); final ISynchronizationScopeManager manager = new SubscriberScopeManager(subscriber.getName(), mappings, subscriber, remoteMappingContext, true) { @Override public ISchedulingRule getSchedulingRule() { return RuleUtil.getRule(merger.getRepository()); } }; manager.initialize(new NullProgressMonitor()); final IMergeContext context = new GitMergeContext(merger, subscriber, manager); // Wait for the asynchronous scope expanding to end (started from // the initialization of our merge context) waitForScope(context); return context; } private void waitForScope(IMergeContext context) { // The UILockListener might prevent us from properly joining. boolean joined = false; while (!joined) { try { Job.getJobManager().join(context, new NullProgressMonitor()); joined = true; } catch (InterruptedException e) { // Some other UI threads were trying to run. Let the // syncExecs do their jobs and re-try to join on ours. } } } } private static class GitMergeContext extends SubscriberMergeContext { private final RecursiveModelMerger merger; /** * Create and initialize a merge context for the given subscriber. * * @param merger * the merger * @param subscriber * the subscriber. * @param scopeManager * the scope manager. */ public GitMergeContext(RecursiveModelMerger merger, Subscriber subscriber, ISynchronizationScopeManager scopeManager) { super(subscriber, scopeManager); this.merger = merger; initialize(); } public void markAsMerged(IDiff node, boolean inSyncHint, IProgressMonitor monitor) throws CoreException { final IResource resource = getDiffTree().getResource(node); merger.addSyncPath(resource); } public void reject(IDiff diff, IProgressMonitor monitor) throws CoreException { // Empty implementation } @Override protected void makeInSync(IDiff diff, IProgressMonitor monitor) throws CoreException { final IResource resource = getDiffTree().getResource(diff); merger.addSyncPath(resource); } } private void addSyncPath(IResource resource) { String repoRelativePath = getRepoRelativePath(resource); registerMergedPath(repoRelativePath); } private boolean registerMergedPath(String path) { if (path != null) { return makeInSync.add(path); } return false; } } // CHECKSTYLE:ON