refactor: simplify collection.toArray()
[egit/eclipse.git] / org.eclipse.egit.core / src / org / eclipse / egit / core / synchronize / GitResourceVariantTreeSubscriber.java
blobb6f35b057ee1522afa2891c6cd43bb3e619e8406
1 /*******************************************************************************
2 * Copyright (c) 2010, 2013 IBM Corporation and others.
3 * All rights reserved. This program and the accompanying materials
4 * are made available under the terms of the Eclipse Public License 2.0
5 * which accompanies this distribution, and is available at
6 * https://www.eclipse.org/legal/epl-2.0/
8 * SPDX-License-Identifier: EPL-2.0
10 * Contributors:
11 * IBM Corporation - initial API and implementation
12 * Dariusz Luksza <dariusz@luksza.org>
13 * François Rey - gracefully ignore linked resources
14 * Laurent Goubet <laurent.goubet@obeo.fr> - 403363
15 *******************************************************************************/
16 package org.eclipse.egit.core.synchronize;
18 import static org.eclipse.jgit.lib.Repository.stripWorkDir;
20 import java.util.ArrayList;
21 import java.util.Collection;
22 import java.util.HashMap;
23 import java.util.Map;
25 import org.eclipse.core.resources.IContainer;
26 import org.eclipse.core.resources.IFile;
27 import org.eclipse.core.resources.IProject;
28 import org.eclipse.core.resources.IResource;
29 import org.eclipse.core.runtime.CoreException;
30 import org.eclipse.core.runtime.IPath;
31 import org.eclipse.core.runtime.IProgressMonitor;
32 import org.eclipse.core.runtime.Path;
33 import org.eclipse.egit.core.Activator;
34 import org.eclipse.egit.core.internal.CoreText;
35 import org.eclipse.egit.core.internal.storage.WorkspaceFileRevision;
36 import org.eclipse.egit.core.project.RepositoryMapping;
37 import org.eclipse.egit.core.synchronize.dto.GitSynchronizeData;
38 import org.eclipse.egit.core.synchronize.dto.GitSynchronizeDataSet;
39 import org.eclipse.jgit.lib.PersonIdent;
40 import org.eclipse.jgit.lib.Repository;
41 import org.eclipse.jgit.treewalk.TreeWalk;
42 import org.eclipse.osgi.util.NLS;
43 import org.eclipse.team.core.TeamException;
44 import org.eclipse.team.core.diff.IDiff;
45 import org.eclipse.team.core.diff.ITwoWayDiff;
46 import org.eclipse.team.core.diff.provider.ThreeWayDiff;
47 import org.eclipse.team.core.history.IFileRevision;
48 import org.eclipse.team.core.mapping.provider.ResourceDiff;
49 import org.eclipse.team.core.synchronize.SyncInfo;
50 import org.eclipse.team.core.variants.IResourceVariant;
51 import org.eclipse.team.core.variants.IResourceVariantComparator;
52 import org.eclipse.team.core.variants.IResourceVariantTree;
53 import org.eclipse.team.core.variants.ResourceVariantTreeSubscriber;
54 import org.eclipse.team.internal.core.mapping.ResourceVariantFileRevision;
55 import org.eclipse.team.internal.core.mapping.SyncInfoToDiffConverter;
57 /**
60 @SuppressWarnings("restriction")
61 public class GitResourceVariantTreeSubscriber extends
62 ResourceVariantTreeSubscriber {
64 /** A resource variant tree of the source branch. */
65 private GitSourceResourceVariantTree sourceTree;
67 /**
68 * A resource variant tree of the remote branch(es).
70 private GitRemoteResourceVariantTree remoteTree;
72 /**
73 * A resource variant tree against HEAD.
75 private GitBaseResourceVariantTree baseTree;
77 private GitSynchronizeDataSet gsds;
79 private IResource[] roots;
81 private GitSyncCache cache;
83 private GitSyncInfoToDiffConverter syncInfoConverter = new GitSyncInfoToDiffConverter();
85 /**
86 * @param data
88 public GitResourceVariantTreeSubscriber(GitSynchronizeDataSet data) {
89 this.gsds = data;
92 /**
93 * Initialize git subscriber. This method will pre-fetch data from git
94 * repository. This approach will reduce number of {@link TreeWalk}'s
95 * created during synchronization
97 * @param monitor
99 public void init(IProgressMonitor monitor) {
100 monitor.beginTask(
101 CoreText.GitResourceVariantTreeSubscriber_fetchTaskName,
102 gsds.size());
103 try {
104 cache = GitSyncCache.getAllData(gsds, monitor);
105 } finally {
106 monitor.done();
110 @Override
111 public boolean isSupervised(IResource res) throws TeamException {
112 return gsds.contains(res.getProject()) && gsds.shouldBeIncluded(res);
116 * Returns all members of the given resource as recorded by git. Resources
117 * ignored by git via .gitignore will not be returned, even if they exist in
118 * the workspace.
120 * @param res
121 * the resource to get the members of
122 * @return the resources, which may or may not exist in the workspace
124 @Override
125 public IResource[] members(IResource res) throws TeamException {
126 if (res.getType() == IResource.FILE || !gsds.shouldBeIncluded(res)) {
127 return new IResource[0];
129 GitSynchronizeData gsd = gsds.getData(res.getProject());
130 Repository repo = gsd.getRepository();
131 GitSyncObjectCache repoCache = cache.get(repo);
133 Collection<IResource> allMembers = new ArrayList<>();
134 Map<String, IResource> existingMembers = new HashMap<>();
136 String path = stripWorkDir(repo.getWorkTree(), res.getLocation().toFile());
137 GitSyncObjectCache cachedMembers = repoCache.get(path);
138 // A normal synchronizer would just return the union of existing
139 // resources and non-existing ones that exist only in git. For git,
140 // however, we want to ignore .gitignored resources completely, and
141 // include untracked files only if the preference to do so is set
142 // (in which case the cache will contain them already). So we add
143 // only the existing ones that are also recorded in the git 3-way
144 // cache, plus those recorded only in git, plus the git recorded
145 // one if it's a file vs.folder conflict.
146 try {
147 IContainer container = (IContainer) res;
148 // Existing resources
149 if (container.exists()) {
150 for (IResource member : container.members()) {
151 existingMembers.put(member.getName(), member);
155 // Now add the ones from git
156 if (cachedMembers != null) {
157 Collection<GitSyncObjectCache> members = cachedMembers
158 .members();
159 if (members != null) {
160 for (GitSyncObjectCache gitMember : members) {
161 String name = gitMember.getName();
162 IResource existing = existingMembers.get(name);
163 if (existing != null) {
164 allMembers.add(existing);
166 if (existing == null || (existing
167 .getType() != IResource.FILE) != gitMember
168 .getDiffEntry().isTree()) {
169 // Non-existing, or file vs. folder
170 IPath localPath = new Path(name);
171 if (gitMember.getDiffEntry().isTree()) {
172 allMembers.add(container.getFolder(localPath));
173 } else {
174 allMembers.add(container.getFile(localPath));
180 } catch (CoreException e) {
181 throw TeamException.asTeamException(e);
184 return allMembers.toArray(new IResource[0]);
187 @Override
188 public void refresh(IResource[] resources, int depth,
189 IProgressMonitor monitor) throws TeamException {
190 for (IResource resource : resources) {
191 // check to see if there is a full refresh
192 if (resource.getType() == IResource.ROOT) {
193 // refresh entire cache
194 cache = GitSyncCache.getAllData(gsds, monitor);
195 super.refresh(resources, depth, monitor);
196 return;
200 // not refreshing the workspace, locate and collect target resources
201 Map<GitSynchronizeData, Collection<String>> updateRequests = new HashMap<GitSynchronizeData, Collection<String>>();
202 for (IResource resource : resources) {
203 IProject project = resource.getProject();
204 GitSynchronizeData data = gsds.getData(project.getName());
205 if (data != null) {
206 RepositoryMapping mapping = RepositoryMapping
207 .getMapping(project);
208 // mapping may be null if the project has been closed
209 if (mapping != null) {
210 Collection<String> paths = updateRequests.get(data);
211 if (paths == null) {
212 paths = new ArrayList<String>();
213 updateRequests.put(data, paths);
216 String path = mapping.getRepoRelativePath(resource);
217 // null path may be returned, check for this
218 if (path == null)
219 // unknown, force a refresh of the whole repository
220 path = ""; //$NON-NLS-1$
221 paths.add(path);
226 // scan only the repositories that were affected
227 if (!updateRequests.isEmpty()) {
228 // refresh cache
229 GitSyncCache.mergeAllDataIntoCache(updateRequests, monitor, cache);
232 super.refresh(resources, depth, monitor);
235 @Override
236 public IResource[] roots() {
237 if (roots == null)
238 roots = gsds.getAllProjects();
239 if (roots == null)
240 return new IResource[0];
241 IResource[] result = new IResource[roots.length];
242 System.arraycopy(roots, 0, result, 0, roots.length);
243 return result;
247 * @param data
249 public void reset(GitSynchronizeDataSet data) {
250 gsds = data;
252 roots = null;
253 sourceTree = null;
254 baseTree = null;
255 remoteTree = null;
259 * Disposes nested resources
261 public void dispose() {
262 if (sourceTree != null)
263 sourceTree.dispose();
264 if (baseTree != null)
265 baseTree.dispose();
266 if (remoteTree != null)
267 remoteTree.dispose();
268 gsds.dispose();
272 * Provide the synchronize data set.
274 * @return The {@link GitSynchronizeDataSet} used by this subscriber.
276 protected GitSynchronizeDataSet getDataSet() {
277 return gsds;
281 * Provide the synchronization cache.
283 * @return The {@link GitSyncCache} used by this subscriber.
285 protected GitSyncCache getCache() {
286 return cache;
289 @Override
290 public IDiff getDiff(IResource resource) throws CoreException {
291 final GitSynchronizeData syncData = gsds.getData(resource.getProject());
292 if (syncData == null || syncData.shouldIncludeLocal())
293 return super.getDiff(resource);
295 SyncInfo info = getSyncInfo(resource);
296 if (info == null || info.getKind() == SyncInfo.IN_SYNC)
297 return null;
298 return syncInfoConverter.getDeltaFor(info);
302 * The default implementation of SyncInfoToDiffConverter uses inaccurate
303 * information with regards to some of EGit features.
304 * <p>
305 * SyncInfoToDiffConverter#asFileRevision(IResourceVariant) is called when a
306 * user double-clicks a revision from the synchronize view (among others).
307 * However, the default implementation returns an IFileRevision with no
308 * comment, author or timestamp information (this can be observed by
309 * commenting this implementation out and launching
310 * HistoryTest#queryHistoryThroughTeam()).
311 * </p>
312 * <p>
313 * SyncInfoToDiffConverter#getDeltaFor(SyncInfo) had been originally thought
314 * by Team to be used for synchronizations that considered local changes.
315 * This is not always the case with EGit. For example, a user might try and
316 * compare two refs together from the Git repository explorer (right click >
317 * synchronize with each other). In such a case, the local files must not be
318 * taken into account (i.e. we must respect the value of our
319 * GitSynchronizeData#shouldIncludeLocal(). Most of the private methods here
320 * were copy/pasted from the super implementation.
321 * </p>
323 private class GitSyncInfoToDiffConverter extends SyncInfoToDiffConverter {
324 @Override
325 public IDiff getDeltaFor(SyncInfo info) {
326 if (info.getComparator().isThreeWay()) {
327 ITwoWayDiff local = getLocalDelta(info);
328 ITwoWayDiff remote = getRemoteDelta(info);
329 return new ThreeWayDiff(local, remote);
330 } else {
331 if (info.getKind() != SyncInfo.IN_SYNC) {
332 IResourceVariant remote = info.getRemote();
333 IResource local = info.getLocal();
335 int kind;
336 if (remote == null)
337 kind = IDiff.REMOVE;
338 else if (!local.exists())
339 kind = IDiff.ADD;
340 else
341 kind = IDiff.CHANGE;
343 if (local.getType() == IResource.FILE) {
344 IFileRevision after = asFileState(remote);
345 IFileRevision before = getLocalFileRevision((IFile) local);
346 return new ResourceDiff(info.getLocal(), kind, 0,
347 before, after);
349 // For folders, we don't need file states
350 return new ResourceDiff(info.getLocal(), kind);
352 return null;
356 private ITwoWayDiff getLocalDelta(SyncInfo info) {
357 int direction = SyncInfo.getDirection(info.getKind());
358 if (direction == SyncInfo.OUTGOING
359 || direction == SyncInfo.CONFLICTING) {
360 IResourceVariant ancestor = info.getBase();
361 IResource local = info.getLocal();
363 int kind;
364 if (ancestor == null)
365 kind = IDiff.ADD;
366 else if (!local.exists())
367 kind = IDiff.REMOVE;
368 else
369 kind = IDiff.CHANGE;
371 if (local.getType() == IResource.FILE) {
372 IFileRevision before = asFileState(ancestor);
373 IFileRevision after = getLocalFileRevision((IFile) local);
374 return new ResourceDiff(info.getLocal(), kind, 0, before,
375 after);
377 // For folders, we don't need file states
378 return new ResourceDiff(info.getLocal(), kind);
381 return null;
385 * Depending on the Synchronize data, this will return either the local
386 * file or the "source" revision.
388 * @param local
389 * The local file.
390 * @return The file revision that should be considered for the local
391 * (left) side a delta
393 protected IFileRevision getLocalFileRevision(IFile local) {
394 final GitSynchronizeData data = gsds.getData(local.getProject());
395 if (data.shouldIncludeLocal())
396 return new WorkspaceFileRevision(local);
398 try {
399 return asFileState(getSourceTree().getResourceVariant(local));
400 } catch (TeamException e) {
401 String error = NLS
402 .bind(CoreText.GitResourceVariantTreeSubscriber_CouldNotFindSourceVariant,
403 local.getName());
404 Activator.logError(error, e);
405 // fall back to the working tree version
406 return new WorkspaceFileRevision(local);
411 * copy-pasted from the private implementation in
412 * SyncInfoToDiffConverter
414 private ITwoWayDiff getRemoteDelta(SyncInfo info) {
415 int direction = SyncInfo.getDirection(info.getKind());
416 if (direction == SyncInfo.INCOMING
417 || direction == SyncInfo.CONFLICTING) {
418 IResourceVariant ancestor = info.getBase();
419 IResourceVariant remote = info.getRemote();
421 int kind;
422 if (ancestor == null)
423 kind = IDiff.ADD;
424 else if (remote == null)
425 kind = IDiff.REMOVE;
426 else
427 kind = IDiff.CHANGE;
429 // For folders, we don't need file states
430 if (info.getLocal().getType() == IResource.FILE) {
431 IFileRevision before = asFileState(ancestor);
432 IFileRevision after = asFileState(remote);
433 return new ResourceDiff(info.getLocal(), kind, 0, before,
434 after);
437 return new ResourceDiff(info.getLocal(), kind);
439 return null;
443 * copy-pasted from the private implementation in
444 * SyncInfoToDiffConverter
446 private IFileRevision asFileState(final IResourceVariant variant) {
447 if (variant == null)
448 return null;
449 return asFileRevision(variant);
452 @Override
453 protected ResourceVariantFileRevision asFileRevision(
454 IResourceVariant variant) {
455 return new GitResourceVariantFileRevision(variant);
460 * The default implementation of ResourceVariantFileRevision has no author,
461 * comment, timestamp... or any information that could be provided by the
462 * Git resource variant. This implementation uses the variant's information.
464 private static class GitResourceVariantFileRevision extends
465 ResourceVariantFileRevision {
466 private final IResourceVariant variant;
468 public GitResourceVariantFileRevision(IResourceVariant variant) {
469 super(variant);
470 this.variant = variant;
473 @Override
474 public String getContentIdentifier() {
475 // Use the same ID as would CommitFileRevision
476 if (variant instanceof GitRemoteResource)
477 return ((GitRemoteResource) variant).getCommitId().getId()
478 .getName();
480 return super.getContentIdentifier();
483 @Override
484 public long getTimestamp() {
485 if (variant instanceof GitRemoteResource) {
486 final PersonIdent author = ((GitRemoteResource) variant)
487 .getCommitId().getAuthorIdent();
488 if (author != null)
489 return author.getWhen().getTime();
492 return super.getTimestamp();
495 @Override
496 public String getAuthor() {
497 if (variant instanceof GitRemoteResource) {
498 final PersonIdent author = ((GitRemoteResource) variant)
499 .getCommitId().getAuthorIdent();
500 if (author != null)
501 return author.getName();
504 return super.getAuthor();
507 @Override
508 public String getComment() {
509 if (variant instanceof GitRemoteResource)
510 return ((GitRemoteResource) variant).getCommitId()
511 .getFullMessage();
513 return super.getComment();
517 @Override
518 public String getName() {
519 return CoreText.GitBranchResourceVariantTreeSubscriber_gitRepository;
522 @Override
523 public IResourceVariantComparator getResourceComparator() {
524 return new GitResourceVariantComparator(gsds);
528 * As opposed to the other repository providers, EGit allows for
529 * synchronization between three remote branches. This will return the
530 * "source" tree for such synchronization use cases.
532 * @return The source tree of this subscriber.
533 * @since 3.0
535 protected IResourceVariantTree getSourceTree() {
536 if (sourceTree == null)
537 sourceTree = new GitSourceResourceVariantTree(cache, gsds);
539 return sourceTree;
543 * This can be used to retrieve the version of the given resource
544 * corresponding to the source tree of this subscriber.
546 * @param resource
547 * The resource for which we need a variant.
548 * @return The revision of the given resource cached in the source tree of
549 * this subscriber.
550 * @throws TeamException
551 * @since 3.0
553 public IFileRevision getSourceFileRevision(IFile resource)
554 throws TeamException {
555 return syncInfoConverter.getLocalFileRevision(resource);
558 @Override
559 protected IResourceVariantTree getBaseTree() {
560 if (baseTree == null)
561 baseTree = new GitBaseResourceVariantTree(cache, gsds);
563 return baseTree;
566 @Override
567 protected IResourceVariantTree getRemoteTree() {
568 if (remoteTree == null)
569 remoteTree = new GitRemoteResourceVariantTree(cache, gsds);
571 return remoteTree;
574 @Override
575 protected SyncInfo getSyncInfo(IResource local, IResourceVariant base,
576 IResourceVariant remote) throws TeamException {
578 Repository repo = gsds.getData(local.getProject()).getRepository();
579 return getSyncInfo(local, base, remote, repo);
583 * Provide a new and initialized {@link SyncInfo} for the given 'local'
584 * resource from a known repository.
586 * @param local
587 * @param base
588 * @param remote
589 * @param repo
590 * Repository to load data from
591 * @return This implementation returns a new instance of {@link GitSyncInfo}
592 * @throws TeamException
594 protected SyncInfo getSyncInfo(IResource local, IResourceVariant base,
595 IResourceVariant remote, Repository repo) throws TeamException {
596 SyncInfo info = new GitSyncInfo(local, base, remote,
597 getResourceComparator(), cache.get(repo), repo);
598 info.init();
599 return info;