ff3d4a7ed80d9ab8a959c83900f5d0afb57af2cc
[idea/community.git] / plugins / git4idea / src / git4idea / history / GitUsersComponent.java
1 /*
2  * Copyright 2000-2010 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package git4idea.history;
17
18 import com.intellij.lifecycle.AtomicSectionsAware;
19 import com.intellij.openapi.application.PathManager;
20 import com.intellij.openapi.components.ServiceManager;
21 import com.intellij.openapi.diagnostic.Logger;
22 import com.intellij.openapi.project.Project;
23 import com.intellij.openapi.util.Pair;
24 import com.intellij.openapi.util.Ref;
25 import com.intellij.openapi.util.UpdatedReference;
26 import com.intellij.openapi.util.text.StringUtil;
27 import com.intellij.openapi.vcs.ProjectLevelVcsManager;
28 import com.intellij.openapi.vcs.VcsException;
29 import com.intellij.openapi.vcs.VcsListener;
30 import com.intellij.openapi.vcs.changes.ControlledCycle;
31 import com.intellij.openapi.vfs.VirtualFile;
32 import com.intellij.util.Consumer;
33 import com.intellij.util.io.DataExternalizer;
34 import com.intellij.util.io.EnumeratorStringDescriptor;
35 import com.intellij.util.io.PersistentHashMap;
36 import com.intellij.util.io.storage.HeavyProcessLatch;
37 import git4idea.GitVcs;
38 import git4idea.history.browser.ChangesFilter;
39 import git4idea.history.browser.GitCommit;
40 import git4idea.history.browser.LowLevelAccess;
41 import git4idea.history.browser.LowLevelAccessImpl;
42 import org.jetbrains.annotations.Nullable;
43
44 import java.io.DataInput;
45 import java.io.DataOutput;
46 import java.io.File;
47 import java.io.IOException;
48 import java.util.*;
49
50 public class GitUsersComponent {
51   private static final int ourPackSize = 50;
52   private static final int ourBackInterval = 60 * 1000;
53   private static final int ourForwardInterval = 100 * 60 * 1000;
54   
55   private static final Logger LOG = Logger.getInstance("#git4idea.history.GitUsersComponent");
56
57   private final Object myLock;
58   private PersistentHashMap<String, UsersData> myState;
59   private final Map<VirtualFile, Pair<String, LowLevelAccess>> myAccessMap;
60
61   // activate-deactivate
62   private volatile boolean myIsActive;
63   private final ControlledCycle myControlledCycle;
64   private final VcsListener myVcsListener;
65   private final GitVcs myVcs;
66   private final ProjectLevelVcsManager myManager;
67   private final File myFile;
68
69   public GitUsersComponent(final ProjectLevelVcsManager manager) {
70     myVcs = (GitVcs) manager.findVcsByName(GitVcs.getKey().getName());
71     myManager = manager;
72     myLock = new Object();
73
74     final File vcsFile = new File(PathManager.getSystemPath(), "vcs");
75     File file = new File(vcsFile, "git_users");
76     file.mkdirs();
77     myFile = new File(file, myVcs.getProject().getLocationHash());
78
79     myAccessMap = new HashMap<VirtualFile, Pair<String, LowLevelAccess>>();
80
81     // every 10 seconds is ok to check
82     myControlledCycle = new ControlledCycle(myVcs.getProject(), new MyRefresher(), "Git users loader");
83     myVcsListener = new VcsListener() {
84       public void directoryMappingChanged() {
85         final VirtualFile[] currentRoots = myManager.getRootsUnderVcs(myVcs);
86         synchronized (myLock) {
87           myAccessMap.clear();
88           for (VirtualFile currentRoot : currentRoots) {
89             myAccessMap.put(currentRoot, new Pair<String, LowLevelAccess>(currentRoot.getPath(),
90                                                                           new LowLevelAccessImpl(myVcs.getProject(), currentRoot)));
91           }
92         }
93       }
94     };
95   }
96
97   public static GitUsersComponent getInstance(final Project project) {
98     return ServiceManager.getService(project, GitUsersComponent.class);
99   }
100
101   @Nullable
102   public List<String> getUsersList(final VirtualFile root) {
103     final Pair<String, LowLevelAccess> pair = myAccessMap.get(root);
104     if (pair == null) return null;
105     try {
106       return new ArrayList<String>(myState.get(pair.getFirst()).getUsers());
107     }
108     catch (IOException e) {
109       LOG.info(e);
110       return null;
111     }
112   }
113
114   // singleton update process, only roots can change outside
115   private class MyRefresher implements ControlledCycle.MyCallback {
116     public boolean call(final AtomicSectionsAware atomicSectionsAware) {
117       atomicSectionsAware.checkShouldExit();
118       if (myIsActive) {
119         try {
120           final HashMap<VirtualFile, Pair<String, LowLevelAccess>> copy;
121           synchronized (myLock) {
122             copy = new HashMap<VirtualFile, Pair<String, LowLevelAccess>>(myAccessMap);
123           }
124
125           final Map<String, UsersData> toUpdate = new HashMap<String, UsersData>();
126           for (Pair<String, LowLevelAccess> pair : copy.values()) {
127             atomicSectionsAware.checkShouldExit();
128
129             final String key = pair.getFirst();
130             UsersData data = myState.get(key);
131             if (data == null) {
132               data = new UsersData();
133               data.forceUpdate();
134             }
135             if (data.load(pair.getSecond(), atomicSectionsAware)) {
136               toUpdate.put(key, data);
137             }
138           }
139
140           for (String s : toUpdate.keySet()) {
141             myState.put(s, toUpdate.get(s));
142           }
143           if (! toUpdate.isEmpty()) {
144             myState.force();
145           }
146         }
147         catch (IOException e) {
148           LOG.info(e);
149         }
150       }
151       return myIsActive;
152     }
153   }
154
155   public void activate() {
156     try {
157       myState = new PersistentHashMap<String, UsersData>(myFile, new EnumeratorStringDescriptor(), createExternalizer());
158     }
159     catch (IOException e) {
160       myState = null;
161     }
162
163     myIsActive = true;
164     myManager.addVcsListener(myVcsListener);
165     myControlledCycle.start();
166   }
167
168   public void deactivate() {
169     if (myState != null) try {
170       myState.close();
171     }
172     catch (IOException e) {
173       LOG.info(e);
174     }
175     myState = null;
176
177     synchronized (myLock) {
178       myAccessMap.clear();
179     }
180     myManager.removeVcsListener(myVcsListener);
181     myIsActive = false;
182   }
183
184   private static DataExternalizer<UsersData> createExternalizer() {
185     return new UsersDataExternalizer();
186   }
187
188   private static class UsersData {
189     private UpdatedReference<Long> myCloserDate;
190     private UpdatedReference<Long> myEarlierDate;
191     private final List<String> myUsers;
192     private boolean myForceUpdate;
193     private boolean myStartReached;
194
195     private UsersData() {
196       myUsers = new LinkedList<String>();
197       final long now = System.currentTimeMillis();
198       myCloserDate = new UpdatedReference<Long>(now);
199       myEarlierDate = new UpdatedReference<Long>(now + 1);
200     }
201
202     public UpdatedReference<Long> getCloserDate() {
203       return myCloserDate;
204     }
205
206     public void setCloserDate(UpdatedReference<Long> closerDate) {
207       myCloserDate = closerDate;
208     }
209
210     public UpdatedReference<Long> getEarlierDate() {
211       return myEarlierDate;
212     }
213
214     public void setEarlierDate(UpdatedReference<Long> earlierDate) {
215       myEarlierDate = earlierDate;
216     }
217
218     public void forceUpdate() {
219       myForceUpdate = true;
220     }
221
222     public List<String> getUsers() {
223       return myUsers;
224     }
225
226     public boolean isStartReached() {
227       return myStartReached;
228     }
229
230     public void setStartReached(boolean startReached) {
231       myStartReached = startReached;
232     }
233
234     public void addUsers(final Collection<String> users) {
235       myUsers.addAll(users);
236     }
237
238     public boolean load(final LowLevelAccess lowLevelAccess, final AtomicSectionsAware atomicSectionsAware) {
239       if (HeavyProcessLatch.INSTANCE.isRunning()) return false;
240
241       HeavyProcessLatch.INSTANCE.processStarted();
242       try {
243         final Set<String> newData = new HashSet<String>();
244         boolean result = false;
245         if (!myStartReached && (myForceUpdate || myEarlierDate.isTimeToUpdate(ourBackInterval))) {
246           result = true;
247           lookBack(lowLevelAccess, newData, atomicSectionsAware);
248         }
249         if (myForceUpdate || myCloserDate.isTimeToUpdate(ourForwardInterval)) {
250           result = true;
251           lookForward(lowLevelAccess, newData, atomicSectionsAware);
252         }
253         myForceUpdate = false;
254         
255         result |= ! newData.isEmpty();
256         putNewData(newData);
257         return result;
258       } finally {
259         HeavyProcessLatch.INSTANCE.processFinished();
260       }
261     }
262
263     private void putNewData(Set<String> newData) {
264       newData.addAll(myUsers);
265       myUsers.clear();
266       myUsers.addAll(newData);
267       Collections.sort(myUsers);
268     }
269
270     private void lookForward(LowLevelAccess lowLevelAccess, Set<String> newData, AtomicSectionsAware atomicSectionsAware) {
271       myCloserDate.updateTs();
272       loadImpl(lowLevelAccess, newData, null, new Date(myCloserDate.getT()), atomicSectionsAware);
273     }
274
275     private void lookBack(LowLevelAccess lowLevelAccess, Set<String> newData, AtomicSectionsAware atomicSectionsAware) {
276       myEarlierDate.updateTs();
277       loadImpl(lowLevelAccess, newData, new Date(myEarlierDate.getT()), null, atomicSectionsAware);
278     }
279
280     private void loadImpl(LowLevelAccess lowLevelAccess,
281                           final Set<String> newData,
282                           @Nullable final Date before,
283                           @Nullable final Date after,
284                           final AtomicSectionsAware atomicSectionsAware) {
285       try {
286         final Ref<Long> beforeTick = new Ref<Long>(Long.MAX_VALUE); // min
287         final Ref<Long> afterTick = new Ref<Long>(-1L);  // max
288         lowLevelAccess.loadCommits(Collections.<String>emptyList(), before, after, Collections.<ChangesFilter.Filter>emptyList(),
289                                    new Consumer<GitCommit>() {
290                                      public void consume(GitCommit gitCommit) {
291                                        atomicSectionsAware.checkShouldExit();
292
293                                        final long time = gitCommit.getDate().getTime();
294                                        beforeTick.set(Math.min(beforeTick.get(), time));
295                                        afterTick.set(Math.max(afterTick.get(), time));
296
297                                        if (! StringUtil.isEmptyOrSpaces(gitCommit.getAuthor())) {
298                                         newData.add(gitCommit.getAuthor());
299                                        }
300                                        if (! StringUtil.isEmptyOrSpaces(gitCommit.getCommitter())) {
301                                         newData.add(gitCommit.getCommitter());
302                                        }
303                                        if (gitCommit.getParentsHashes().isEmpty()) {
304                                          myStartReached = true;
305                                        }
306                                      }
307                                    }, ourPackSize, Collections.<String>emptyList());
308         if (myCloserDate.getT() < afterTick.get()) {
309           myCloserDate.updateT(afterTick.get());
310         }
311         if (myEarlierDate.getT() > beforeTick.get()) {
312           myEarlierDate.updateT(beforeTick.get());
313         }
314       }
315       catch (VcsException e) {
316         LOG.info(e);
317       }
318     }
319   }
320
321   private static class UsersDataExternalizer implements DataExternalizer<UsersData> {
322     public void save(DataOutput out, UsersData value) throws IOException {
323       final UpdatedReference<Long> closer = value.getCloserDate();
324       out.writeLong(closer.getT());
325       out.writeLong(closer.getTime());
326
327       final UpdatedReference<Long> earlier = value.getEarlierDate();
328       out.writeLong(earlier.getT());
329       out.writeLong(earlier.getTime());
330
331       final List<String> users = value.getUsers();
332       out.writeInt(users.size());
333       for (String user : users) {
334         out.writeUTF(user);
335       }
336
337       out.writeBoolean(value.isStartReached());
338     }
339
340     public UsersData read(DataInput in) throws IOException {
341       final UsersData data = new UsersData();
342       final long closerDate = in.readLong();
343       final long closerUpdate = in.readLong();
344       data.setCloserDate(new UpdatedReference<Long>(closerDate, closerUpdate));
345       final long earlierDate = in.readLong();
346       final long earlierUpdate = in.readLong();
347       data.setEarlierDate(new UpdatedReference<Long>(earlierDate, earlierUpdate));
348
349       final List<String> users = new LinkedList<String>();
350       final int size = in.readInt();
351       for (int i = 0; i < size; i++) {
352         users.add(in.readUTF());
353       }
354       data.addUsers(users);
355       data.setStartReached(in.readBoolean());
356       return data;
357     }
358   }
359 }