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
9 * Mathias Kinzler (SAP AG) - initial implementation
10 *******************************************************************************/
11 package org
.eclipse
.egit
.core
;
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
;
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
;
64 * Utility class for handling Repositories in the UI.
66 public class RepositoryUtil
{
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$
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
;
95 * Clients should obtain an instance from {@link Activator}
98 workspacePath
= ResourcesPlugin
.getWorkspace().getRoot().getLocation()
103 * Used by {@link Activator}
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.
116 public static String
getDefaultRepositoryDir() {
117 String key
= GitCorePreferences
.core_defaultRepositoryDir
;
118 String dir
= migrateRepoRootPreference();
119 IEclipsePreferences p
= InstanceScope
.INSTANCE
120 .getNode(Activator
.getPluginId());
122 dir
= p
.get(key
, getDefaultDefaultRepositoryDir());
126 IStringVariableManager manager
= VariablesPlugin
.getDefault()
127 .getStringVariableManager();
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()
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
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()) {
162 p
.remove(deprecatedUiKey
);
168 * Tries to map a commit to a symbolic reference.
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"
174 * Since this mapping is not unique, the following precedence rules are
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>
187 * the {@link Repository}
191 * if true, the cache will be invalidated
192 * @return the symbolic reference, or <code>null</code> if no such reference
195 public String
mapCommitToRef(Repository repository
, String commitId
,
197 synchronized (commitMappingCache
) {
199 if (!ObjectId
.isId(commitId
)) {
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());
213 ObjectId objectId
= ref
.getObjectId();
214 if (objectId
!= null && objectId
.getName()
216 return checkoutEntry
.getToBranch();
218 ref
= repository
.peel(ref
);
221 ObjectId id
= ref
.getPeeledObjectId();
222 if (id
!= null && id
.getName().equals(commitId
)) {
223 return checkoutEntry
.getToBranch();
230 } catch (IOException e
) {
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(),
249 Map
<String
, Date
> tagMap
= new HashMap
<String
, Date
>();
250 try (RevWalk rw
= new RevWalk(repository
)) {
251 Map
<String
, Ref
> tags
= repository
.getRefDatabase().getRefs(
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
)) {
259 if (tag
.getTaggerIdent() != null) {
260 timestamp
= tag
.getTaggerIdent().getWhen();
263 RevCommit commit
= rw
.parseCommit(tag
.getObject());
264 timestamp
= commit
.getCommitterIdent().getWhen();
265 } catch (IncorrectObjectTypeException e
) {
266 // not referencing a comit.
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
) {
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
311 Map
<String
, Ref
> remoteBranches
= repository
312 .getRefDatabase().getRefs(Constants
.R_HEADS
);
313 for (Ref branch
: remoteBranches
.values()) {
314 ObjectId objectId
= branch
.getObjectId();
316 && objectId
.name().equals(commitId
)) {
317 branchNames
.add(branch
.getName());
320 } catch (IOException e
) {
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
335 Map
<String
, Ref
> remoteBranches
= repository
336 .getRefDatabase().getRefs(Constants
.R_REMOTES
);
337 for (Ref branch
: remoteBranches
.values()) {
338 ObjectId objectId
= branch
.getObjectId();
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
) {
353 cacheEntry
.put(commitId
, cacheValue
);
359 * Return a cached UI "name" for a Repository
361 * This uses the name of the working directory. In case of a bare
362 * repository, the repository directory name is used.
367 public String
getRepositoryName(final Repository repository
) {
369 // Use working directory name for non-bare repositories
370 if (!repository
.isBare())
371 dir
= repository
.getWorkTree();
373 dir
= repository
.getDirectory();
376 return ""; //$NON-NLS-1$
378 synchronized (repositoryNameCache
) {
379 final String path
= dir
.getPath();
380 String name
= repositoryNameCache
.get(path
);
383 name
= dir
.getName();
384 repositoryNameCache
.put(path
, name
);
390 * @return the underlying preferences
392 public IEclipsePreferences
getPreferences() {
397 * Get the set of absolute path strings of all configured repositories.
399 * @return set of absolute paths of all configured repositories' .git
405 public Set
<String
> getRepositories() {
408 synchronized (prefs
) {
409 dirString
= prefs
.get(PREFS_DIRECTORIES_REL
, ""); //$NON-NLS-1$
410 if (dirString
.equals("")) { //$NON-NLS-1$
411 dirs
= migrateAbolutePaths();
413 dirs
= toDirSet(dirString
);
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
424 * @return set of absolute paths of all configured git repositories
426 private Set
<String
> migrateAbolutePaths() {
429 dirString
= prefs
.get(PREFS_DIRECTORIES
, ""); //$NON-NLS-1$
430 dirs
= toDirSet(dirString
);
431 // save migrated list
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()) {
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
);
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
,
482 String dirString
= repositoryDir
.getAbsolutePath();
484 List
<String
> dirStrings
= getConfiguredRepositories();
485 if (dirStrings
.contains(dirString
)) {
488 Set
<String
> dirs
= new HashSet
<String
>();
489 dirs
.addAll(dirStrings
);
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
);
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
527 prefs
.put(PREFS_DIRECTORIES
, sbAbsolute
.toString());
530 } catch (BackingStoreException e
) {
531 IStatus error
= new Status(IStatus
.ERROR
, Activator
.getPluginId(),
533 Activator
.getDefault().getLog().log(error
);
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();
555 * Does the collection of repository returned by
556 * {@link #getConfiguredRepositories()} contain the given 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
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
581 * @return short branch text
582 * @throws IOException
584 public String
getShortBranch(Repository repository
) throws IOException
{
585 Ref head
= repository
.getRef(Constants
.HEAD
);
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);
601 return Repository
.shortenRefName(ref
) + ' ' + id
.substring(0, 7);
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.
611 * Only use this if you don't already have to work with a RevWalk.
614 * @return the commit or null if HEAD does not exist or could not be parsed.
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)
623 RevCommit commit
= walk
.parseCommit(head
.getObjectId());
625 } catch (IOException e
) {
631 * Checks if existing resource with given path is to be ignored.
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.
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
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) {
655 String repoRelativePath
= mapping
.getRepoRelativePath(path
);
656 if (repoRelativePath
== null || repoRelativePath
.isEmpty()) {
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
)) {
678 * Checks if the existing resource with given path can be automatically
679 * added to the .gitignore file.
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,
686 * @throws IOException
689 public static boolean canBeAutoIgnored(IPath path
) throws IOException
{
690 Repository repository
= Activator
.getDefault().getRepositoryCache()
691 .getRepository(path
);
692 if (repository
== null || repository
.isBare()) {
695 WorkingTreeIterator treeIterator
= IteratorService
696 .createInitialIterator(repository
);
697 if (treeIterator
== null) {
700 String repoRelativePath
= path
702 new Path(repository
.getWorkTree().getAbsolutePath()))
704 if (repoRelativePath
.length() == 0
705 || repoRelativePath
.equals(path
.toString())) {
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
)) {
723 // path not found in tree, we should not automatically ignore it
728 * Checks if given repository is in the 'detached HEAD' state.
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
736 public static boolean isDetachedHead(Repository repository
) {
738 return ObjectId
.isId(repository
.getFullBranch());
739 } catch (IOException e
) {
740 Activator
.logError(e
.getMessage(), e
);
746 * Determines whether the given {@link Repository} has any changes by
747 * checking the {@link IndexDiffCacheEntry} of the repository.
751 * @return {@code true} if the repository has any changes, {@code false}
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();