blob: 4ad12d8a121fe699ea8615af646cad638820204e [file] [log] [blame]
/*
* Copyright 2000-2011 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package git4idea.repo;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.components.ServiceManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.openapi.vcs.changes.ChangeListManager;
import com.intellij.openapi.vcs.changes.VcsDirtyScopeManager;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.VirtualFileManager;
import com.intellij.openapi.vfs.newvfs.BulkFileListener;
import com.intellij.openapi.vfs.newvfs.events.*;
import com.intellij.util.messages.MessageBusConnection;
import git4idea.GitUtil;
import git4idea.commands.Git;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
/**
* <p>
* Stores files which are untracked by the Git repository.
* Should be updated by calling {@link #add(com.intellij.openapi.vfs.VirtualFile)} and {@link #remove(java.util.Collection)}
* whenever the list of unversioned files changes.
* Able to get the list of unversioned files from Git.
* </p>
*
* <p>
* This class is used by {@link git4idea.status.GitNewChangesCollector}.
* By keeping track of unversioned files in the Git repository we may invoke
* <code>'git status --porcelain --untracked-files=no'</code> which gives a significant speed boost: the command gets more than twice
* faster, because it doesn't need to seek for untracked files.
* </p>
*
* <p>
* "Keeping track" means the following:
* <ul>
* <li>
* Once a file is created, it is added to untracked (by this class).
* Once a file is deleted, it is removed from untracked.
* </li>
* <li>
* Once a file is added to the index, it is removed from untracked.
* Once it is removed from the index, it is added to untracked.
* </li>
* </ul>
* </p>
* <p>
* In some cases (file creation/deletion) the file is not silently added/removed from the list - instead the file is marked as
* "possibly untracked" and Git is asked for the exact status of this file.
* It is needed, since the file may be created and added to the index independently, and events may race.
* </p>
* <p>
* Also, if .git/index changes, then a full refresh is initiated. The reason is not only untracked files tracking, but also handling
* committing outside IDEA, etc.
* </p>
* <p>
* Synchronization policy used in this class:<br/>
* myDefinitelyUntrackedFiles is accessed under the myDefinitelyUntrackedFiles lock.<br/>
* myPossiblyUntrackedFiles and myReady is accessed under the LOCK lock.<br/>
* This is done so, because the latter two variables are accessed from the AWT in after() and we don't want to lock the AWT long,
* while myDefinitelyUntrackedFiles is modified along with native request to Git.
* </p>
*
* @author Kirill Likhodedov
*/
public class GitUntrackedFilesHolder implements Disposable, BulkFileListener {
private static final Logger LOG = Logger.getInstance(GitUntrackedFilesHolder.class);
private final Project myProject;
private final VirtualFile myRoot;
private final ChangeListManager myChangeListManager;
private final VcsDirtyScopeManager myDirtyScopeManager;
private final GitRepositoryFiles myRepositoryFiles;
private final Git myGit;
private final Set<VirtualFile> myDefinitelyUntrackedFiles = new HashSet<VirtualFile>();
private final Set<VirtualFile> myPossiblyUntrackedFiles = new HashSet<VirtualFile>();
private boolean myReady; // if false, total refresh is needed
private final Object LOCK = new Object();
private final GitRepositoryManager myRepositoryManager;
GitUntrackedFilesHolder(@NotNull GitRepository repository) {
myProject = repository.getProject();
myRoot = repository.getRoot();
myChangeListManager = ChangeListManager.getInstance(myProject);
myDirtyScopeManager = VcsDirtyScopeManager.getInstance(myProject);
myGit = ServiceManager.getService(Git.class);
myRepositoryManager = GitUtil.getRepositoryManager(myProject);
myRepositoryFiles = GitRepositoryFiles.getInstance(repository.getGitDir());
}
void setupVfsListener(@NotNull Project project) {
if (!project.isDisposed()) {
MessageBusConnection connection = project.getMessageBus().connect(this);
connection.subscribe(VirtualFileManager.VFS_CHANGES, this);
}
}
@Override
public void dispose() {
synchronized (myDefinitelyUntrackedFiles) {
myDefinitelyUntrackedFiles.clear();
}
synchronized (LOCK) {
myPossiblyUntrackedFiles.clear();
}
}
/**
* Adds the file to the list of untracked.
*/
public void add(@NotNull VirtualFile file) {
synchronized (myDefinitelyUntrackedFiles) {
myDefinitelyUntrackedFiles.add(file);
}
}
/**
* Adds several files to the list of untracked.
*/
public void add(@NotNull Collection<VirtualFile> files) {
synchronized (myDefinitelyUntrackedFiles) {
myDefinitelyUntrackedFiles.addAll(files);
}
}
/**
* Removes several files from untracked.
*/
public void remove(@NotNull Collection<VirtualFile> files) {
synchronized (myDefinitelyUntrackedFiles) {
myDefinitelyUntrackedFiles.removeAll(files);
}
}
/**
* Returns the list of unversioned files.
* This method may be slow, if the full-refresh of untracked files is needed.
* @return untracked files.
* @throws VcsException if there is an unexpected error during Git execution.
*/
@NotNull
public Collection<VirtualFile> retrieveUntrackedFiles() throws VcsException {
if (isReady()) {
verifyPossiblyUntrackedFiles();
} else {
rescanAll();
}
synchronized (myDefinitelyUntrackedFiles) {
return new ArrayList<VirtualFile>(myDefinitelyUntrackedFiles);
}
}
public void invalidate() {
synchronized (LOCK) {
myReady = false;
}
}
/**
* Resets the list of untracked files after retrieving the full list of them from Git.
*/
public void rescanAll() throws VcsException {
Set<VirtualFile> untrackedFiles = myGit.untrackedFiles(myProject, myRoot, null);
synchronized (myDefinitelyUntrackedFiles) {
myDefinitelyUntrackedFiles.clear();
myDefinitelyUntrackedFiles.addAll(untrackedFiles);
}
synchronized (LOCK) {
myPossiblyUntrackedFiles.clear();
myReady = true;
}
}
/**
* @return <code>true</code> if untracked files list is initialized and being kept up-to-date, <code>false</code> if full refresh is needed.
*/
private boolean isReady() {
synchronized (LOCK) {
return myReady;
}
}
/**
* Queries Git to check the status of {@code myPossiblyUntrackedFiles} and moves them to {@code myDefinitelyUntrackedFiles}.
*/
private void verifyPossiblyUntrackedFiles() throws VcsException {
Set<VirtualFile> suspiciousFiles = new HashSet<VirtualFile>();
synchronized (LOCK) {
suspiciousFiles.addAll(myPossiblyUntrackedFiles);
myPossiblyUntrackedFiles.clear();
}
synchronized (myDefinitelyUntrackedFiles) {
Set<VirtualFile> untrackedFiles = myGit.untrackedFiles(myProject, myRoot, suspiciousFiles);
suspiciousFiles.removeAll(untrackedFiles);
// files that were suspicious (and thus passed to 'git ls-files'), but are not untracked, are definitely tracked.
@SuppressWarnings("UnnecessaryLocalVariable")
Set<VirtualFile> trackedFiles = suspiciousFiles;
myDefinitelyUntrackedFiles.addAll(untrackedFiles);
myDefinitelyUntrackedFiles.removeAll(trackedFiles);
}
}
@Override
public void before(@NotNull List<? extends VFileEvent> events) {
}
@Override
public void after(@NotNull List<? extends VFileEvent> events) {
boolean allChanged = false;
Set<VirtualFile> filesToRefresh = new HashSet<VirtualFile>();
for (VFileEvent event : events) {
if (allChanged) {
break;
}
String path = event.getPath();
if (totalRefreshNeeded(path)) {
allChanged = true;
}
else {
VirtualFile affectedFile = getAffectedFile(event);
if (notIgnored(affectedFile)) {
filesToRefresh.add(affectedFile);
}
}
}
// if index has changed, no need to refresh specific files - we get the full status of all files
if (allChanged) {
LOG.debug(String.format("GitUntrackedFilesHolder: Index has changed, marking %s recursively dirty", myRoot));
myDirtyScopeManager.dirDirtyRecursively(myRoot);
synchronized (LOCK) {
myReady = false;
}
} else {
synchronized (LOCK) {
myPossiblyUntrackedFiles.addAll(filesToRefresh);
}
}
}
private boolean totalRefreshNeeded(@NotNull String path) {
return indexChanged(path) || externallyCommitted(path) || gitignoreChanged(path);
}
private boolean indexChanged(@NotNull String path) {
return myRepositoryFiles.isIndexFile(path);
}
private boolean externallyCommitted(@NotNull String path) {
return myRepositoryFiles.isCommitMessageFile(path);
}
private boolean gitignoreChanged(@NotNull String path) {
// TODO watch file stored in core.excludesfile
return path.endsWith(".gitignore") || myRepositoryFiles.isExclude(path);
}
@Nullable
private static VirtualFile getAffectedFile(@NotNull VFileEvent event) {
if (event instanceof VFileCreateEvent || event instanceof VFileDeleteEvent || event instanceof VFileMoveEvent) {
return event.getFile();
} else if (event instanceof VFileCopyEvent) {
VFileCopyEvent copyEvent = (VFileCopyEvent) event;
return copyEvent.getNewParent().findChild(copyEvent.getNewChildName());
}
return null;
}
private boolean notIgnored(@Nullable VirtualFile file) {
return file != null && belongsToThisRepository(file) && !myChangeListManager.isIgnoredFile(file);
}
private boolean belongsToThisRepository(VirtualFile file) {
final GitRepository repository = myRepositoryManager.getRepositoryForFile(file);
return repository != null && repository.getRoot().equals(myRoot);
}
}