| /******************************************************************************* |
| * Copyright (c) 2010, 2013 IBM Corporation 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: |
| * IBM Corporation - initial API and implementation |
| * Dariusz Luksza <dariusz@luksza.org> |
| * François Rey - gracefully ignore linked resources |
| * Laurent Goubet <laurent.goubet@obeo.fr> - 403363 |
| *******************************************************************************/ |
| package org.eclipse.egit.core.synchronize; |
| |
| import static org.eclipse.jgit.lib.Repository.stripWorkDir; |
| |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.HashMap; |
| import java.util.Map; |
| |
| import org.eclipse.core.resources.IContainer; |
| import org.eclipse.core.resources.IFile; |
| import org.eclipse.core.resources.IProject; |
| 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.Activator; |
| import org.eclipse.egit.core.internal.CoreText; |
| import org.eclipse.egit.core.internal.storage.WorkspaceFileRevision; |
| import org.eclipse.egit.core.project.RepositoryMapping; |
| import org.eclipse.egit.core.synchronize.dto.GitSynchronizeData; |
| import org.eclipse.egit.core.synchronize.dto.GitSynchronizeDataSet; |
| import org.eclipse.jgit.lib.PersonIdent; |
| import org.eclipse.jgit.lib.Repository; |
| import org.eclipse.jgit.treewalk.TreeWalk; |
| import org.eclipse.osgi.util.NLS; |
| import org.eclipse.team.core.TeamException; |
| import org.eclipse.team.core.diff.IDiff; |
| import org.eclipse.team.core.diff.ITwoWayDiff; |
| import org.eclipse.team.core.diff.provider.ThreeWayDiff; |
| import org.eclipse.team.core.history.IFileRevision; |
| import org.eclipse.team.core.mapping.provider.ResourceDiff; |
| import org.eclipse.team.core.synchronize.SyncInfo; |
| import org.eclipse.team.core.variants.IResourceVariant; |
| import org.eclipse.team.core.variants.IResourceVariantComparator; |
| import org.eclipse.team.core.variants.IResourceVariantTree; |
| import org.eclipse.team.core.variants.ResourceVariantTreeSubscriber; |
| import org.eclipse.team.internal.core.mapping.ResourceVariantFileRevision; |
| import org.eclipse.team.internal.core.mapping.SyncInfoToDiffConverter; |
| |
| /** |
| * |
| */ |
| @SuppressWarnings("restriction") |
| public class GitResourceVariantTreeSubscriber extends |
| ResourceVariantTreeSubscriber { |
| |
| /** A resource variant tree of the source branch. */ |
| private GitSourceResourceVariantTree sourceTree; |
| |
| /** |
| * A resource variant tree of the remote branch(es). |
| */ |
| private GitRemoteResourceVariantTree remoteTree; |
| |
| /** |
| * A resource variant tree against HEAD. |
| */ |
| private GitBaseResourceVariantTree baseTree; |
| |
| private GitSynchronizeDataSet gsds; |
| |
| private IResource[] roots; |
| |
| private GitSyncCache cache; |
| |
| private GitSyncInfoToDiffConverter syncInfoConverter = new GitSyncInfoToDiffConverter(); |
| |
| /** |
| * @param data |
| */ |
| public GitResourceVariantTreeSubscriber(GitSynchronizeDataSet data) { |
| this.gsds = data; |
| } |
| |
| /** |
| * Initialize git subscriber. This method will pre-fetch data from git |
| * repository. This approach will reduce number of {@link TreeWalk}'s |
| * created during synchronization |
| * |
| * @param monitor |
| */ |
| public void init(IProgressMonitor monitor) { |
| monitor.beginTask( |
| CoreText.GitResourceVariantTreeSubscriber_fetchTaskName, |
| gsds.size()); |
| try { |
| cache = GitSyncCache.getAllData(gsds, monitor); |
| } finally { |
| monitor.done(); |
| } |
| } |
| |
| @Override |
| public boolean isSupervised(IResource res) throws TeamException { |
| return gsds.contains(res.getProject()) && gsds.shouldBeIncluded(res); |
| } |
| |
| /** |
| * Returns all members of the given resource as recorded by git. Resources |
| * ignored by git via .gitignore will not be returned, even if they exist in |
| * the workspace. |
| * |
| * @param res |
| * the resource to get the members of |
| * @return the resources, which may or may not exist in the workspace |
| */ |
| @Override |
| public IResource[] members(IResource res) throws TeamException { |
| if (res.getType() == IResource.FILE || !gsds.shouldBeIncluded(res)) { |
| return new IResource[0]; |
| } |
| GitSynchronizeData gsd = gsds.getData(res.getProject()); |
| if (gsd == null) { |
| return new IResource[0]; |
| } |
| Repository repo = gsd.getRepository(); |
| if (repo == null) { |
| return new IResource[0]; |
| } |
| GitSyncObjectCache repoCache = cache.get(repo); |
| |
| Collection<IResource> allMembers = new ArrayList<>(); |
| Map<String, IResource> existingMembers = new HashMap<>(); |
| |
| String path = stripWorkDir(repo.getWorkTree(), res.getLocation().toFile()); |
| GitSyncObjectCache cachedMembers = repoCache.get(path); |
| // A normal synchronizer would just return the union of existing |
| // resources and non-existing ones that exist only in git. For git, |
| // however, we want to ignore .gitignored resources completely, and |
| // include untracked files only if the preference to do so is set |
| // (in which case the cache will contain them already). So we add |
| // only the existing ones that are also recorded in the git 3-way |
| // cache, plus those recorded only in git, plus the git recorded |
| // one if it's a file vs.folder conflict. |
| try { |
| IContainer container = (IContainer) res; |
| // Existing resources |
| if (container.exists()) { |
| for (IResource member : container.members()) { |
| existingMembers.put(member.getName(), member); |
| } |
| } |
| |
| // Now add the ones from git |
| if (cachedMembers != null) { |
| Collection<GitSyncObjectCache> members = cachedMembers |
| .members(); |
| if (members != null) { |
| for (GitSyncObjectCache gitMember : members) { |
| String name = gitMember.getName(); |
| IResource existing = existingMembers.get(name); |
| if (existing != null) { |
| allMembers.add(existing); |
| } |
| if (existing == null || (existing |
| .getType() != IResource.FILE) != gitMember |
| .getDiffEntry().isTree()) { |
| // Non-existing, or file vs. folder |
| IPath localPath = new Path(name); |
| if (gitMember.getDiffEntry().isTree()) { |
| allMembers.add(container.getFolder(localPath)); |
| } else { |
| allMembers.add(container.getFile(localPath)); |
| } |
| } |
| } |
| } |
| } |
| } catch (CoreException e) { |
| throw TeamException.asTeamException(e); |
| } |
| |
| return allMembers.toArray(new IResource[0]); |
| } |
| |
| @Override |
| public void refresh(IResource[] resources, int depth, |
| IProgressMonitor monitor) throws TeamException { |
| for (IResource resource : resources) { |
| // check to see if there is a full refresh |
| if (resource.getType() == IResource.ROOT) { |
| // refresh entire cache |
| cache = GitSyncCache.getAllData(gsds, monitor); |
| super.refresh(resources, depth, monitor); |
| return; |
| } |
| } |
| |
| // not refreshing the workspace, locate and collect target resources |
| Map<GitSynchronizeData, Collection<String>> updateRequests = new HashMap<>(); |
| for (IResource resource : resources) { |
| IProject project = resource.getProject(); |
| GitSynchronizeData data = gsds.getData(project.getName()); |
| if (data != null) { |
| RepositoryMapping mapping = RepositoryMapping |
| .getMapping(project); |
| // mapping may be null if the project has been closed |
| if (mapping != null) { |
| Collection<String> paths = updateRequests.get(data); |
| if (paths == null) { |
| paths = new ArrayList<>(); |
| updateRequests.put(data, paths); |
| } |
| |
| String path = mapping.getRepoRelativePath(resource); |
| // null path may be returned, check for this |
| if (path == null) |
| // unknown, force a refresh of the whole repository |
| path = ""; //$NON-NLS-1$ |
| paths.add(path); |
| } |
| } |
| } |
| |
| // scan only the repositories that were affected |
| if (!updateRequests.isEmpty()) { |
| // refresh cache |
| GitSyncCache.mergeAllDataIntoCache(updateRequests, monitor, cache); |
| } |
| |
| super.refresh(resources, depth, monitor); |
| } |
| |
| @Override |
| public IResource[] roots() { |
| if (roots == null) |
| roots = gsds.getAllProjects(); |
| if (roots == null) |
| return new IResource[0]; |
| IResource[] result = new IResource[roots.length]; |
| System.arraycopy(roots, 0, result, 0, roots.length); |
| return result; |
| } |
| |
| /** |
| * @param data |
| */ |
| public void reset(GitSynchronizeDataSet data) { |
| gsds = data; |
| |
| roots = null; |
| sourceTree = null; |
| baseTree = null; |
| remoteTree = null; |
| } |
| |
| /** |
| * Disposes nested resources |
| */ |
| public void dispose() { |
| if (sourceTree != null) |
| sourceTree.dispose(); |
| if (baseTree != null) |
| baseTree.dispose(); |
| if (remoteTree != null) |
| remoteTree.dispose(); |
| gsds.dispose(); |
| } |
| |
| /** |
| * Provide the synchronize data set. |
| * |
| * @return The {@link GitSynchronizeDataSet} used by this subscriber. |
| */ |
| protected GitSynchronizeDataSet getDataSet() { |
| return gsds; |
| } |
| |
| /** |
| * Provide the synchronization cache. |
| * |
| * @return The {@link GitSyncCache} used by this subscriber. |
| */ |
| protected GitSyncCache getCache() { |
| return cache; |
| } |
| |
| @Override |
| public IDiff getDiff(IResource resource) throws CoreException { |
| final GitSynchronizeData syncData = gsds.getData(resource.getProject()); |
| if (syncData == null || syncData.shouldIncludeLocal()) |
| return super.getDiff(resource); |
| |
| SyncInfo info = getSyncInfo(resource); |
| if (info == null || info.getKind() == SyncInfo.IN_SYNC) |
| return null; |
| return syncInfoConverter.getDeltaFor(info); |
| } |
| |
| /** |
| * The default implementation of SyncInfoToDiffConverter uses inaccurate |
| * information with regards to some of EGit features. |
| * <p> |
| * SyncInfoToDiffConverter#asFileRevision(IResourceVariant) is called when a |
| * user double-clicks a revision from the synchronize view (among others). |
| * However, the default implementation returns an IFileRevision with no |
| * comment, author or timestamp information (this can be observed by |
| * commenting this implementation out and launching |
| * HistoryTest#queryHistoryThroughTeam()). |
| * </p> |
| * <p> |
| * SyncInfoToDiffConverter#getDeltaFor(SyncInfo) had been originally thought |
| * by Team to be used for synchronizations that considered local changes. |
| * This is not always the case with EGit. For example, a user might try and |
| * compare two refs together from the Git repository explorer (right click > |
| * synchronize with each other). In such a case, the local files must not be |
| * taken into account (i.e. we must respect the value of our |
| * GitSynchronizeData#shouldIncludeLocal(). Most of the private methods here |
| * were copy/pasted from the super implementation. |
| * </p> |
| */ |
| private class GitSyncInfoToDiffConverter extends SyncInfoToDiffConverter { |
| @Override |
| public IDiff getDeltaFor(SyncInfo info) { |
| if (info.getComparator().isThreeWay()) { |
| ITwoWayDiff local = getLocalDelta(info); |
| ITwoWayDiff remote = getRemoteDelta(info); |
| return new ThreeWayDiff(local, remote); |
| } else { |
| if (info.getKind() != SyncInfo.IN_SYNC) { |
| IResourceVariant remote = info.getRemote(); |
| IResource local = info.getLocal(); |
| |
| int kind; |
| if (remote == null) |
| kind = IDiff.REMOVE; |
| else if (!local.exists()) |
| kind = IDiff.ADD; |
| else |
| kind = IDiff.CHANGE; |
| |
| if (local.getType() == IResource.FILE) { |
| IFileRevision after = asFileState(remote); |
| IFileRevision before = getLocalFileRevision((IFile) local); |
| return new ResourceDiff(info.getLocal(), kind, 0, |
| before, after); |
| } |
| // For folders, we don't need file states |
| return new ResourceDiff(info.getLocal(), kind); |
| } |
| return null; |
| } |
| } |
| |
| private ITwoWayDiff getLocalDelta(SyncInfo info) { |
| int direction = SyncInfo.getDirection(info.getKind()); |
| if (direction == SyncInfo.OUTGOING |
| || direction == SyncInfo.CONFLICTING) { |
| IResourceVariant ancestor = info.getBase(); |
| IResource local = info.getLocal(); |
| |
| int kind; |
| if (ancestor == null) |
| kind = IDiff.ADD; |
| else if (!local.exists()) |
| kind = IDiff.REMOVE; |
| else |
| kind = IDiff.CHANGE; |
| |
| if (local.getType() == IResource.FILE) { |
| IFileRevision before = asFileState(ancestor); |
| IFileRevision after = getLocalFileRevision((IFile) local); |
| return new ResourceDiff(info.getLocal(), kind, 0, before, |
| after); |
| } |
| // For folders, we don't need file states |
| return new ResourceDiff(info.getLocal(), kind); |
| |
| } |
| return null; |
| } |
| |
| /** |
| * Depending on the Synchronize data, this will return either the local |
| * file or the "source" revision. |
| * |
| * @param local |
| * The local file. |
| * @return The file revision that should be considered for the local |
| * (left) side a delta |
| */ |
| protected IFileRevision getLocalFileRevision(IFile local) { |
| final GitSynchronizeData data = gsds.getData(local.getProject()); |
| if (data.shouldIncludeLocal()) |
| return new WorkspaceFileRevision(local); |
| |
| try { |
| return asFileState(getSourceTree().getResourceVariant(local)); |
| } catch (TeamException e) { |
| String error = NLS |
| .bind(CoreText.GitResourceVariantTreeSubscriber_CouldNotFindSourceVariant, |
| local.getName()); |
| Activator.logError(error, e); |
| // fall back to the working tree version |
| return new WorkspaceFileRevision(local); |
| } |
| } |
| |
| /* |
| * copy-pasted from the private implementation in |
| * SyncInfoToDiffConverter |
| */ |
| private ITwoWayDiff getRemoteDelta(SyncInfo info) { |
| int direction = SyncInfo.getDirection(info.getKind()); |
| if (direction == SyncInfo.INCOMING |
| || direction == SyncInfo.CONFLICTING) { |
| IResourceVariant ancestor = info.getBase(); |
| IResourceVariant remote = info.getRemote(); |
| |
| int kind; |
| if (ancestor == null) |
| kind = IDiff.ADD; |
| else if (remote == null) |
| kind = IDiff.REMOVE; |
| else |
| kind = IDiff.CHANGE; |
| |
| // For folders, we don't need file states |
| if (info.getLocal().getType() == IResource.FILE) { |
| IFileRevision before = asFileState(ancestor); |
| IFileRevision after = asFileState(remote); |
| return new ResourceDiff(info.getLocal(), kind, 0, before, |
| after); |
| } |
| |
| return new ResourceDiff(info.getLocal(), kind); |
| } |
| return null; |
| } |
| |
| /* |
| * copy-pasted from the private implementation in |
| * SyncInfoToDiffConverter |
| */ |
| private IFileRevision asFileState(final IResourceVariant variant) { |
| if (variant == null) |
| return null; |
| return asFileRevision(variant); |
| } |
| |
| @Override |
| protected ResourceVariantFileRevision asFileRevision( |
| IResourceVariant variant) { |
| return new GitResourceVariantFileRevision(variant); |
| } |
| } |
| |
| /** |
| * The default implementation of ResourceVariantFileRevision has no author, |
| * comment, timestamp... or any information that could be provided by the |
| * Git resource variant. This implementation uses the variant's information. |
| */ |
| private static class GitResourceVariantFileRevision extends |
| ResourceVariantFileRevision { |
| private final IResourceVariant variant; |
| |
| public GitResourceVariantFileRevision(IResourceVariant variant) { |
| super(variant); |
| this.variant = variant; |
| } |
| |
| @Override |
| public String getContentIdentifier() { |
| // Use the same ID as would CommitFileRevision |
| if (variant instanceof GitRemoteResource) |
| return ((GitRemoteResource) variant).getCommitId().getId() |
| .getName(); |
| |
| return super.getContentIdentifier(); |
| } |
| |
| @Override |
| public long getTimestamp() { |
| if (variant instanceof GitRemoteResource) { |
| final PersonIdent author = ((GitRemoteResource) variant) |
| .getCommitId().getAuthorIdent(); |
| if (author != null) |
| return author.getWhen().getTime(); |
| } |
| |
| return super.getTimestamp(); |
| } |
| |
| @Override |
| public String getAuthor() { |
| if (variant instanceof GitRemoteResource) { |
| final PersonIdent author = ((GitRemoteResource) variant) |
| .getCommitId().getAuthorIdent(); |
| if (author != null) |
| return author.getName(); |
| } |
| |
| return super.getAuthor(); |
| } |
| |
| @Override |
| public String getComment() { |
| if (variant instanceof GitRemoteResource) |
| return ((GitRemoteResource) variant).getCommitId() |
| .getFullMessage(); |
| |
| return super.getComment(); |
| } |
| } |
| |
| @Override |
| public String getName() { |
| return CoreText.GitBranchResourceVariantTreeSubscriber_gitRepository; |
| } |
| |
| @Override |
| public IResourceVariantComparator getResourceComparator() { |
| return new GitResourceVariantComparator(gsds); |
| } |
| |
| /** |
| * As opposed to the other repository providers, EGit allows for |
| * synchronization between three remote branches. This will return the |
| * "source" tree for such synchronization use cases. |
| * |
| * @return The source tree of this subscriber. |
| * @since 3.0 |
| */ |
| protected IResourceVariantTree getSourceTree() { |
| if (sourceTree == null) |
| sourceTree = new GitSourceResourceVariantTree(cache, gsds); |
| |
| return sourceTree; |
| } |
| |
| /** |
| * This can be used to retrieve the version of the given resource |
| * corresponding to the source tree of this subscriber. |
| * |
| * @param resource |
| * The resource for which we need a variant. |
| * @return The revision of the given resource cached in the source tree of |
| * this subscriber. |
| * @throws TeamException |
| * @since 3.0 |
| */ |
| public IFileRevision getSourceFileRevision(IFile resource) |
| throws TeamException { |
| return syncInfoConverter.getLocalFileRevision(resource); |
| } |
| |
| @Override |
| protected IResourceVariantTree getBaseTree() { |
| if (baseTree == null) |
| baseTree = new GitBaseResourceVariantTree(cache, gsds); |
| |
| return baseTree; |
| } |
| |
| @Override |
| protected IResourceVariantTree getRemoteTree() { |
| if (remoteTree == null) |
| remoteTree = new GitRemoteResourceVariantTree(cache, gsds); |
| |
| return remoteTree; |
| } |
| |
| @Override |
| protected SyncInfo getSyncInfo(IResource local, IResourceVariant base, |
| IResourceVariant remote) throws TeamException { |
| GitSynchronizeData data = gsds.getData(local.getProject()); |
| Repository repo = data != null ? data.getRepository() : null; |
| return repo != null ? getSyncInfo(local, base, remote, repo) : null; |
| } |
| |
| /** |
| * Provide a new and initialized {@link SyncInfo} for the given 'local' |
| * resource from a known repository. |
| * |
| * @param local |
| * @param base |
| * @param remote |
| * @param repo |
| * Repository to load data from |
| * @return This implementation returns a new instance of {@link GitSyncInfo} |
| * @throws TeamException |
| */ |
| protected SyncInfo getSyncInfo(IResource local, IResourceVariant base, |
| IResourceVariant remote, Repository repo) throws TeamException { |
| SyncInfo info = new GitSyncInfo(local, base, remote, |
| getResourceComparator(), cache.get(repo), repo); |
| info.init(); |
| return info; |
| } |
| |
| } |