Support copy/move of workspace if Git repository is under workspace
[egit/eclipse.git] / org.eclipse.egit.core / src / org / eclipse / egit / core / RepositoryUtil.java
blob3f26c19dfcb293ed8f81d7116c759624592e4448
1 /*******************************************************************************
2 * Copyright (c) 2010, 2015 SAP AG and others.
3 * All rights reserved. This program and the accompanying materials
4 * are made available under the terms of the Eclipse Public License v1.0
5 * which accompanies this distribution, and is available at
6 * http://www.eclipse.org/legal/epl-v10.html
8 * Contributors:
9 * Mathias Kinzler (SAP AG) - initial implementation
10 *******************************************************************************/
11 package org.eclipse.egit.core;
13 import java.io.File;
14 import java.io.IOException;
15 import java.text.MessageFormat;
16 import java.util.ArrayList;
17 import java.util.Collections;
18 import java.util.Date;
19 import java.util.HashMap;
20 import java.util.HashSet;
21 import java.util.List;
22 import java.util.Map;
23 import java.util.Set;
24 import java.util.StringTokenizer;
25 import java.util.TreeSet;
27 import org.eclipse.core.resources.ResourcesPlugin;
28 import org.eclipse.core.runtime.CoreException;
29 import org.eclipse.core.runtime.IPath;
30 import org.eclipse.core.runtime.IStatus;
31 import org.eclipse.core.runtime.Path;
32 import org.eclipse.core.runtime.Status;
33 import org.eclipse.core.runtime.preferences.IEclipsePreferences;
34 import org.eclipse.core.runtime.preferences.InstanceScope;
35 import org.eclipse.core.variables.IStringVariableManager;
36 import org.eclipse.core.variables.VariablesPlugin;
37 import org.eclipse.egit.core.internal.CoreText;
38 import org.eclipse.egit.core.internal.indexdiff.IndexDiffCacheEntry;
39 import org.eclipse.egit.core.internal.indexdiff.IndexDiffData;
40 import org.eclipse.egit.core.project.RepositoryMapping;
41 import org.eclipse.jgit.annotations.NonNull;
42 import org.eclipse.jgit.annotations.Nullable;
43 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
44 import org.eclipse.jgit.lib.CheckoutEntry;
45 import org.eclipse.jgit.lib.Constants;
46 import org.eclipse.jgit.lib.FileMode;
47 import org.eclipse.jgit.lib.ObjectId;
48 import org.eclipse.jgit.lib.Ref;
49 import org.eclipse.jgit.lib.ReflogEntry;
50 import org.eclipse.jgit.lib.ReflogReader;
51 import org.eclipse.jgit.lib.Repository;
52 import org.eclipse.jgit.lib.RepositoryCache.FileKey;
53 import org.eclipse.jgit.revwalk.RevCommit;
54 import org.eclipse.jgit.revwalk.RevObject;
55 import org.eclipse.jgit.revwalk.RevTag;
56 import org.eclipse.jgit.revwalk.RevWalk;
57 import org.eclipse.jgit.treewalk.TreeWalk;
58 import org.eclipse.jgit.treewalk.WorkingTreeIterator;
59 import org.eclipse.jgit.treewalk.filter.PathFilter;
60 import org.eclipse.jgit.util.FS;
61 import org.osgi.service.prefs.BackingStoreException;
63 /**
64 * Utility class for handling Repositories in the UI.
66 public class RepositoryUtil {
68 /**
69 * The preferences to store the absolute paths of all repositories shown in
70 * the Git Repositories view
72 * @deprecated maintained to ensure compatibility for old EGit versions
74 public static final String PREFS_DIRECTORIES = "GitRepositoriesView.GitDirectories"; //$NON-NLS-1$
76 /**
77 * The preferences to store paths of all repositories shown in the Git
78 * Repositories view. For repositories located in the Eclipse workspace
79 * store the relative path to the workspace root to enable moving and
80 * copying the workspace. For repositories outside the Eclipse workspace
81 * store their absolute path.
83 public static final String PREFS_DIRECTORIES_REL = "GitRepositoriesView.GitDirectories.relative"; //$NON-NLS-1$
85 private final Map<String, Map<String, String>> commitMappingCache = new HashMap<String, Map<String, String>>();
87 private final Map<String, String> repositoryNameCache = new HashMap<String, String>();
89 private final IEclipsePreferences prefs = InstanceScope.INSTANCE
90 .getNode(Activator.getPluginId());
92 private final java.nio.file.Path workspacePath;
94 /**
95 * Clients should obtain an instance from {@link Activator}
97 RepositoryUtil() {
98 workspacePath = ResourcesPlugin.getWorkspace().getRoot().getLocation()
99 .toFile().toPath();
103 * Used by {@link Activator}
105 void dispose() {
106 commitMappingCache.clear();
107 repositoryNameCache.clear();
111 * @return The default repository directory as configured in the
112 * preferences, with variables substituted. Returns workspace
113 * location if there was an error during substitution.
115 @NonNull
116 public static String getDefaultRepositoryDir() {
117 String key = GitCorePreferences.core_defaultRepositoryDir;
118 String dir = migrateRepoRootPreference();
119 IEclipsePreferences p = InstanceScope.INSTANCE
120 .getNode(Activator.getPluginId());
121 if (dir == null) {
122 dir = p.get(key, getDefaultDefaultRepositoryDir());
123 } else {
124 p.put(key, dir);
126 IStringVariableManager manager = VariablesPlugin.getDefault()
127 .getStringVariableManager();
128 String result;
129 try {
130 result = manager.performStringSubstitution(dir);
131 } catch (CoreException e) {
132 result = ""; //$NON-NLS-1$
134 if (result == null || result.isEmpty()) {
135 result = ResourcesPlugin.getWorkspace().getRoot().getRawLocation()
136 .toOSString();
138 return result;
141 @NonNull
142 static String getDefaultDefaultRepositoryDir() {
143 return new File(FS.DETECTED.userHome(), "git").getPath(); //$NON-NLS-1$
147 * Prior to 4.1 the preference was hosted in the UI plugin. So if this one
148 * exists, we remove it from there and return. Otherwise null is returned.
150 * @return previously existing UI preference or null
152 @Nullable
153 private static String migrateRepoRootPreference() {
154 IEclipsePreferences p = InstanceScope.INSTANCE
155 .getNode("org.eclipse.egit.ui"); //$NON-NLS-1$
156 String deprecatedUiKey = "default_repository_dir"; //$NON-NLS-1$
157 String value = p.get(deprecatedUiKey, null);
158 if (value != null && value.isEmpty()) {
159 value = null;
161 if (value != null) {
162 p.remove(deprecatedUiKey);
164 return value;
168 * Tries to map a commit to a symbolic reference.
169 * <p>
170 * This value will be cached for the given commit ID unless refresh is
171 * specified. The return value will be the full name, e.g.
172 * "refs/remotes/someBranch", "refs/tags/v.1.0"
173 * <p>
174 * Since this mapping is not unique, the following precedence rules are
175 * used:
176 * <ul>
177 * <li>Tags take precedence over branches</li>
178 * <li>Local branches take preference over remote branches</li>
179 * <li>Newer references take precedence over older ones where time stamps
180 * are available. Use commiter time stamp from commit if no stamp can be
181 * found on the tag</li>
182 * <li>If there are still ambiguities, the reference name with the highest
183 * lexicographic value will be returned</li>
184 * </ul>
186 * @param repository
187 * the {@link Repository}
188 * @param commitId
189 * a commit
190 * @param refresh
191 * if true, the cache will be invalidated
192 * @return the symbolic reference, or <code>null</code> if no such reference
193 * can be found
195 public String mapCommitToRef(Repository repository, String commitId,
196 boolean refresh) {
197 synchronized (commitMappingCache) {
199 if (!ObjectId.isId(commitId)) {
200 return null;
203 try {
204 ReflogReader reflogReader = repository.getReflogReader(Constants.HEAD);
205 if (reflogReader != null) {
206 List<ReflogEntry> lastEntry = reflogReader.getReverseEntries();
207 for (ReflogEntry entry : lastEntry) {
208 if (entry.getNewId().name().equals(commitId)) {
209 CheckoutEntry checkoutEntry = entry.parseCheckout();
210 if (checkoutEntry != null) {
211 Ref ref = repository.getRef(checkoutEntry.getToBranch());
212 if (ref != null) {
213 ObjectId objectId = ref.getObjectId();
214 if (objectId != null && objectId.getName()
215 .equals(commitId)) {
216 return checkoutEntry.getToBranch();
218 ref = repository.peel(ref);
220 if (ref != null) {
221 ObjectId id = ref.getPeeledObjectId();
222 if (id != null && id.getName().equals(commitId)) {
223 return checkoutEntry.getToBranch();
230 } catch (IOException e) {
231 // ignore here
234 Map<String, String> cacheEntry = commitMappingCache.get(repository
235 .getDirectory().toString());
236 if (!refresh && cacheEntry != null
237 && cacheEntry.containsKey(commitId)) {
238 // this may be null in fact
239 return cacheEntry.get(commitId);
241 if (cacheEntry == null) {
242 cacheEntry = new HashMap<String, String>();
243 commitMappingCache.put(repository.getDirectory().getPath(),
244 cacheEntry);
245 } else {
246 cacheEntry.clear();
249 Map<String, Date> tagMap = new HashMap<String, Date>();
250 try (RevWalk rw = new RevWalk(repository)) {
251 Map<String, Ref> tags = repository.getRefDatabase().getRefs(
252 Constants.R_TAGS);
253 for (Ref tagRef : tags.values()) {
254 RevObject any = rw.parseAny(repository.resolve(tagRef.getName()));
255 if (any instanceof RevTag) {
256 RevTag tag = (RevTag) any;
257 if (tag.getObject().name().equals(commitId)) {
258 Date timestamp;
259 if (tag.getTaggerIdent() != null) {
260 timestamp = tag.getTaggerIdent().getWhen();
261 } else {
262 try {
263 RevCommit commit = rw.parseCommit(tag.getObject());
264 timestamp = commit.getCommitterIdent().getWhen();
265 } catch (IncorrectObjectTypeException e) {
266 // not referencing a comit.
267 timestamp = null;
270 tagMap.put(tagRef.getName(), timestamp);
272 } else if (any instanceof RevCommit) {
273 RevCommit commit = ((RevCommit)any);
274 if (commit.name().equals(commitId))
275 tagMap.put(tagRef.getName(), commit.getCommitterIdent().getWhen());
276 } // else ignore here
278 } catch (IOException e) {
279 // ignore here
282 String cacheValue = null;
284 if (!tagMap.isEmpty()) {
285 // we try to obtain the "latest" tag
286 Date compareDate = new Date(0);
287 for (Map.Entry<String, Date> tagEntry : tagMap.entrySet()) {
288 if (tagEntry.getValue() != null
289 && tagEntry.getValue().after(compareDate)) {
290 compareDate = tagEntry.getValue();
291 cacheValue = tagEntry.getKey();
294 // if we don't have time stamps, we sort
295 if (cacheValue == null) {
296 String compareString = ""; //$NON-NLS-1$
297 for (String tagName : tagMap.keySet()) {
298 if (tagName.compareTo(compareString) >= 0) {
299 cacheValue = tagName;
300 compareString = tagName;
306 if (cacheValue == null) {
307 // we didnt't find a tag, so let's look for local branches
308 Set<String> branchNames = new TreeSet<String>();
309 // put this into a sorted set
310 try {
311 Map<String, Ref> remoteBranches = repository
312 .getRefDatabase().getRefs(Constants.R_HEADS);
313 for (Ref branch : remoteBranches.values()) {
314 ObjectId objectId = branch.getObjectId();
315 if (objectId != null
316 && objectId.name().equals(commitId)) {
317 branchNames.add(branch.getName());
320 } catch (IOException e) {
321 // ignore here
323 if (!branchNames.isEmpty()) {
324 // get the last (sorted) entry
325 cacheValue = branchNames.toArray(new String[branchNames
326 .size()])[branchNames.size() - 1];
330 if (cacheValue == null) {
331 // last try: remote branches
332 Set<String> branchNames = new TreeSet<String>();
333 // put this into a sorted set
334 try {
335 Map<String, Ref> remoteBranches = repository
336 .getRefDatabase().getRefs(Constants.R_REMOTES);
337 for (Ref branch : remoteBranches.values()) {
338 ObjectId objectId = branch.getObjectId();
339 if (objectId != null
340 && objectId.name().equals(commitId)) {
341 branchNames.add(branch.getName());
344 if (!branchNames.isEmpty()) {
345 // get the last (sorted) entry
346 cacheValue = branchNames.toArray(new String[branchNames
347 .size()])[branchNames.size() - 1];
349 } catch (IOException e) {
350 // ignore here
353 cacheEntry.put(commitId, cacheValue);
354 return cacheValue;
359 * Return a cached UI "name" for a Repository
360 * <p>
361 * This uses the name of the working directory. In case of a bare
362 * repository, the repository directory name is used.
364 * @param repository
365 * @return the name
367 public String getRepositoryName(final Repository repository) {
368 File dir;
369 // Use working directory name for non-bare repositories
370 if (!repository.isBare())
371 dir = repository.getWorkTree();
372 else
373 dir = repository.getDirectory();
375 if (dir == null)
376 return ""; //$NON-NLS-1$
378 synchronized (repositoryNameCache) {
379 final String path = dir.getPath();
380 String name = repositoryNameCache.get(path);
381 if (name != null)
382 return name;
383 name = dir.getName();
384 repositoryNameCache.put(path, name);
385 return name;
390 * @return the underlying preferences
392 public IEclipsePreferences getPreferences() {
393 return prefs;
397 * Get the set of absolute path strings of all configured repositories.
399 * @return set of absolute paths of all configured repositories' .git
400 * directories
402 * @since 4.2
404 @NonNull
405 public Set<String> getRepositories() {
406 String dirString;
407 Set<String> dirs;
408 synchronized (prefs) {
409 dirString = prefs.get(PREFS_DIRECTORIES_REL, ""); //$NON-NLS-1$
410 if (dirString.equals("")) { //$NON-NLS-1$
411 dirs = migrateAbolutePaths();
412 } else {
413 dirs = toDirSet(dirString);
416 return dirs;
420 * Migrate set of absolute paths created by an older version of EGit to the
421 * new format using relative paths for repositories located under the
422 * Eclipse workspace
424 * @return set of absolute paths of all configured git repositories
426 private Set<String> migrateAbolutePaths() {
427 String dirString;
428 Set<String> dirs;
429 dirString = prefs.get(PREFS_DIRECTORIES, ""); //$NON-NLS-1$
430 dirs = toDirSet(dirString);
431 // save migrated list
432 saveDirs(dirs);
433 return dirs;
437 * @param dirs
438 * String with repository directories separated by path separator
439 * @return set of absolute paths of repository directories, relative paths
440 * are resolved against the workspace root
442 private Set<String> toDirSet(String dirs) {
443 if (dirs == null || dirs.isEmpty()) {
444 return Collections.emptySet();
446 Set<String> configuredStrings = new HashSet<String>();
447 StringTokenizer tok = new StringTokenizer(dirs, File.pathSeparator);
448 while (tok.hasMoreTokens()) {
449 configuredStrings
450 .add(workspacePath.resolve(tok.nextToken()).toString());
452 return configuredStrings;
457 * @return the list of configured Repository paths; will be sorted
459 public List<String> getConfiguredRepositories() {
460 final List<String> repos = new ArrayList<String>(getRepositories());
461 Collections.sort(repos);
462 return repos;
467 * @param repositoryDir
468 * the Repository path
469 * @return <code>true</code> if the repository path was not yet configured
470 * @throws IllegalArgumentException
471 * if the path does not "look" like a Repository
473 public boolean addConfiguredRepository(File repositoryDir)
474 throws IllegalArgumentException {
475 synchronized (prefs) {
477 if (!FileKey.isGitRepository(repositoryDir, FS.DETECTED))
478 throw new IllegalArgumentException(MessageFormat.format(
479 CoreText.RepositoryUtil_DirectoryIsNotGitDirectory,
480 repositoryDir));
482 String dirString = repositoryDir.getAbsolutePath();
484 List<String> dirStrings = getConfiguredRepositories();
485 if (dirStrings.contains(dirString)) {
486 return false;
487 } else {
488 Set<String> dirs = new HashSet<String>();
489 dirs.addAll(dirStrings);
490 dirs.add(dirString);
491 saveDirs(dirs);
492 return true;
498 * @param file
499 * @return <code>true</code> if the configuration was changed by the remove
501 public boolean removeDir(File file) {
502 synchronized (prefs) {
503 String dirString = file.getAbsolutePath();
504 Set<String> dirStrings = new HashSet<String>();
505 dirStrings.addAll(getConfiguredRepositories());
506 if (dirStrings.remove(dirString)) {
507 saveDirs(dirStrings);
508 return true;
510 return false;
514 private void saveDirs(Set<String> gitDirStrings) {
515 StringBuilder sbRelative = new StringBuilder();
516 StringBuilder sbAbsolute = new StringBuilder();
517 for (String gitDirString : gitDirStrings) {
518 sbRelative.append(relativizeToWorkspace(gitDirString));
519 sbRelative.append(File.pathSeparatorChar);
520 sbAbsolute.append(gitDirString);
521 sbAbsolute.append(File.pathSeparatorChar);
524 prefs.put(PREFS_DIRECTORIES_REL, sbRelative.toString());
525 // redundantly store absolute paths to ensure compatibility with older
526 // EGit versions
527 prefs.put(PREFS_DIRECTORIES, sbAbsolute.toString());
528 try {
529 prefs.flush();
530 } catch (BackingStoreException e) {
531 IStatus error = new Status(IStatus.ERROR, Activator.getPluginId(),
532 e.getMessage(), e);
533 Activator.getDefault().getLog().log(error);
538 * @param pathString
539 * an absolute path String
540 * @return if the given {@code pathString} is under the workspace root the
541 * relative path of {@code pathString} relative to the workspace
542 * root, otherwise the absolute path {@code pathString}. This
543 * enables moving or copying the workspace.
545 private String relativizeToWorkspace(String pathString) {
546 java.nio.file.Path p = java.nio.file.Paths.get(pathString);
547 if (p.startsWith(workspacePath)) {
548 return workspacePath.relativize(p).toString();
549 } else {
550 return pathString;
555 * Does the collection of repository returned by
556 * {@link #getConfiguredRepositories()} contain the given repository?
558 * @param repository
559 * @return true if contains repository, false otherwise
561 public boolean contains(final Repository repository) {
562 return contains(repository.getDirectory().getAbsolutePath());
566 * Does the collection of repository returned by
567 * {@link #getConfiguredRepositories()} contain the given repository
568 * directory?
570 * @param repositoryDir
571 * @return true if contains repository directory, false otherwise
573 public boolean contains(final String repositoryDir) {
574 return getRepositories().contains(repositoryDir);
578 * Get short branch text for given repository
580 * @param repository
581 * @return short branch text
582 * @throws IOException
584 public String getShortBranch(Repository repository) throws IOException {
585 Ref head = repository.getRef(Constants.HEAD);
586 if (head == null) {
587 return CoreText.RepositoryUtil_noHead;
589 ObjectId objectId = head.getObjectId();
590 if (objectId == null) {
591 return CoreText.RepositoryUtil_noHead;
594 if (head.isSymbolic()) {
595 return repository.getBranch();
598 String id = objectId.name();
599 String ref = mapCommitToRef(repository, id, false);
600 if (ref != null) {
601 return Repository.shortenRefName(ref) + ' ' + id.substring(0, 7);
602 } else {
603 return id.substring(0, 7);
608 * Resolve HEAD and parse the commit. Returns null if HEAD does not exist or
609 * could not be parsed.
610 * <p>
611 * Only use this if you don't already have to work with a RevWalk.
613 * @param repository
614 * @return the commit or null if HEAD does not exist or could not be parsed.
615 * @since 2.2
617 public RevCommit parseHeadCommit(Repository repository) {
618 try (RevWalk walk = new RevWalk(repository)) {
619 Ref head = repository.getRef(Constants.HEAD);
620 if (head == null || head.getObjectId() == null)
621 return null;
623 RevCommit commit = walk.parseCommit(head.getObjectId());
624 return commit;
625 } catch (IOException e) {
626 return null;
631 * Checks if existing resource with given path is to be ignored.
632 * <p>
633 * <b>Note:</b>The check makes sense only for files which exists in the
634 * working directory. This method returns false for paths to not existing
635 * files or directories.
637 * @param path
638 * Path to be checked, file or directory must exist on the disk
639 * @return true if the path is either not inside git repository or exists
640 * and matches an ignore rule
641 * @throws IOException
642 * @since 2.3
644 public static boolean isIgnored(IPath path) throws IOException {
645 RepositoryMapping mapping = RepositoryMapping.getMapping(path);
646 if (mapping == null) {
647 return true; // Linked resources may not be mapped
649 Repository repository = mapping.getRepository();
650 WorkingTreeIterator treeIterator = IteratorService
651 .createInitialIterator(repository);
652 if (treeIterator == null) {
653 return true;
655 String repoRelativePath = mapping.getRepoRelativePath(path);
656 if (repoRelativePath == null || repoRelativePath.isEmpty()) {
657 return true;
659 try (TreeWalk walk = new TreeWalk(repository)) {
660 walk.addTree(treeIterator);
661 walk.setFilter(PathFilter.create(repoRelativePath));
662 while (walk.next()) {
663 WorkingTreeIterator workingTreeIterator = walk.getTree(0,
664 WorkingTreeIterator.class);
665 if (walk.getPathString().equals(repoRelativePath)) {
666 return workingTreeIterator.isEntryIgnored();
668 if (workingTreeIterator.getEntryFileMode()
669 .equals(FileMode.TREE)) {
670 walk.enterSubtree();
674 return false;
678 * Checks if the existing resource with given path can be automatically
679 * added to the .gitignore file.
681 * @param path
682 * Path to be checked, file or directory must exist on the disk
683 * @return true if the file or directory at given path exists, is inside
684 * known git repository and does not match any existing ignore rule,
685 * false otherwise
686 * @throws IOException
687 * @since 4.1.0
689 public static boolean canBeAutoIgnored(IPath path) throws IOException {
690 Repository repository = Activator.getDefault().getRepositoryCache()
691 .getRepository(path);
692 if (repository == null || repository.isBare()) {
693 return false;
695 WorkingTreeIterator treeIterator = IteratorService
696 .createInitialIterator(repository);
697 if (treeIterator == null) {
698 return false;
700 String repoRelativePath = path
701 .makeRelativeTo(
702 new Path(repository.getWorkTree().getAbsolutePath()))
703 .toString();
704 if (repoRelativePath.length() == 0
705 || repoRelativePath.equals(path.toString())) {
706 return false;
708 try (TreeWalk walk = new TreeWalk(repository)) {
709 walk.addTree(treeIterator);
710 walk.setFilter(PathFilter.create(repoRelativePath));
711 while (walk.next()) {
712 WorkingTreeIterator workingTreeIterator = walk.getTree(0,
713 WorkingTreeIterator.class);
714 if (walk.getPathString().equals(repoRelativePath)) {
715 return !workingTreeIterator.isEntryIgnored();
717 if (workingTreeIterator.getEntryFileMode()
718 .equals(FileMode.TREE)) {
719 walk.enterSubtree();
723 // path not found in tree, we should not automatically ignore it
724 return false;
728 * Checks if given repository is in the 'detached HEAD' state.
730 * @param repository
731 * the repository to check
732 * @return <code>true</code> if the repository is in the 'detached HEAD'
733 * state, <code>false</code> if it's not or an error occurred
734 * @since 3.2
736 public static boolean isDetachedHead(Repository repository) {
737 try {
738 return ObjectId.isId(repository.getFullBranch());
739 } catch (IOException e) {
740 Activator.logError(e.getMessage(), e);
742 return false;
746 * Determines whether the given {@link Repository} has any changes by
747 * checking the {@link IndexDiffCacheEntry} of the repository.
749 * @param repository
750 * to check
751 * @return {@code true} if the repository has any changes, {@code false}
752 * otherwise
754 public static boolean hasChanges(@NonNull Repository repository) {
755 IndexDiffCacheEntry entry = Activator.getDefault().getIndexDiffCache()
756 .getIndexDiffCacheEntry(repository);
757 IndexDiffData data = entry != null ? entry.getIndexDiff() : null;
758 return data != null && data.hasChanges();