1 // TortoiseGit - a Windows shell extension for easy version control
3 // External Cache Copyright (C) 2005-2006,2008,2010 - TortoiseSVN
4 // Copyright (C) 2008-2012 - 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.
22 #include "GitStatus.h"
23 #include "GitStatuscache.h"
24 #include "CacheInterface.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
);
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;
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)
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"));
71 CopyFile(path
, path2
, FALSE
);
73 pFile
= _tfsopen(path2
, _T("rb"), _SH_DENYNO
);
78 LOADVALUEFROMFILE(value
);
84 LOADVALUEFROMFILE(mapsize
);
85 for (int i
=0; i
<mapsize
; ++i
)
87 LOADVALUEFROMFILE2(value
);
93 if (fread(sKey
.GetBuffer(value
+1), sizeof(TCHAR
), value
, pFile
)!=value
)
95 sKey
.ReleaseBuffer(0);
98 sKey
.ReleaseBuffer(value
);
99 CCachedDirectory
* cacheddir
= new CCachedDirectory();
100 if (cacheddir
== NULL
)
102 if (!cacheddir
->LoadFromDisk(pFile
))
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
, false);
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
)
130 m_pInstance
->watcher
.ClearInfoMap();
131 ATLTRACE("cache loaded from disk successfully!\n");
137 m_pInstance
->watcher
.ClearInfoMap();
139 m_pInstance
= new CGitStatusCache
;
140 ATLTRACE("cache not loaded from disk\n");
143 bool CGitStatusCache::SaveCache()
145 #define WRITEVALUETOFILE(x) if (fwrite(&x, sizeof(x), 1, pFile)!=1) goto error;
146 unsigned int value
= 0;
147 // save the cache to disk
149 // find a location to write the cache to
150 TCHAR path
[MAX_PATH
]; //MAX_PATH ok here.
151 if (SHGetFolderPath(NULL
, CSIDL_LOCAL_APPDATA
, NULL
, SHGFP_TYPE_CURRENT
, path
)==S_OK
)
153 _tcscat_s(path
, MAX_PATH
, _T("\\TGitCache"));
154 if (!PathIsDirectory(path
))
155 CreateDirectory(path
, NULL
);
156 _tcscat_s(path
, MAX_PATH
, _T("\\cache"));
157 _tfopen_s(&pFile
, path
, _T("wb"));
160 value
= 2; // 'version'
161 WRITEVALUETOFILE(value
);
162 value
= (int)m_pInstance
->m_directoryCache
.size();
163 WRITEVALUETOFILE(value
);
164 for (CCachedDirectory::CachedDirMap::iterator I
= m_pInstance
->m_directoryCache
.begin(); I
!= m_pInstance
->m_directoryCache
.end(); ++I
)
166 if (I
->second
== NULL
)
169 WRITEVALUETOFILE(value
);
172 const CString
& key
= I
->first
.GetWinPathString();
173 value
= key
.GetLength();
174 WRITEVALUETOFILE(value
);
177 if (fwrite((LPCTSTR
)key
, sizeof(TCHAR
), value
, pFile
)!=value
)
179 if (!I
->second
->SaveToDisk(pFile
))
186 ATLTRACE(_T("cache saved to disk at %s\n"), path
);
195 void CGitStatusCache::Destroy()
206 void CGitStatusCache::Stop()
208 // m_svnHelp.Cancel(true);
210 m_folderCrawler
.Stop();
211 m_shellUpdater
.Stop();
214 void CGitStatusCache::Init()
216 m_folderCrawler
.Initialise();
217 m_shellUpdater
.Initialise();
220 CGitStatusCache::CGitStatusCache(void)
222 #define forever DWORD(-1)
223 AutoLocker
lock(m_NoWatchPathCritSec
);
224 TCHAR path
[MAX_PATH
];
225 SHGetFolderPath(NULL
, CSIDL_COOKIES
, NULL
, 0, path
);
226 m_NoWatchPaths
[CTGitPath(CString(path
))] = forever
;
227 SHGetFolderPath(NULL
, CSIDL_HISTORY
, NULL
, 0, path
);
228 m_NoWatchPaths
[CTGitPath(CString(path
))] = forever
;
229 SHGetFolderPath(NULL
, CSIDL_INTERNET_CACHE
, NULL
, 0, path
);
230 m_NoWatchPaths
[CTGitPath(CString(path
))] = forever
;
231 SHGetFolderPath(NULL
, CSIDL_SYSTEM
, NULL
, 0, path
);
232 m_NoWatchPaths
[CTGitPath(CString(path
))] = forever
;
233 SHGetFolderPath(NULL
, CSIDL_WINDOWS
, NULL
, 0, path
);
234 m_NoWatchPaths
[CTGitPath(CString(path
))] = forever
;
235 m_bClearMemory
= false;
236 m_mostRecentExpiresAt
= 0;
239 CGitStatusCache::~CGitStatusCache(void)
241 CAutoWriteLock
writeLock(m_guard
);
245 void CGitStatusCache::Refresh()
247 m_shellCache
.ForceRefresh();
248 // m_pInstance->m_svnHelp.ReloadConfig();
249 if (!m_pInstance
->m_directoryCache
.empty())
251 CCachedDirectory::CachedDirMap::iterator I
= m_pInstance
->m_directoryCache
.begin();
252 for (/* no init */; I
!= m_pInstance
->m_directoryCache
.end(); ++I
)
254 if (m_shellCache
.IsPathAllowed(I
->first
.GetWinPath()))
255 I
->second
->RefreshMostImportant();
258 CGitStatusCache::Instance().RemoveCacheForPath(I
->first
);
259 I
= m_pInstance
->m_directoryCache
.begin();
260 if (I
== m_pInstance
->m_directoryCache
.end())
267 bool CGitStatusCache::IsPathGood(const CTGitPath
& path
)
269 AutoLocker
lock(m_NoWatchPathCritSec
);
270 for (std::map
<CTGitPath
, DWORD
>::const_iterator it
= m_NoWatchPaths
.begin(); it
!= m_NoWatchPaths
.end(); ++it
)
272 // the ticks check is necessary here, because RemoveTimedoutBlocks is only called within the FolderCrawler loop
273 // and we might miss update calls
274 // 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)
275 if (GetTickCount() < it
->second
&& it
->first
.IsAncestorOf(path
))
277 ATLTRACE(_T("path not good: %s\n"), it
->first
.GetWinPath());
284 bool CGitStatusCache::BlockPath(const CTGitPath
& path
, DWORD timeout
/* = 0 */)
287 timeout
= BLOCK_PATH_DEFAULT_TIMEOUT
;
289 if (timeout
> BLOCK_PATH_MAX_TIMEOUT
)
290 timeout
= BLOCK_PATH_MAX_TIMEOUT
;
292 timeout
= GetTickCount() + (timeout
* 1000); // timeout is in seconds, but we need the milliseconds
294 AutoLocker
lock(m_NoWatchPathCritSec
);
295 m_NoWatchPaths
[path
.GetDirectory()] = timeout
;
300 bool CGitStatusCache::UnBlockPath(const CTGitPath
& path
)
303 AutoLocker
lock(m_NoWatchPathCritSec
);
304 std::map
<CTGitPath
, DWORD
>::iterator it
= m_NoWatchPaths
.find(path
.GetDirectory());
305 if (it
!= m_NoWatchPaths
.end())
307 ATLTRACE(_T("path removed from no good: %s\n"), it
->first
.GetWinPath());
308 m_NoWatchPaths
.erase(it
);
311 AddFolderForCrawling(path
.GetDirectory());
316 bool CGitStatusCache::RemoveTimedoutBlocks()
319 DWORD currentTicks
= GetTickCount();
320 AutoLocker
lock(m_NoWatchPathCritSec
);
321 std::vector
<CTGitPath
> toRemove
;
322 for (std::map
<CTGitPath
, DWORD
>::const_iterator it
= m_NoWatchPaths
.begin(); it
!= m_NoWatchPaths
.end(); ++it
)
324 if (currentTicks
> it
->second
)
326 toRemove
.push_back(it
->first
);
329 if (!toRemove
.empty())
331 for (std::vector
<CTGitPath
>::const_iterator it
= toRemove
.begin(); it
!= toRemove
.end(); ++it
)
333 ret
= ret
|| UnBlockPath(*it
);
340 void CGitStatusCache::UpdateShell(const CTGitPath
& path
)
342 m_shellUpdater
.AddPathForUpdate(path
);
345 void CGitStatusCache::ClearCache()
347 for (CCachedDirectory::CachedDirMap::iterator I
= m_directoryCache
.begin(); I
!= m_directoryCache
.end(); ++I
)
352 m_directoryCache
.clear();
355 bool CGitStatusCache::RemoveCacheForDirectory(CCachedDirectory
* cdir
)
360 typedef std::map
<CTGitPath
, git_wc_status_kind
> ChildDirStatus
;
361 if (!cdir
->m_childDirectories
.empty())
363 ChildDirStatus::iterator it
= cdir
->m_childDirectories
.begin();
364 for (; it
!= cdir
->m_childDirectories
.end(); )
366 CCachedDirectory
* childdir
= CGitStatusCache::Instance().GetDirectoryCacheEntryNoCreate(it
->first
);
367 if ((childdir
) && (!cdir
->m_directoryPath
.IsEquivalentTo(childdir
->m_directoryPath
)) && (cdir
->m_directoryPath
.GetFileOrDirectoryName() != L
".."))
368 RemoveCacheForDirectory(childdir
);
369 cdir
->m_childDirectories
.erase(it
->first
);
370 it
= cdir
->m_childDirectories
.begin();
373 cdir
->m_childDirectories
.clear();
374 m_directoryCache
.erase(cdir
->m_directoryPath
);
376 // we could have entries versioned and/or stored in our cache which are
377 // children of the specified directory, but not in the m_childDirectories
379 CCachedDirectory::ItDir itMap
= m_directoryCache
.lower_bound(cdir
->m_directoryPath
);
382 if (itMap
!= m_directoryCache
.end())
384 if (cdir
->m_directoryPath
.IsAncestorOf(itMap
->first
))
386 // just in case (see TortoiseSVN issue #255)
387 if (itMap
->second
== cdir
)
389 m_directoryCache
.erase(itMap
);
392 RemoveCacheForDirectory(itMap
->second
);
395 itMap
= m_directoryCache
.lower_bound(cdir
->m_directoryPath
);
396 } while (itMap
!= m_directoryCache
.end() && cdir
->m_directoryPath
.IsAncestorOf(itMap
->first
));
398 ATLTRACE(_T("removed path %s from cache\n"), cdir
->m_directoryPath
.GetWinPathString());
404 void CGitStatusCache::RemoveCacheForPath(const CTGitPath
& path
)
406 // Stop the crawler starting on a new folder
407 CCrawlInhibitor
crawlInhibit(&m_folderCrawler
);
408 CCachedDirectory::ItDir itMap
;
409 CCachedDirectory
* dirtoremove
= NULL
;
411 itMap
= m_directoryCache
.find(path
);
412 if ((itMap
!= m_directoryCache
.end())&&(itMap
->second
))
413 dirtoremove
= itMap
->second
;
414 if (dirtoremove
== NULL
)
416 ATLASSERT(path
.IsEquivalentToWithoutCase(dirtoremove
->m_directoryPath
));
417 RemoveCacheForDirectory(dirtoremove
);
420 CCachedDirectory
* CGitStatusCache::GetDirectoryCacheEntry(const CTGitPath
& path
, bool isAddToWatch
)
422 ATLASSERT(path
.IsDirectory() || !PathFileExists(path
.GetWinPath()));
425 CCachedDirectory::ItDir itMap
;
426 itMap
= m_directoryCache
.find(path
);
427 if ((itMap
!= m_directoryCache
.end())&&(itMap
->second
))
429 // We've found this directory in the cache
430 return itMap
->second
;
434 // if the CCachedDirectory is NULL but the path is in our cache,
435 // that means that path got invalidated and needs to be treated
436 // as if it never was in our cache. So we remove the last remains
437 // from the cache and start from scratch.
439 CAutoWriteLock
writeLock(m_guard
);
440 // Since above there's a small chance that before we can upgrade to
441 // writer state some other thread gained writer state and changed
442 // the data, we have to recreate the iterator here again.
443 itMap
= m_directoryCache
.find(path
);
444 if (itMap
!=m_directoryCache
.end())
446 delete itMap
->second
;
447 itMap
->second
= NULL
;
448 m_directoryCache
.erase(itMap
);
450 // We don't know anything about this directory yet - lets add it to our cache
451 // but only if it exists!
452 if (path
.Exists() && m_shellCache
.IsPathAllowed(path
.GetWinPath()) && !g_GitAdminDir
.IsAdminDirPath(path
.GetWinPath()))
454 // some notifications are for files which got removed/moved around.
455 // In such cases, the CTGitPath::IsDirectory() will return true (it assumes a directory if
456 // the path doesn't exist). Which means we can get here with a path to a file
457 // instead of a directory.
458 // Since we're here most likely called from the crawler thread, the file could exist
459 // again. If that's the case, just do nothing
460 if (path
.IsDirectory()||(!path
.Exists()))
462 ATLTRACE(_T("adding %s to our cache\n"), path
.GetWinPath());
463 CCachedDirectory
* newcdir
= new CCachedDirectory(path
);
466 CCachedDirectory
* cdir
= m_directoryCache
.insert(m_directoryCache
.lower_bound(path
), std::make_pair(path
, newcdir
))->second
;
468 if ((!path
.IsEmpty())&&(path
.HasAdminDir(&gitdir
))&&isAddToWatch
)
470 /* Just watch version path */
471 watcher
.AddPath(gitdir
);
472 watcher
.AddPath(path
);
476 m_bClearMemory
= true;
483 CCachedDirectory
* CGitStatusCache::GetDirectoryCacheEntryNoCreate(const CTGitPath
& path
)
485 ATLASSERT(path
.IsDirectory() || !PathFileExists(path
.GetWinPath()));
487 CCachedDirectory::ItDir itMap
;
488 itMap
= m_directoryCache
.find(path
);
489 if(itMap
!= m_directoryCache
.end())
491 // We've found this directory in the cache
492 return itMap
->second
;
497 /* Fetch is true, means fetch status from */
498 /* Fetch is false, means fetch status from cache */
499 CStatusCacheEntry
CGitStatusCache::GetStatusForPath(const CTGitPath
& path
, DWORD flags
, bool bFetch
/* = true */)
501 bool bRecursive
= !!(flags
& TGITCACHE_FLAGS_RECUSIVE_STATUS
);
503 // Check a very short-lived 'mini-cache' of the last thing we were asked for.
504 long now
= (long)GetTickCount();
505 if(now
-m_mostRecentExpiresAt
< 0)
507 if(path
.IsEquivalentToWithoutCase(m_mostRecentPath
))
509 return m_mostRecentStatus
;
513 AutoLocker
lock(m_critSec
);
514 m_mostRecentPath
= path
;
515 m_mostRecentExpiresAt
= now
+ 1000;
518 if (IsPathGood(path
))
520 // Stop the crawler starting on a new folder while we're doing this much more important task...
521 // Please note, that this may be a second "lock" used concurrently to the one in RemoveCacheForPath().
522 CCrawlInhibitor
crawlInhibit(&m_folderCrawler
);
524 CTGitPath dirpath
= path
.GetContainingDirectory();
525 if ((dirpath
.IsEmpty()) || (!m_shellCache
.IsPathAllowed(dirpath
.GetWinPath())))
526 dirpath
= path
.GetDirectory();
527 CCachedDirectory
* cachedDir
= GetDirectoryCacheEntry(dirpath
);
528 if (cachedDir
!= NULL
)
530 //ATLTRACE(_T("GetStatusForMember %d\n"), bFetch);
531 CStatusCacheEntry entry
= cachedDir
->GetStatusForMember(path
, bRecursive
, bFetch
);
533 AutoLocker
lock(m_critSec
);
534 m_mostRecentStatus
= entry
;
535 return m_mostRecentStatus
;
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 CStatusCacheEntry entry
= itMap
->second
->GetOwnStatus(false);
549 AutoLocker
lock(m_critSec
);
550 m_mostRecentStatus
= entry
;
551 return m_mostRecentStatus
;
555 // We've found this directory in the cache
556 CCachedDirectory
* cachedDir
= itMap
->second
;
557 CStatusCacheEntry entry
= cachedDir
->GetCacheStatusForMember(path
);
559 AutoLocker
lock(m_critSec
);
560 m_mostRecentStatus
= entry
;
561 return m_mostRecentStatus
;
566 AutoLocker
lock(m_critSec
);
567 ATLTRACE(_T("ignored no good path %s\n"), path
.GetWinPath());
568 m_mostRecentStatus
= CStatusCacheEntry();
569 if (m_shellCache
.ShowExcludedAsNormal() && path
.IsDirectory() && m_shellCache
.HasGITAdminDir(path
.GetWinPath(), true))
571 ATLTRACE(_T("force status %s\n"), path
.GetWinPath());
572 m_mostRecentStatus
.ForceStatus(git_wc_status_normal
);
574 return m_mostRecentStatus
;
577 void CGitStatusCache::AddFolderForCrawling(const CTGitPath
& path
)
579 m_folderCrawler
.AddDirectoryForUpdate(path
);
582 void CGitStatusCache::CloseWatcherHandles(HDEVNOTIFY hdev
)
584 CTGitPath path
= watcher
.CloseInfoMap(hdev
);
585 m_folderCrawler
.BlockPath(path
);
588 void CGitStatusCache::CloseWatcherHandles(const CTGitPath
& path
)
590 watcher
.CloseHandlesForPath(path
);
591 m_folderCrawler
.BlockPath(path
);