Skip virtual resources when auto-ignoring derived resources
[egit/eclipse.git] / org.eclipse.egit.core / src / org / eclipse / egit / core / op / ConnectProviderOperation.java
blob7ceee4b4cad4f0c7a1b6ada840b659883154a76b
1 /*******************************************************************************
2 * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org>
3 * Copyright (C) 2008, Google Inc.
4 * Copyright (C) 2009, Mykola Nikishov <mn@mn.com.ua>
5 * Copyright (C) 2013, Matthias Sohn <matthias.sohn@sap.com>
6 * Copyright (C) 2016, Stefan Dirix <sdirix@eclipsesource.com>
8 * All rights reserved. This program and the accompanying materials
9 * are made available under the terms of the Eclipse Public License 2.0
10 * which accompanies this distribution, and is available at
11 * https://www.eclipse.org/legal/epl-2.0/
13 * SPDX-License-Identifier: EPL-2.0
14 *******************************************************************************/
15 package org.eclipse.egit.core.op;
17 import java.io.File;
18 import java.io.IOException;
19 import java.util.ArrayList;
20 import java.util.Arrays;
21 import java.util.Collection;
22 import java.util.HashSet;
23 import java.util.LinkedHashMap;
24 import java.util.List;
25 import java.util.Map;
26 import java.util.Map.Entry;
27 import java.util.Set;
29 import org.eclipse.core.resources.IContainer;
30 import org.eclipse.core.resources.IProject;
31 import org.eclipse.core.resources.IResource;
32 import org.eclipse.core.resources.IResourceProxy;
33 import org.eclipse.core.resources.IResourceProxyVisitor;
34 import org.eclipse.core.resources.ResourcesPlugin;
35 import org.eclipse.core.runtime.CoreException;
36 import org.eclipse.core.runtime.IPath;
37 import org.eclipse.core.runtime.IProgressMonitor;
38 import org.eclipse.core.runtime.MultiStatus;
39 import org.eclipse.core.runtime.Path;
40 import org.eclipse.core.runtime.Platform;
41 import org.eclipse.core.runtime.SubMonitor;
42 import org.eclipse.core.runtime.jobs.ISchedulingRule;
43 import org.eclipse.core.runtime.jobs.MultiRule;
44 import org.eclipse.egit.core.Activator;
45 import org.eclipse.egit.core.GitCorePreferences;
46 import org.eclipse.egit.core.GitProvider;
47 import org.eclipse.egit.core.JobFamilies;
48 import org.eclipse.egit.core.RepositoryCache;
49 import org.eclipse.egit.core.RepositoryUtil;
50 import org.eclipse.egit.core.internal.CoreText;
51 import org.eclipse.egit.core.internal.indexdiff.IndexDiffCache;
52 import org.eclipse.egit.core.internal.indexdiff.IndexDiffCacheEntry;
53 import org.eclipse.egit.core.internal.job.JobUtil;
54 import org.eclipse.egit.core.internal.trace.GitTraceLocation;
55 import org.eclipse.egit.core.project.GitProjectData;
56 import org.eclipse.egit.core.project.RepositoryFinder;
57 import org.eclipse.egit.core.project.RepositoryMapping;
58 import org.eclipse.jgit.annotations.Nullable;
59 import org.eclipse.jgit.lib.Constants;
60 import org.eclipse.jgit.lib.Repository;
61 import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
62 import org.eclipse.osgi.util.NLS;
63 import org.eclipse.team.core.RepositoryProvider;
65 /**
66 * Connects Eclipse to an existing Git repository
68 public class ConnectProviderOperation implements IEGitOperation {
69 private final Map<IProject, File> projects = new LinkedHashMap<>();
71 private boolean refreshResources = true;
73 /**
74 * Create a new connection operation to execute within the workspace.
75 * <p>
76 * Uses <code>.git</code> as a default relative path to repository.
77 * @see #ConnectProviderOperation(IProject, File)
79 * @param proj
80 * the project to connect to the Git team provider.
82 public ConnectProviderOperation(final IProject proj) {
83 this(proj, proj.getLocation().append(Constants.DOT_GIT).toFile());
86 /**
87 * Create a new connection operation to execute within the workspace.
89 * @param proj
90 * the project to connect to the Git team provider.
91 * @param pathToRepo
92 * absolute path to the repository
94 public ConnectProviderOperation(final IProject proj, File pathToRepo) {
95 this.projects.put(proj, pathToRepo);
98 /**
99 * Create a new connection operation to execute within the workspace.
101 * @param projects
102 * the projects to connect to the Git team provider.
104 public ConnectProviderOperation(final Map<IProject, File> projects) {
105 this.projects.putAll(projects);
108 @Override
109 public void execute(IProgressMonitor m) throws CoreException {
110 SubMonitor progress = SubMonitor.convert(m,
111 CoreText.ConnectProviderOperation_connecting, projects.size());
112 MultiStatus ms = new MultiStatus(Activator.PLUGIN_ID, 0,
113 CoreText.ConnectProviderOperation_ConnectErrors, null);
114 for (Entry<IProject, File> entry : projects.entrySet()) {
115 connectProject(entry, ms, progress.newChild(1));
117 if (!ms.isOK()) {
118 throw new CoreException(ms);
122 private void connectProject(Entry<IProject, File> entry, MultiStatus ms,
123 IProgressMonitor monitor) throws CoreException {
124 IProject project = entry.getKey();
126 String taskName = NLS.bind(
127 CoreText.ConnectProviderOperation_ConnectingProject,
128 project.getName());
129 SubMonitor subMon = SubMonitor.convert(monitor, taskName, 100);
131 if (GitTraceLocation.CORE.isActive()) {
132 GitTraceLocation.getTrace()
133 .trace(GitTraceLocation.CORE.getLocation(), taskName);
136 RepositoryFinder finder = new RepositoryFinder(project);
137 finder.setFindInChildren(false);
138 Collection<RepositoryMapping> repos = finder.find(subMon.newChild(50));
139 if (repos.isEmpty()) {
140 ms.add(Activator.error(NLS.bind(
141 CoreText.ConnectProviderOperation_NoRepositoriesError,
142 project.getName()), null));
143 return;
145 RepositoryMapping actualMapping = findActualRepository(repos,
146 entry.getValue());
147 if (actualMapping == null) {
148 ms.add(Activator.error(NLS.bind(
149 CoreText.ConnectProviderOperation_UnexpectedRepositoryError,
150 new Object[] { project.getName(),
151 entry.getValue().toString(), repos.toString() }),
152 null));
153 return;
155 GitProjectData projectData = new GitProjectData(project);
156 try {
157 projectData.setRepositoryMappings(Arrays.asList(actualMapping));
158 projectData.store();
159 GitProjectData.add(project, projectData);
160 } catch (CoreException ce) {
161 ms.add(ce.getStatus());
162 deleteGitProvider(ms, project);
163 return;
164 } catch (RuntimeException ce) {
165 ms.add(Activator.error(ce.getMessage(), ce));
166 deleteGitProvider(ms, project);
167 return;
169 RepositoryProvider.map(project, GitProvider.ID);
171 IPath gitPath = actualMapping.getGitDirAbsolutePath();
172 checkForSubmodules(project, subMon.newChild(10));
173 if (refreshResources) {
174 project.refreshLocal(IResource.DEPTH_INFINITE, subMon.newChild(30));
175 if (gitPath != null) {
176 try {
177 Repository repository = RepositoryCache.INSTANCE
178 .lookupRepository(gitPath.toFile());
179 IndexDiffCacheEntry cacheEntry = IndexDiffCache.INSTANCE
180 .getIndexDiffCacheEntry(repository);
181 if (cacheEntry != null) {
182 cacheEntry.refresh();
184 } catch (IOException e) {
185 Activator.logError(e.getMessage(), e);
188 } else {
189 subMon.worked(30);
192 autoIgnoreDerivedResources(project, subMon.newChild(10));
193 if (gitPath != null) {
194 autoIgnoreWorkspaceMetaData(gitPath.toFile());
199 * Scan all descendants for files or folders named ".git" and pass them to
200 * GitProjectData to pick up possible submodules or nested repositories.
202 * @param project
203 * to process
204 * @param monitor
205 * for progress reporting and cancellation, may be {@code null}
206 * if neither is desired
208 private void checkForSubmodules(IProject project,
209 IProgressMonitor monitor) {
210 SubMonitor progress = SubMonitor.convert(monitor, 2);
211 SubMonitor seekProgress = progress.newChild(1);
212 List<IResource> gitDirs = new ArrayList<>();
213 try {
214 project.accept(new IResourceProxyVisitor() {
215 @Override
216 public boolean visit(IResourceProxy resource)
217 throws CoreException {
218 int type = resource.getType();
219 if ((type == IResource.FILE || type == IResource.FOLDER)
220 && Constants.DOT_GIT.equals(resource.getName())) {
221 seekProgress.setWorkRemaining(2);
222 gitDirs.add(resource.requestResource());
223 seekProgress.worked(1);
224 return false;
226 return true;
228 }, IResource.NONE);
229 } catch (CoreException e) {
230 Activator.logError(e.getMessage(), e);
232 GitProjectData.update(gitDirs, progress.newChild(1));
235 private void deleteGitProvider(MultiStatus ms, IProject project) {
236 try {
237 GitProjectData.delete(project);
238 } catch (IOException e) {
239 ms.add(Activator.error(e.getMessage(), e));
243 private void autoIgnoreDerivedResources(IProject project,
244 IProgressMonitor monitor) throws CoreException {
245 if (!Platform.getPreferencesService().getBoolean(Activator.PLUGIN_ID,
246 GitCorePreferences.core_autoIgnoreDerivedResources, true,
247 null)) {
248 return;
250 List<IPath> paths = findDerivedResources(project);
251 if (paths.size() > 0) {
252 IgnoreOperation ignoreOp = new IgnoreOperation(paths);
253 ignoreOp.execute(monitor);
258 * Auto-ignore the .metadata and .recommenders folders located in the
259 * workspace root if the workspace is in the working tree of a git
260 * repository (which is not recommended)
262 * @param gitDir
263 * the .git directory containing the repository metadata
265 private static void autoIgnoreWorkspaceMetaData(File gitDir) {
266 java.nio.file.Path workspaceRoot = ResourcesPlugin.getWorkspace()
267 .getRoot().getLocation().toFile().toPath();
268 try (Repository r = FileRepositoryBuilder.create(gitDir)) {
269 if (!r.isBare()
270 && workspaceRoot.startsWith(r.getWorkTree().toPath())) {
271 Collection<IPath> ignoredPaths = buildIgnoredPathsList(
272 workspaceRoot, ".metadata", //$NON-NLS-1$
273 ".recommenders"); //$NON-NLS-1$
274 JobUtil.scheduleUserJob(new IgnoreOperation(ignoredPaths),
275 CoreText.ConnectProviderOperation_autoIgnoreMetaData,
276 JobFamilies.AUTO_IGNORE);
278 } catch (IOException e) {
279 Activator.logError(e.getMessage(), e);
283 private static Collection<IPath> buildIgnoredPathsList(
284 java.nio.file.Path workspaceRoot,
285 String... metaDataDirectoryNames) {
286 Collection<IPath> ignoredPaths = new HashSet<>();
287 for (String m : metaDataDirectoryNames) {
288 Path metaData = new Path(
289 workspaceRoot.resolve(m).toAbsolutePath().toString());
290 try {
291 if (RepositoryUtil.canBeAutoIgnored(metaData)) {
292 ignoredPaths.add(metaData);
294 } catch (IOException e) {
295 Activator.logError(e.getMessage(), e);
298 return ignoredPaths;
301 private List<IPath> findDerivedResources(IContainer c)
302 throws CoreException {
303 List<IPath> derived = new ArrayList<>();
304 IResource[] members = c.members(IContainer.INCLUDE_HIDDEN);
305 for (IResource r : members) {
306 if (r.isVirtual()) {
307 continue;
309 if (r.isDerived()) {
310 derived.add(r.getLocation());
311 } else if (r instanceof IContainer) {
312 derived.addAll(findDerivedResources((IContainer) r));
315 return derived;
318 @Override
319 public ISchedulingRule getSchedulingRule() {
320 Set<IProject> projectSet = projects.keySet();
321 return new MultiRule(projectSet.toArray(new IProject[0]));
325 * @param repos
326 * available repositories
327 * @param suggestedRepo
328 * relative path to git repository
329 * @return a repository mapping which corresponds to a suggested repository
330 * location, <code>null</code> otherwise
332 @Nullable
333 private RepositoryMapping findActualRepository(
334 Collection<RepositoryMapping> repos, File suggestedRepo) {
335 File path = Path.fromOSString(suggestedRepo.getPath()).toFile();
336 for (RepositoryMapping rm : repos) {
337 IPath other = rm.getGitDirAbsolutePath();
338 if (other == null) {
339 continue;
341 if (path.equals(other.toFile())) {
342 return rm;
345 return null;
349 * @param refresh
350 * true to refresh resources after connect operation (default)
352 public void setRefreshResources(boolean refresh) {
353 this.refreshResources = refresh;