From 79fddb16900ced1ffb55b6c59f70633666c7c920 Mon Sep 17 00:00:00 2001 From: Constantine Plotnikov Date: Tue, 27 Jul 2010 21:20:00 +0400 Subject: [PATCH] git4idea: Added reference tracker --- plugins/git4idea/src/git4idea/GitVcs.java | 28 ++ .../src/git4idea/vfs/GitIgnoreTracker.java | 5 +- .../git4idea/vfs/GitReferenceListener.java | 32 ++ .../src/git4idea/vfs/GitReferenceTracker.java | 280 ++++++++++++++++++ 4 files changed, 344 insertions(+), 1 deletion(-) create mode 100644 plugins/git4idea/src/git4idea/vfs/GitReferenceListener.java create mode 100644 plugins/git4idea/src/git4idea/vfs/GitReferenceTracker.java diff --git a/plugins/git4idea/src/git4idea/GitVcs.java b/plugins/git4idea/src/git4idea/GitVcs.java index c32aac887549..b286f4240811 100644 --- a/plugins/git4idea/src/git4idea/GitVcs.java +++ b/plugins/git4idea/src/git4idea/GitVcs.java @@ -178,6 +178,10 @@ public class GitVcs extends AbstractVcs { * The dispatcher object for git configuration events */ private final EventDispatcher myConfigListeners = EventDispatcher.create(GitConfigListener.class); + /** + * The dispatcher object for git configuration events + */ + private final EventDispatcher myReferenceListeners = EventDispatcher.create(GitReferenceListener.class); /** * Tracker for ignored files */ @@ -198,6 +202,10 @@ public class GitVcs extends AbstractVcs { private final TreeDiffProvider myTreeDiffProvider; private final GitCommitAndPushExecutor myCommitAndPushExecutor; + /** + * The reference tracker + */ + private GitReferenceTracker myReferenceTracker; public static GitVcs getInstance(@NotNull Project project) { return (GitVcs)ProjectLevelVcsManager.getInstance(project).findVcsByName(NAME); @@ -230,6 +238,7 @@ public class GitVcs extends AbstractVcs { myOutgoingChangesProvider = new GitOutgoingChangesProvider(myProject); myTreeDiffProvider = new GitTreeDiffProvider(myProject); myCommitAndPushExecutor = new GitCommitAndPushExecutor(gitCheckinEnvironment); + myReferenceTracker = new GitReferenceTracker(myProject, this, myReferenceListeners.getMulticaster()); myTaskQueue = new BackgroundTaskQueue(myProject, GitBundle.getString("task.queue.title")); } @@ -274,6 +283,23 @@ public class GitVcs extends AbstractVcs { myConfigListeners.removeListener(listener); } + /** + * Add listener for git roots + * + * @param listener the listener to add + */ + public void addGitReferenceListener(GitReferenceListener listener) { + myReferenceListeners.addListener(listener); + } + + /** + * Remove listener for git roots + * + * @param listener the listener to remove + */ + public void removeGitReferenceListener(GitReferenceListener listener) { + myReferenceListeners.removeListener(listener); + } /** * Add listener for git roots @@ -481,6 +507,7 @@ public class GitVcs extends AbstractVcs { if (myGitIgnoreTracker == null) { myGitIgnoreTracker = new GitIgnoreTracker(myProject, this); } + myReferenceTracker.activate(); GitUsersComponent.getInstance(myProject).activate(); GitProjectLogManager.getInstance(myProject).activate(); } @@ -506,6 +533,7 @@ public class GitVcs extends AbstractVcs { myConfigTracker.dispose(); myConfigTracker = null; } + myReferenceTracker.deactivate(); GitUsersComponent.getInstance(myProject).deactivate(); GitProjectLogManager.getInstance(myProject).deactivate(); } diff --git a/plugins/git4idea/src/git4idea/vfs/GitIgnoreTracker.java b/plugins/git4idea/src/git4idea/vfs/GitIgnoreTracker.java index 4bb46b4ed0e4..5d9d0708dad9 100644 --- a/plugins/git4idea/src/git4idea/vfs/GitIgnoreTracker.java +++ b/plugins/git4idea/src/git4idea/vfs/GitIgnoreTracker.java @@ -233,7 +233,10 @@ public class GitIgnoreTracker { private static VirtualFile scanParents(VirtualFile root) { VirtualFile meta = root.findChild(GIT_FOLDER); if (meta != null) { - root.findFileByRelativePath(LOCAL_EXCLUDE); + final VirtualFile localExclude = root.findFileByRelativePath(LOCAL_EXCLUDE); + if (localExclude != null) { + localExclude.getTimeStamp(); + } return root; } else { diff --git a/plugins/git4idea/src/git4idea/vfs/GitReferenceListener.java b/plugins/git4idea/src/git4idea/vfs/GitReferenceListener.java new file mode 100644 index 000000000000..13d011d54294 --- /dev/null +++ b/plugins/git4idea/src/git4idea/vfs/GitReferenceListener.java @@ -0,0 +1,32 @@ +/* + * Copyright 2000-2010 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.vfs; + +import com.intellij.openapi.vfs.VirtualFile; + +import java.util.EventListener; + +/** + * The listener for reference changes + */ +public interface GitReferenceListener extends EventListener { + /** + * This method is invoked when some references changed. The events do not describe which ones. + * + * @param root the root for which references might have changed, null if all roots might have been affected + */ + void referencesChanged(VirtualFile root); +} diff --git a/plugins/git4idea/src/git4idea/vfs/GitReferenceTracker.java b/plugins/git4idea/src/git4idea/vfs/GitReferenceTracker.java new file mode 100644 index 000000000000..44d726b04ccf --- /dev/null +++ b/plugins/git4idea/src/git4idea/vfs/GitReferenceTracker.java @@ -0,0 +1,280 @@ +/* + * Copyright 2000-2010 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.vfs; + +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vcs.VcsException; +import com.intellij.openapi.vfs.*; +import com.intellij.util.containers.HashSet; +import git4idea.GitUtil; +import git4idea.GitVcs; + +import java.util.*; +import java.util.concurrent.atomic.AtomicReference; + +/** + * The tracker for the git references + */ +public class GitReferenceTracker { + /** + * The root references that must be tracked + */ + private static final Set myRootReferences = new HashSet(); + + static { + myRootReferences.add("HEAD"); + myRootReferences.add("refs"); + myRootReferences.add("MERGE_HEAD"); + // note, they are here intentionally, since their presence changes the way HEAD is interpreted + myRootReferences.add("rebase_merge"); + myRootReferences.add("rebase_apply"); + } + + /** + * The context project + */ + private final Project myProject; + /** + * The vcs instance that requested the tracking + */ + private final GitVcs myVcs; + /** + * The event multicaster + */ + private final GitReferenceListener myListener; + /** + * The listener for vcs roots + */ + private final GitRootsListener myRootsListener; + /** + * The list of git roots + */ + private final AtomicReference> myGitRoots = + new AtomicReference>(Collections.emptyList()); + /** + * The list of git roots + */ + private final AtomicReference myState = new AtomicReference(State.DEACTIVATED); + /** + * File system listener + */ + private final MyVfsListener myVfsListener; + + /** + * The reference tracker + * + * @param project the project + * @param vcs the vcs that created tracker + * @param listener the listener to use for notifications (multicaster is expected) + */ + public GitReferenceTracker(Project project, GitVcs vcs, GitReferenceListener listener) { + myProject = project; + myVcs = vcs; + myListener = listener; + myVfsListener = new MyVfsListener(); + myRootsListener = new GitRootsListener() { + @Override + public void gitRootsChanged() { + checkRoots(); + } + }; + } + + /** + * Start listening for events + */ + public void activate() { + if (!myState.compareAndSet(State.DEACTIVATED, State.ACTIVATED)) { + return; + } + if (myProject.isDefault()) { + return; + } + checkRoots(); + LocalFileSystem.getInstance().addVirtualFileListener(myVfsListener); + myVcs.addGitRootsListener(myRootsListener); + } + + /** + * Deactivate service + */ + public void deactivate() { + if (!myState.compareAndSet(State.ACTIVATED, State.DEACTIVATED)) { + return; + } + removeListeners(); + } + + /** + * Remove listeners from services + */ + private void removeListeners() { + myVcs.removeGitRootsListener(myRootsListener); + LocalFileSystem.getInstance().removeVirtualFileListener(myVfsListener); + } + + /** + * Dispose the tracker removing all listeners + */ + public void dispose() { + if (myState.getAndSet(State.DISPOSED) != State.ACTIVATED) { + return; + } + removeListeners(); + } + + /** + * Visit roots so file listener will be modified if special status changed + */ + private void checkRoots() { + try { + List roots = GitUtil.getGitRoots(myProject, myVcs); + myGitRoots.set(roots); + for (VirtualFile root : roots) { + final VirtualFile git = root.findChild(".git"); + if (git != null) { + for (String name : myRootReferences) { + final VirtualFile child = git.findChild(name); + if (child != null) { + child.getTimeStamp(); + } + } + final VirtualFile infoRefs = root.findFileByRelativePath("info/refs"); + if (infoRefs != null) { + infoRefs.getTimeStamp(); + } + visitRecursively(git.findChild("refs")); + } + } + notifyChanges(null); + } + catch (VcsException e) { + myGitRoots.set(Collections.emptyList()); + } + } + + private void notifyChanges(VirtualFile root) { + myListener.referencesChanged(root); + } + + /** + * Visit recursively + * + * @param child the child directory to visit + */ + private static void visitRecursively(VirtualFile child) { + if (child == null) { + return; + } + child.getTimeStamp(); + LinkedList toVisit = new LinkedList(); + toVisit.add(child); + while (!toVisit.isEmpty()) { + VirtualFile file = toVisit.removeLast(); + if (file.isValid() && file.isDirectory()) { + toVisit.addAll(Arrays.asList(file.getChildren())); + } + } + } + + /** + * The listener for vfs events + */ + class MyVfsListener extends VirtualFileAdapter { + + @Override + public void contentsChanged(VirtualFileEvent event) { + checkFile(event.getParent(), event.getFileName()); + } + + @Override + public void fileCreated(VirtualFileEvent event) { + checkFile(event.getParent(), event.getFileName()); + } + + @Override + public void fileDeleted(VirtualFileEvent event) { + checkFile(event.getParent(), event.getFileName()); + } + + @Override + public void fileMoved(VirtualFileMoveEvent event) { + checkFile(event.getNewParent(), event.getFileName()); + checkFile(event.getOldParent(), event.getFileName()); + } + + @Override + public void fileCopied(VirtualFileCopyEvent event) { + checkFile(event.getParent(), event.getFileName()); + } + + @Override + public void propertyChanged(VirtualFilePropertyEvent event) { + if (event.getPropertyName() == VirtualFile.PROP_NAME) { + checkFile(event.getParent(), (String)event.getOldValue()); + checkFile(event.getParent(), (String)event.getNewValue()); + } + } + + /** + * Check if the file change might affect reference mapping + * + * @param parent the file parent to check + * @param name the name check + */ + private void checkFile(VirtualFile parent, String name) { + VirtualFile git = parent; + while (git != null && !git.getName().equals(".git")) { + git = git.getParent(); + } + if (git == null) { + return; + } + if (parent == git && myRootReferences.contains(name)) { + notifyChanges(git.getParent()); + } + else if (parent.getParent() == git && name.equals("refs") && parent.getName().equals("info")) { + notifyChanges(git.getParent()); + } + else { + while (parent != null && parent.getParent() != git) { + parent = parent.getParent(); + } + if (parent != null && parent.isDirectory() && parent.getName().equals("refs")) { + notifyChanges(git.getParent()); + } + } + } + } + + /** + * The component state + */ + private enum State { + /** + * Initial state + */ + DEACTIVATED, + /** + * Started state + */ + ACTIVATED, + /** + * Disposed + */ + DISPOSED + } +}