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
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
;
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
;
60 @SuppressWarnings("restriction")
61 public class GitResourceVariantTreeSubscriber
extends
62 ResourceVariantTreeSubscriber
{
64 /** A resource variant tree of the source branch. */
65 private GitSourceResourceVariantTree sourceTree
;
68 * A resource variant tree of the remote branch(es).
70 private GitRemoteResourceVariantTree remoteTree
;
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();
88 public GitResourceVariantTreeSubscriber(GitSynchronizeDataSet data
) {
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
99 public void init(IProgressMonitor monitor
) {
101 CoreText
.GitResourceVariantTreeSubscriber_fetchTaskName
,
104 cache
= GitSyncCache
.getAllData(gsds
, monitor
);
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
121 * the resource to get the members of
122 * @return the resources, which may or may not exist in the workspace
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.
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
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
));
174 allMembers
.add(container
.getFile(localPath
));
180 } catch (CoreException e
) {
181 throw TeamException
.asTeamException(e
);
184 return allMembers
.toArray(new IResource
[0]);
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
);
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());
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
);
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
219 // unknown, force a refresh of the whole repository
220 path
= ""; //$NON-NLS-1$
226 // scan only the repositories that were affected
227 if (!updateRequests
.isEmpty()) {
229 GitSyncCache
.mergeAllDataIntoCache(updateRequests
, monitor
, cache
);
232 super.refresh(resources
, depth
, monitor
);
236 public IResource
[] roots() {
238 roots
= gsds
.getAllProjects();
240 return new IResource
[0];
241 IResource
[] result
= new IResource
[roots
.length
];
242 System
.arraycopy(roots
, 0, result
, 0, roots
.length
);
249 public void reset(GitSynchronizeDataSet data
) {
259 * Disposes nested resources
261 public void dispose() {
262 if (sourceTree
!= null)
263 sourceTree
.dispose();
264 if (baseTree
!= null)
266 if (remoteTree
!= null)
267 remoteTree
.dispose();
272 * Provide the synchronize data set.
274 * @return The {@link GitSynchronizeDataSet} used by this subscriber.
276 protected GitSynchronizeDataSet
getDataSet() {
281 * Provide the synchronization cache.
283 * @return The {@link GitSyncCache} used by this subscriber.
285 protected GitSyncCache
getCache() {
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
)
298 return syncInfoConverter
.getDeltaFor(info
);
302 * The default implementation of SyncInfoToDiffConverter uses inaccurate
303 * information with regards to some of EGit features.
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()).
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.
323 private class GitSyncInfoToDiffConverter
extends SyncInfoToDiffConverter
{
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
);
331 if (info
.getKind() != SyncInfo
.IN_SYNC
) {
332 IResourceVariant remote
= info
.getRemote();
333 IResource local
= info
.getLocal();
338 else if (!local
.exists())
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,
349 // For folders, we don't need file states
350 return new ResourceDiff(info
.getLocal(), kind
);
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();
364 if (ancestor
== null)
366 else if (!local
.exists())
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
,
377 // For folders, we don't need file states
378 return new ResourceDiff(info
.getLocal(), kind
);
385 * Depending on the Synchronize data, this will return either the local
386 * file or the "source" revision.
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
);
399 return asFileState(getSourceTree().getResourceVariant(local
));
400 } catch (TeamException e
) {
402 .bind(CoreText
.GitResourceVariantTreeSubscriber_CouldNotFindSourceVariant
,
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();
422 if (ancestor
== null)
424 else if (remote
== null)
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
,
437 return new ResourceDiff(info
.getLocal(), kind
);
443 * copy-pasted from the private implementation in
444 * SyncInfoToDiffConverter
446 private IFileRevision
asFileState(final IResourceVariant variant
) {
449 return asFileRevision(variant
);
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
) {
470 this.variant
= variant
;
474 public String
getContentIdentifier() {
475 // Use the same ID as would CommitFileRevision
476 if (variant
instanceof GitRemoteResource
)
477 return ((GitRemoteResource
) variant
).getCommitId().getId()
480 return super.getContentIdentifier();
484 public long getTimestamp() {
485 if (variant
instanceof GitRemoteResource
) {
486 final PersonIdent author
= ((GitRemoteResource
) variant
)
487 .getCommitId().getAuthorIdent();
489 return author
.getWhen().getTime();
492 return super.getTimestamp();
496 public String
getAuthor() {
497 if (variant
instanceof GitRemoteResource
) {
498 final PersonIdent author
= ((GitRemoteResource
) variant
)
499 .getCommitId().getAuthorIdent();
501 return author
.getName();
504 return super.getAuthor();
508 public String
getComment() {
509 if (variant
instanceof GitRemoteResource
)
510 return ((GitRemoteResource
) variant
).getCommitId()
513 return super.getComment();
518 public String
getName() {
519 return CoreText
.GitBranchResourceVariantTreeSubscriber_gitRepository
;
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.
535 protected IResourceVariantTree
getSourceTree() {
536 if (sourceTree
== null)
537 sourceTree
= new GitSourceResourceVariantTree(cache
, gsds
);
543 * This can be used to retrieve the version of the given resource
544 * corresponding to the source tree of this subscriber.
547 * The resource for which we need a variant.
548 * @return The revision of the given resource cached in the source tree of
550 * @throws TeamException
553 public IFileRevision
getSourceFileRevision(IFile resource
)
554 throws TeamException
{
555 return syncInfoConverter
.getLocalFileRevision(resource
);
559 protected IResourceVariantTree
getBaseTree() {
560 if (baseTree
== null)
561 baseTree
= new GitBaseResourceVariantTree(cache
, gsds
);
567 protected IResourceVariantTree
getRemoteTree() {
568 if (remoteTree
== null)
569 remoteTree
= new GitRemoteResourceVariantTree(cache
, gsds
);
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.
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
);