also detect modifications to .git/index and .git/HEAD to unblock path
[TortoiseGit.git] / src / TGitCache / GITStatusCache.cpp
blob32aa985378ba0e1b1471cbeff83421a69de09e78
1 // TortoiseGit - a Windows shell extension for easy version control
3 // External Cache Copyright (C) 2005-2006,2008,2010 - TortoiseSVN
4 // Copyright (C) 2008-2011 - TortoiseGit
6 // This program is free software; you can redistribute it and/or
7 // modify it under the terms of the GNU General Public License
8 // as published by the Free Software Foundation; either version 2
9 // of the License, or (at your option) any later version.
11 // This program is distributed in the hope that it will be useful,
12 // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 // GNU General Public License for more details.
16 // You should have received a copy of the GNU General Public License
17 // along with this program; if not, write to the Free Software Foundation,
18 // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
21 #include "StdAfx.h"
22 #include "GitStatus.h"
23 #include "GitStatuscache.h"
24 #include "CacheInterface.h"
25 #include "shlobj.h"
27 //////////////////////////////////////////////////////////////////////////
29 #define BLOCK_PATH_DEFAULT_TIMEOUT 600 // 10 minutes
30 #define BLOCK_PATH_MAX_TIMEOUT 1200 // 20 minutes
32 CGitStatusCache* CGitStatusCache::m_pInstance;
34 CGitStatusCache& CGitStatusCache::Instance()
36 ATLASSERT(m_pInstance != NULL);
37 return *m_pInstance;
40 void CGitStatusCache::Create()
42 ATLASSERT(m_pInstance == NULL);
43 m_pInstance = new CGitStatusCache;
45 m_pInstance->watcher.SetFolderCrawler(&m_pInstance->m_folderCrawler);
46 #define LOADVALUEFROMFILE(x) if (fread(&x, sizeof(x), 1, pFile)!=1) goto exit;
47 #define LOADVALUEFROMFILE2(x) if (fread(&x, sizeof(x), 1, pFile)!=1) goto error;
48 unsigned int value = (unsigned int)-1;
49 FILE * pFile = NULL;
50 // find the location of the cache
51 TCHAR path[MAX_PATH]; //MAX_PATH ok here.
52 TCHAR path2[MAX_PATH];
53 if (SHGetFolderPath(NULL, CSIDL_LOCAL_APPDATA, NULL, SHGFP_TYPE_CURRENT, path)==S_OK)
55 _tcscat_s(path, MAX_PATH, _T("\\TGitCache"));
56 if (!PathIsDirectory(path))
58 if (CreateDirectory(path, NULL)==0)
59 goto error;
61 _tcscat_s(path, MAX_PATH, _T("\\cache"));
62 // in case the cache file is corrupt, we could crash while
63 // reading it! To prevent crashing every time once that happens,
64 // we make a copy of the cache file and use that copy to read from.
65 // if that copy is corrupt, the original file won't exist anymore
66 // and the second time we start up and try to read the file,
67 // it's not there anymore and we start from scratch without a crash.
68 _tcscpy_s(path2, MAX_PATH, path);
69 _tcscat_s(path2, MAX_PATH, _T("2"));
70 DeleteFile(path2);
71 CopyFile(path, path2, FALSE);
72 DeleteFile(path);
73 pFile = _tfsopen(path2, _T("rb"), _SH_DENYNO);
74 if (pFile)
76 try
78 LOADVALUEFROMFILE(value);
79 if (value != 2)
81 goto error;
83 int mapsize = 0;
84 LOADVALUEFROMFILE(mapsize);
85 for (int i=0; i<mapsize; ++i)
87 LOADVALUEFROMFILE2(value);
88 if (value > MAX_PATH)
89 goto error;
90 if (value)
92 CString sKey;
93 if (fread(sKey.GetBuffer(value+1), sizeof(TCHAR), value, pFile)!=value)
95 sKey.ReleaseBuffer(0);
96 goto error;
98 sKey.ReleaseBuffer(value);
99 CCachedDirectory * cacheddir = new CCachedDirectory();
100 if (cacheddir == NULL)
101 goto error;
102 if (!cacheddir->LoadFromDisk(pFile))
103 goto error;
104 CTGitPath KeyPath = CTGitPath(sKey);
105 if (m_pInstance->IsPathAllowed(KeyPath))
107 m_pInstance->m_directoryCache[KeyPath] = cacheddir;
108 // only add the path to the watch list if it is versioned
109 if ((cacheddir->GetCurrentFullStatus() != git_wc_status_unversioned)&&(cacheddir->GetCurrentFullStatus() != git_wc_status_none))
110 m_pInstance->watcher.AddPath(KeyPath);
111 // do *not* add the paths for crawling!
112 // because crawled paths will trigger a shell
113 // notification, which makes the desktop flash constantly
114 // until the whole first time crawling is over
115 // m_pInstance->AddFolderForCrawling(KeyPath);
120 catch (CAtlException)
122 goto error;
126 exit:
127 if (pFile)
128 fclose(pFile);
129 DeleteFile(path2);
130 ATLTRACE("cache loaded from disk successfully!\n");
131 return;
132 error:
133 fclose(pFile);
134 DeleteFile(path2);
135 if (m_pInstance)
137 m_pInstance->Stop();
138 Sleep(100);
140 delete m_pInstance;
141 m_pInstance = new CGitStatusCache;
142 ATLTRACE("cache not loaded from disk\n");
145 bool CGitStatusCache::SaveCache()
147 #define WRITEVALUETOFILE(x) if (fwrite(&x, sizeof(x), 1, pFile)!=1) goto error;
148 unsigned int value = 0;
149 // save the cache to disk
150 FILE * pFile = NULL;
151 // find a location to write the cache to
152 TCHAR path[MAX_PATH]; //MAX_PATH ok here.
153 if (SHGetFolderPath(NULL, CSIDL_LOCAL_APPDATA, NULL, SHGFP_TYPE_CURRENT, path)==S_OK)
155 _tcscat_s(path, MAX_PATH, _T("\\TGitCache"));
156 if (!PathIsDirectory(path))
157 CreateDirectory(path, NULL);
158 _tcscat_s(path, MAX_PATH, _T("\\cache"));
159 _tfopen_s(&pFile, path, _T("wb"));
160 if (pFile)
162 value = 2; // 'version'
163 WRITEVALUETOFILE(value);
164 value = (int)m_pInstance->m_directoryCache.size();
165 WRITEVALUETOFILE(value);
166 for (CCachedDirectory::CachedDirMap::iterator I = m_pInstance->m_directoryCache.begin(); I != m_pInstance->m_directoryCache.end(); ++I)
168 if (I->second == NULL)
170 value = 0;
171 WRITEVALUETOFILE(value);
172 continue;
174 const CString& key = I->first.GetWinPathString();
175 value = key.GetLength();
176 WRITEVALUETOFILE(value);
177 if (value)
179 if (fwrite((LPCTSTR)key, sizeof(TCHAR), value, pFile)!=value)
180 goto error;
181 if (!I->second->SaveToDisk(pFile))
182 goto error;
185 fclose(pFile);
188 ATLTRACE(_T("cache saved to disk at %s\n"), path);
189 return true;
190 error:
191 fclose(pFile);
192 if (m_pInstance)
194 m_pInstance->Stop();
195 Sleep(100);
197 delete m_pInstance;
198 m_pInstance = NULL;
199 DeleteFile(path);
200 return false;
203 void CGitStatusCache::Destroy()
205 if (m_pInstance)
207 m_pInstance->Stop();
208 Sleep(100);
210 delete m_pInstance;
211 m_pInstance = NULL;
214 void CGitStatusCache::Stop()
216 // m_svnHelp.Cancel(true);
217 watcher.Stop();
218 m_folderCrawler.Stop();
219 m_shellUpdater.Stop();
222 void CGitStatusCache::Init()
224 m_folderCrawler.Initialise();
225 m_shellUpdater.Initialise();
228 CGitStatusCache::CGitStatusCache(void)
230 #define forever DWORD(-1)
231 AutoLocker lock(m_NoWatchPathCritSec);
232 TCHAR path[MAX_PATH];
233 SHGetFolderPath(NULL, CSIDL_COOKIES, NULL, 0, path);
234 m_NoWatchPaths[CTGitPath(CString(path))] = forever;
235 SHGetFolderPath(NULL, CSIDL_HISTORY, NULL, 0, path);
236 m_NoWatchPaths[CTGitPath(CString(path))] = forever;
237 SHGetFolderPath(NULL, CSIDL_INTERNET_CACHE, NULL, 0, path);
238 m_NoWatchPaths[CTGitPath(CString(path))] = forever;
239 SHGetFolderPath(NULL, CSIDL_SYSTEM, NULL, 0, path);
240 m_NoWatchPaths[CTGitPath(CString(path))] = forever;
241 SHGetFolderPath(NULL, CSIDL_WINDOWS, NULL, 0, path);
242 m_NoWatchPaths[CTGitPath(CString(path))] = forever;
243 m_bClearMemory = false;
244 m_mostRecentExpiresAt = 0;
247 CGitStatusCache::~CGitStatusCache(void)
249 for (CCachedDirectory::CachedDirMap::iterator I = m_pInstance->m_directoryCache.begin(); I != m_pInstance->m_directoryCache.end(); ++I)
251 delete I->second;
252 I->second = NULL;
256 void CGitStatusCache::Refresh()
258 m_shellCache.ForceRefresh();
259 // m_pInstance->m_svnHelp.ReloadConfig();
260 if (m_pInstance->m_directoryCache.size())
262 CCachedDirectory::CachedDirMap::iterator I = m_pInstance->m_directoryCache.begin();
263 for (/* no init */; I != m_pInstance->m_directoryCache.end(); ++I)
265 if (m_shellCache.IsPathAllowed(I->first.GetWinPath()))
266 I->second->RefreshMostImportant();
267 else
269 CGitStatusCache::Instance().RemoveCacheForPath(I->first);
270 I = m_pInstance->m_directoryCache.begin();
271 if (I == m_pInstance->m_directoryCache.end())
272 break;
278 bool CGitStatusCache::IsPathGood(const CTGitPath& path)
280 AutoLocker lock(m_NoWatchPathCritSec);
281 for (std::map<CTGitPath, DWORD>::const_iterator it = m_NoWatchPaths.begin(); it != m_NoWatchPaths.end(); ++it)
283 // the ticks check is necessary here, because RemoveTimedoutBlocks is only called within the FolderCrawler loop
284 // and we might miss update calls
285 // TODO: maybe we also need to check if index.lock and HEAD.lock still exists after a specific timeout (if we miss update notifications for these files too often)
286 if (GetTickCount() < it->second && it->first.IsAncestorOf(path))
288 ATLTRACE(_T("path not good: %s\n"), it->first.GetWinPath());
289 return false;
292 return true;
295 bool CGitStatusCache::BlockPath(const CTGitPath& path, DWORD timeout /* = 0 */)
297 if (timeout == 0)
298 timeout = BLOCK_PATH_DEFAULT_TIMEOUT;
300 if (timeout > BLOCK_PATH_MAX_TIMEOUT)
301 timeout = BLOCK_PATH_MAX_TIMEOUT;
303 timeout = GetTickCount() + (timeout * 1000); // timeout is in seconds, but we need the milliseconds
305 AutoLocker lock(m_NoWatchPathCritSec);
306 m_NoWatchPaths[path.GetDirectory()] = timeout;
308 return true;
311 bool CGitStatusCache::UnBlockPath(const CTGitPath& path)
313 bool ret = false;
314 AutoLocker lock(m_NoWatchPathCritSec);
315 std::map<CTGitPath, DWORD>::iterator it = m_NoWatchPaths.find(path.GetDirectory());
316 if (it != m_NoWatchPaths.end())
318 ATLTRACE(_T("path removed from no good: %s\n"), it->first.GetWinPath());
319 m_NoWatchPaths.erase(it);
320 ret = true;
322 AddFolderForCrawling(path.GetDirectory());
324 return ret;
327 bool CGitStatusCache::RemoveTimedoutBlocks()
329 bool ret = false;
330 DWORD currentTicks = GetTickCount();
331 AutoLocker lock(m_NoWatchPathCritSec);
332 std::vector<CTGitPath> toRemove;
333 for (std::map<CTGitPath, DWORD>::const_iterator it = m_NoWatchPaths.begin(); it != m_NoWatchPaths.end(); ++it)
335 if (currentTicks > it->second)
337 toRemove.push_back(it->first);
340 if (toRemove.size())
342 for (std::vector<CTGitPath>::const_iterator it = toRemove.begin(); it != toRemove.end(); ++it)
344 ret = ret || UnBlockPath(*it);
348 return ret;
351 void CGitStatusCache::UpdateShell(const CTGitPath& path)
353 m_shellUpdater.AddPathForUpdate(path);
356 void CGitStatusCache::ClearCache()
358 for (CCachedDirectory::CachedDirMap::iterator I = m_directoryCache.begin(); I != m_directoryCache.end(); ++I)
360 delete I->second;
361 I->second = NULL;
363 m_directoryCache.clear();
366 bool CGitStatusCache::RemoveCacheForDirectory(CCachedDirectory * cdir)
368 if (cdir == NULL)
369 return false;
370 AssertWriting();
371 typedef std::map<CTGitPath, git_wc_status_kind> ChildDirStatus;
372 if (cdir->m_childDirectories.size())
374 ChildDirStatus::iterator it = cdir->m_childDirectories.begin();
375 for (; it != cdir->m_childDirectories.end(); )
377 CCachedDirectory * childdir = CGitStatusCache::Instance().GetDirectoryCacheEntryNoCreate(it->first);
378 if ((childdir)&&(!cdir->m_directoryPath.IsEquivalentTo(childdir->m_directoryPath)))
379 RemoveCacheForDirectory(childdir);
380 cdir->m_childDirectories.erase(it->first);
381 it = cdir->m_childDirectories.begin();
384 cdir->m_childDirectories.clear();
385 m_directoryCache.erase(cdir->m_directoryPath);
386 ATLTRACE(_T("removed path %s from cache\n"), cdir->m_directoryPath.GetWinPathString());
387 delete cdir;
388 cdir = NULL;
389 return true;
392 void CGitStatusCache::RemoveCacheForPath(const CTGitPath& path)
394 // Stop the crawler starting on a new folder
395 CCrawlInhibitor crawlInhibit(&m_folderCrawler);
396 CCachedDirectory::ItDir itMap;
397 CCachedDirectory * dirtoremove = NULL;
399 AssertWriting();
400 itMap = m_directoryCache.find(path);
401 if ((itMap != m_directoryCache.end())&&(itMap->second))
402 dirtoremove = itMap->second;
403 if (dirtoremove == NULL)
404 return;
405 ATLASSERT(path.IsEquivalentToWithoutCase(dirtoremove->m_directoryPath));
406 RemoveCacheForDirectory(dirtoremove);
409 CCachedDirectory * CGitStatusCache::GetDirectoryCacheEntry(const CTGitPath& path, bool isAddToWatch)
411 ATLASSERT(path.IsDirectory() || !PathFileExists(path.GetWinPath()));
414 CCachedDirectory::ItDir itMap;
415 itMap = m_directoryCache.find(path);
416 if ((itMap != m_directoryCache.end())&&(itMap->second))
418 // We've found this directory in the cache
419 return itMap->second;
421 else
423 // if the CCachedDirectory is NULL but the path is in our cache,
424 // that means that path got invalidated and needs to be treated
425 // as if it never was in our cache. So we remove the last remains
426 // from the cache and start from scratch.
427 AssertLock();
428 if (!IsWriter())
430 // upgrading our state to writer
431 ATLTRACE("trying to upgrade the state to \"Writer\"\n");
432 Done();
433 ATLTRACE("Returned \"Reader\" state\n");
434 WaitToWrite();
435 ATLTRACE("Got \"Writer\" state now\n");
437 // Since above there's a small chance that before we can upgrade to
438 // writer state some other thread gained writer state and changed
439 // the data, we have to recreate the iterator here again.
440 itMap = m_directoryCache.find(path);
441 if (itMap!=m_directoryCache.end())
442 m_directoryCache.erase(itMap);
443 // We don't know anything about this directory yet - lets add it to our cache
444 // but only if it exists!
445 if (path.Exists() && m_shellCache.IsPathAllowed(path.GetWinPath()) && !g_GitAdminDir.IsAdminDirPath(path.GetWinPath()))
447 // some notifications are for files which got removed/moved around.
448 // In such cases, the CTGitPath::IsDirectory() will return true (it assumes a directory if
449 // the path doesn't exist). Which means we can get here with a path to a file
450 // instead of a directory.
451 // Since we're here most likely called from the crawler thread, the file could exist
452 // again. If that's the case, just do nothing
453 if (path.IsDirectory()||(!path.Exists()))
455 ATLTRACE(_T("adding %s to our cache\n"), path.GetWinPath());
456 CCachedDirectory * newcdir = new CCachedDirectory(path);
457 if (newcdir)
459 CCachedDirectory * cdir = m_directoryCache.insert(m_directoryCache.lower_bound(path), std::make_pair(path, newcdir))->second;
460 CString gitdir;
461 if ((!path.IsEmpty())&&(path.HasAdminDir(&gitdir))&&isAddToWatch)
463 bool isVersion = true;
464 CString subpaths;
465 if(subpaths.GetLength() > gitdir.GetLength())
467 if(subpaths[gitdir.GetLength()] == _T('\\'))
468 subpaths=subpaths.Right(subpaths.GetLength() - gitdir.GetLength()-1);
469 else
470 subpaths=subpaths.Right(subpaths.GetLength() - gitdir.GetLength());
472 CGitStatusCache::Instance().m_GitStatus.IsUnderVersionControl(gitdir, subpaths, true, &isVersion);
474 /* Just watch version path */
475 if(isVersion)
477 watcher.AddPath(gitdir);
478 watcher.AddPath(path);
481 return cdir;
483 m_bClearMemory = true;
486 return NULL;
490 CCachedDirectory * CGitStatusCache::GetDirectoryCacheEntryNoCreate(const CTGitPath& path)
492 ATLASSERT(path.IsDirectory() || !PathFileExists(path.GetWinPath()));
494 CCachedDirectory::ItDir itMap;
495 itMap = m_directoryCache.find(path);
496 if(itMap != m_directoryCache.end())
498 // We've found this directory in the cache
499 return itMap->second;
501 return NULL;
504 /* Fetch is true, means fetch status from */
505 /* Fetch is false, means fetch status from cache */
506 CStatusCacheEntry CGitStatusCache::GetStatusForPath(const CTGitPath& path, DWORD flags, bool bFetch /* = true */)
508 bool bRecursive = !!(flags & TGITCACHE_FLAGS_RECUSIVE_STATUS);
510 // Check a very short-lived 'mini-cache' of the last thing we were asked for.
511 long now = (long)GetTickCount();
512 if(now-m_mostRecentExpiresAt < 0)
514 if(path.IsEquivalentToWithoutCase(m_mostRecentPath))
516 return m_mostRecentStatus;
519 m_mostRecentPath = path;
520 m_mostRecentExpiresAt = now+1000;
522 if (IsPathGood(path))
524 // Stop the crawler starting on a new folder while we're doing this much more important task...
525 // Please note, that this may be a second "lock" used concurrently to the one in RemoveCacheForPath().
526 CCrawlInhibitor crawlInhibit(&m_folderCrawler);
528 CTGitPath dirpath = path.GetContainingDirectory();
529 if ((dirpath.IsEmpty()) || (!m_shellCache.IsPathAllowed(dirpath.GetWinPath())))
530 dirpath = path.GetDirectory();
531 CCachedDirectory * cachedDir = GetDirectoryCacheEntry(dirpath);
532 if (cachedDir != NULL)
534 //ATLTRACE(_T("GetStatusForMember %d\n"), bFetch);
535 m_mostRecentStatus = cachedDir->GetStatusForMember(path, bRecursive, bFetch);
536 return m_mostRecentStatus;
539 else
541 // path is blocked for some reason: return the cached status if we have one
542 // we do here only a cache search, absolutely no disk access is allowed!
543 CCachedDirectory::ItDir itMap = m_directoryCache.find(path.GetDirectory());
544 if ((itMap != m_directoryCache.end())&&(itMap->second))
546 if (path.IsDirectory())
548 m_mostRecentStatus = itMap->second->GetOwnStatus(false);
549 return m_mostRecentStatus;
551 else
553 // We've found this directory in the cache
554 CCachedDirectory * cachedDir = itMap->second;
555 CStatusCacheEntry entry = cachedDir->GetCacheStatusForMember(path);
557 m_mostRecentStatus = entry;
558 return m_mostRecentStatus;
563 ATLTRACE(_T("ignored no good path %s\n"), path.GetWinPath());
564 m_mostRecentStatus = CStatusCacheEntry();
565 if (m_shellCache.ShowExcludedAsNormal() && path.IsDirectory() && m_shellCache.HasGITAdminDir(path.GetWinPath(), true))
567 ATLTRACE(_T("force status %s\n"), path.GetWinPath());
568 m_mostRecentStatus.ForceStatus(git_wc_status_normal);
570 return m_mostRecentStatus;
573 void CGitStatusCache::AddFolderForCrawling(const CTGitPath& path)
575 m_folderCrawler.AddDirectoryForUpdate(path);
578 void CGitStatusCache::CloseWatcherHandles(HDEVNOTIFY hdev)
580 CTGitPath path = watcher.CloseInfoMap(hdev);
581 m_folderCrawler.BlockPath(path);
584 void CGitStatusCache::CloseWatcherHandles(const CTGitPath& path)
586 watcher.CloseHandlesForPath(path);
587 m_folderCrawler.BlockPath(path);