1 // TortoiseGit - a Windows shell extension for easy version control
3 // External Cache Copyright (C) 2005-2008,2011 - 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 "foldercrawler.h"
23 #include "GitStatusCache.h"
25 #include "TGitCache.h"
30 CFolderCrawler::CFolderCrawler(void)
32 m_hWakeEvent
= CreateEvent(NULL
,FALSE
,FALSE
,NULL
);
33 m_hTerminationEvent
= CreateEvent(NULL
,TRUE
,FALSE
,NULL
);
34 m_lCrawlInhibitSet
= 0;
35 m_crawlHoldoffReleasesAt
= (long)GetTickCount();
37 m_bPathsAddedSinceLastCrawl
= false;
38 m_bItemsAddedSinceLastCrawl
= false;
41 CFolderCrawler::~CFolderCrawler(void)
46 void CFolderCrawler::Stop()
49 if (m_hTerminationEvent
)
51 SetEvent(m_hTerminationEvent
);
52 if(WaitForSingleObject(m_hThread
, 4000) != WAIT_OBJECT_0
)
54 ATLTRACE("Error terminating crawler thread\n");
57 m_hThread
.CloseHandle();
58 m_hTerminationEvent
.CloseHandle();
59 m_hWakeEvent
.CloseHandle();
62 void CFolderCrawler::Initialise()
64 // Don't call Initialize more than once
65 ATLASSERT(!m_hThread
);
67 // Just start the worker thread.
68 // It will wait for event being signaled.
69 // If m_hWakeEvent is already signaled the worker thread
70 // will behave properly (with normal priority at worst).
73 unsigned int threadId
;
74 m_hThread
= (HANDLE
)_beginthreadex(NULL
,0,ThreadEntry
,this,0,&threadId
);
75 SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_IDLE
);
78 void CFolderCrawler::RemoveDuplicate(std::deque
<CTGitPath
> &list
,const CTGitPath
&path
)
80 std::deque
<CTGitPath
>::iterator it
, lastit
;
81 for(it
= list
.begin(); it
!= list
.end(); ++it
)
86 it
= list
.begin(); /* search again*/
92 void CFolderCrawler::AddDirectoryForUpdate(const CTGitPath
& path
)
94 /* Index file changing*/
95 if( GitStatus::IsExistIndexLockFile((CString
&)path
.GetWinPathString()))
98 if (!CGitStatusCache::Instance().IsPathGood(path
))
101 ATLTRACE(_T("AddDirectoryForUpdate %s\n"),path
.GetWinPath());
103 AutoLocker
lock(m_critSec
);
105 m_foldersToUpdate
.Push(path
);
107 //ATLASSERT(path.IsDirectory() || !path.Exists());
108 // set this flag while we are sync'ed
109 // with the worker thread
110 m_bItemsAddedSinceLastCrawl
= true;
113 SetEvent(m_hWakeEvent
);
116 void CFolderCrawler::AddPathForUpdate(const CTGitPath
& path
)
118 /* Index file changing*/
119 if( GitStatus::IsExistIndexLockFile((CString
&)path
.GetWinPathString()))
123 AutoLocker
lock(m_critSec
);
125 m_pathsToUpdate
.Push(path
);
126 m_bPathsAddedSinceLastCrawl
= true;
129 SetEvent(m_hWakeEvent
);
132 unsigned int CFolderCrawler::ThreadEntry(void* pContext
)
134 ((CFolderCrawler
*)pContext
)->WorkerThread();
138 void CFolderCrawler::WorkerThread()
140 HANDLE hWaitHandles
[2];
141 hWaitHandles
[0] = m_hTerminationEvent
;
142 hWaitHandles
[1] = m_hWakeEvent
;
143 CTGitPath workingPath
;
144 bool bFirstRunAfterWakeup
= false;
145 DWORD currentTicks
= 0;
149 bool bRecursive
= !!(DWORD
)CRegStdDWORD(_T("Software\\TortoiseGit\\RecursiveOverlay"), TRUE
);
151 if (SysInfo::Instance().IsVistaOrLater())
153 SetThreadPriority(GetCurrentThread(), THREAD_MODE_BACKGROUND_END
);
155 DWORD waitResult
= WaitForMultipleObjects(_countof(hWaitHandles
), hWaitHandles
, FALSE
, INFINITE
);
157 // exit event/working loop if the first event (m_hTerminationEvent)
158 // has been signaled or if one of the events has been abandoned
159 // (i.e. ~CFolderCrawler() is being executed)
160 if(m_bRun
== false || waitResult
== WAIT_OBJECT_0
|| waitResult
== WAIT_ABANDONED_0
|| waitResult
== WAIT_ABANDONED_0
+1)
166 if (SysInfo::Instance().IsVistaOrLater())
168 SetThreadPriority(GetCurrentThread(), THREAD_MODE_BACKGROUND_BEGIN
);
171 // If we get here, we've been woken up by something being added to the queue.
172 // However, it's important that we don't do our crawling while
173 // the shell is still asking for items
174 bFirstRunAfterWakeup
= true;
180 if (CGitStatusCache::Instance().m_bClearMemory
)
182 CAutoWriteLock
writeLock(CGitStatusCache::Instance().GetGuard());
183 CGitStatusCache::Instance().ClearCache();
184 CGitStatusCache::Instance().m_bClearMemory
= false;
186 if(m_lCrawlInhibitSet
> 0)
188 // We're in crawl hold-off
189 ATLTRACE("Crawl hold-off\n");
193 if (bFirstRunAfterWakeup
)
196 ATLTRACE("Crawl bFirstRunAfterWakeup\n");
197 bFirstRunAfterWakeup
= false;
200 if ((m_blockReleasesAt
< GetTickCount())&&(!m_blockedPath
.IsEmpty()))
202 ATLTRACE(_T("Crawl stop blocking path %s\n"), m_blockedPath
.GetWinPath());
203 m_blockedPath
.Reset();
205 CGitStatusCache::Instance().RemoveTimedoutBlocks();
207 if (m_foldersToUpdate
.empty() && m_pathsToUpdate
.empty())
209 // Nothing left to do
212 currentTicks
= GetTickCount();
213 if (!m_pathsToUpdate
.empty())
216 AutoLocker
lock(m_critSec
);
218 m_bPathsAddedSinceLastCrawl
= false;
220 workingPath
= m_pathsToUpdate
.Pop();
221 if ((!m_blockedPath
.IsEmpty()) && (m_blockedPath
.IsAncestorOf(workingPath
)))
223 // move the path to the end of the list
224 m_pathsToUpdate
.Push(workingPath
);
225 if (m_pathsToUpdate
.size() < 3)
231 // don't crawl paths that are excluded
232 if (!CGitStatusCache::Instance().IsPathAllowed(workingPath
))
234 // check if the changed path is inside an .git folder
236 if ((workingPath
.HasAdminDir(&projectroot
)&&workingPath
.IsDirectory()) || workingPath
.IsAdminDir())
238 // we don't crawl for paths changed in a tmp folder inside an .git folder.
239 // Because we also get notifications for those even if we just ask for the status!
240 // And changes there don't affect the file status at all, so it's safe
241 // to ignore notifications on those paths.
242 if (workingPath
.IsAdminDir())
244 // TODO: add git specific filters here. is there really any change besides index file in .git
245 // that is relevant for overlays?
246 /*CString lowerpath = workingPath.GetWinPathString();
247 lowerpath.MakeLower();
248 if (lowerpath.Find(_T("\\tmp\\"))>0)
250 if (lowerpath.Find(_T("\\tmp")) == (lowerpath.GetLength()-4))
252 if (lowerpath.Find(_T("\\log"))>0)
254 // Here's a little problem:
255 // the lock file is also created for fetching the status
256 // and not just when committing.
257 // If we could find out why the lock file was changed
258 // we could decide to crawl the folder again or not.
259 // But for now, we have to crawl the parent folder
262 //if (lowerpath.Find(_T("\\lock"))>0)
264 // only go back to wc root if we are in .git-dir
267 workingPath
= workingPath
.GetContainingDirectory();
268 } while(workingPath
.IsAdminDir());
270 else if (!workingPath
.Exists())
272 CAutoWriteLock
writeLock(CGitStatusCache::Instance().GetGuard());
273 CGitStatusCache::Instance().RemoveCacheForPath(workingPath
);
277 if (!CGitStatusCache::Instance().IsPathGood(workingPath
))
279 AutoLocker
lock(m_critSec
);
280 // move the path, the root of the repository, to the end of the list
281 if (projectroot
.IsEmpty())
282 m_pathsToUpdate
.Push(workingPath
);
284 m_pathsToUpdate
.Push(CTGitPath(projectroot
));
285 if (m_pathsToUpdate
.size() < 3)
290 ATLTRACE(_T("Invalidating and refreshing folder: %s\n"), workingPath
.GetWinPath());
292 AutoLocker
print(critSec
);
293 _sntprintf_s(szCurrentCrawledPath
[nCurrentCrawledpathIndex
], MAX_CRAWLEDPATHSLEN
, _TRUNCATE
, _T("Invalidating and refreshing folder: %s"), workingPath
.GetWinPath());
294 nCurrentCrawledpathIndex
++;
295 if (nCurrentCrawledpathIndex
>= MAX_CRAWLEDPATHS
)
296 nCurrentCrawledpathIndex
= 0;
298 InvalidateRect(hWnd
, NULL
, FALSE
);
300 CAutoReadLock
readLock(CGitStatusCache::Instance().GetGuard());
301 // Invalidate the cache of this folder, to make sure its status is fetched again.
302 CCachedDirectory
* pCachedDir
= CGitStatusCache::Instance().GetDirectoryCacheEntry(workingPath
);
305 git_wc_status_kind status
= pCachedDir
->GetCurrentFullStatus();
306 pCachedDir
->Invalidate();
307 if (workingPath
.Exists())
309 pCachedDir
->RefreshStatus(bRecursive
);
310 // if the previous status wasn't normal and now it is, then
311 // send a notification too.
312 // We do this here because GetCurrentFullStatus() doesn't send
313 // notifications for 'normal' status - if it would, we'd get tons
314 // of notifications when crawling a working copy not yet in the cache.
315 if ((status
!= git_wc_status_normal
) && (pCachedDir
->GetCurrentFullStatus() != status
))
317 CGitStatusCache::Instance().UpdateShell(workingPath
);
318 ATLTRACE(_T("shell update in crawler for %s\n"), workingPath
.GetWinPath());
323 CAutoWriteLock
writeLock(CGitStatusCache::Instance().GetGuard());
324 CGitStatusCache::Instance().RemoveCacheForPath(workingPath
);
328 //In case that svn_client_stat() modified a file and we got
329 //a notification about that in the directory watcher,
330 //remove that here again - this is to prevent an endless loop
331 AutoLocker
lock(m_critSec
);
332 m_pathsToUpdate
.erase(workingPath
);
334 else if (workingPath
.HasAdminDir())
336 if (!workingPath
.Exists())
338 CAutoWriteLock
writeLock(CGitStatusCache::Instance().GetGuard());
339 CGitStatusCache::Instance().RemoveCacheForPath(workingPath
);
340 if (!workingPath
.GetContainingDirectory().Exists())
343 workingPath
= workingPath
.GetContainingDirectory();
345 ATLTRACE(_T("Updating path: %s\n"), workingPath
.GetWinPath());
347 AutoLocker
print(critSec
);
348 _sntprintf_s(szCurrentCrawledPath
[nCurrentCrawledpathIndex
], MAX_CRAWLEDPATHSLEN
, _TRUNCATE
, _T("Updating path: %s"), workingPath
.GetWinPath());
349 nCurrentCrawledpathIndex
++;
350 if (nCurrentCrawledpathIndex
>= MAX_CRAWLEDPATHS
)
351 nCurrentCrawledpathIndex
= 0;
353 InvalidateRect(hWnd
, NULL
, FALSE
);
354 // HasAdminDir() already checks if the path points to a dir
355 DWORD flags
= TGITCACHE_FLAGS_FOLDERISKNOWN
;
356 flags
|= (workingPath
.IsDirectory() ? TGITCACHE_FLAGS_ISFOLDER
: 0);
357 flags
|= (bRecursive
? TGITCACHE_FLAGS_RECUSIVE_STATUS
: 0);
359 CAutoReadLock
readLock(CGitStatusCache::Instance().GetGuard());
360 // Invalidate the cache of folders manually. The cache of files is invalidated
361 // automatically if the status is asked for it and the file times don't match
362 // anymore, so we don't need to manually invalidate those.
363 if (workingPath
.IsDirectory())
365 CCachedDirectory
* cachedDir
= CGitStatusCache::Instance().GetDirectoryCacheEntry(workingPath
);
367 cachedDir
->Invalidate();
369 CStatusCacheEntry ce
= CGitStatusCache::Instance().GetStatusForPath(workingPath
, flags
);
370 if (ce
.GetEffectiveStatus() > git_wc_status_unversioned
)
372 CGitStatusCache::Instance().UpdateShell(workingPath
);
373 ATLTRACE(_T("shell update in folder crawler for %s\n"), workingPath
.GetWinPath());
376 AutoLocker
lock(m_critSec
);
377 m_pathsToUpdate
.erase(workingPath
);
381 if (!workingPath
.Exists())
383 CAutoWriteLock
writeLock(CGitStatusCache::Instance().GetGuard());
384 CGitStatusCache::Instance().RemoveCacheForPath(workingPath
);
388 else if (!m_foldersToUpdate
.empty())
391 AutoLocker
lock(m_critSec
);
392 m_bItemsAddedSinceLastCrawl
= false;
394 // create a new CTSVNPath object to make sure the cached flags are requested again.
395 // without this, a missing file/folder is still treated as missing even if it is available
396 // now when crawling.
397 workingPath
= CTGitPath(m_foldersToUpdate
.Pop().GetWinPath());
399 if ((!m_blockedPath
.IsEmpty())&&(m_blockedPath
.IsAncestorOf(workingPath
)))
401 // move the path to the end of the list
402 m_foldersToUpdate
.Push(workingPath
);
403 if (m_foldersToUpdate
.size() < 3)
408 if (DWORD(workingPath
.GetCustomData()) >= currentTicks
)
413 if ((!m_blockedPath
.IsEmpty())&&(m_blockedPath
.IsAncestorOf(workingPath
)))
415 if (!CGitStatusCache::Instance().IsPathAllowed(workingPath
))
417 if (!CGitStatusCache::Instance().IsPathGood(workingPath
))
420 ATLTRACE(_T("Crawling folder: %s\n"), workingPath
.GetWinPath());
422 AutoLocker
print(critSec
);
423 _sntprintf_s(szCurrentCrawledPath
[nCurrentCrawledpathIndex
], MAX_CRAWLEDPATHSLEN
, _TRUNCATE
, _T("Crawling folder: %s"), workingPath
.GetWinPath());
424 nCurrentCrawledpathIndex
++;
425 if (nCurrentCrawledpathIndex
>= MAX_CRAWLEDPATHS
)
426 nCurrentCrawledpathIndex
= 0;
428 InvalidateRect(hWnd
, NULL
, FALSE
);
430 CAutoReadLock
readLock(CGitStatusCache::Instance().GetGuard());
431 // Now, we need to visit this folder, to make sure that we know its 'most important' status
432 CCachedDirectory
* cachedDir
= CGitStatusCache::Instance().GetDirectoryCacheEntry(workingPath
.GetDirectory());
433 // check if the path is monitored by the watcher. If it isn't, then we have to invalidate the cache
434 // for that path and add it to the watcher.
435 if (!CGitStatusCache::Instance().IsPathWatched(workingPath
))
437 if (workingPath
.HasAdminDir())
439 ATLTRACE(_T("Add watch path %s\n"), workingPath
.GetWinPath());
440 CGitStatusCache::Instance().AddPathToWatch(workingPath
);
443 cachedDir
->Invalidate();
446 CAutoWriteLock
writeLock(CGitStatusCache::Instance().GetGuard());
447 CGitStatusCache::Instance().RemoveCacheForPath(workingPath
);
448 // now cacheDir is invalid because it got deleted in the RemoveCacheForPath() call above.
453 cachedDir
->RefreshStatus(bRecursive
);
456 // While refreshing the status, we could get another crawl request for the same folder.
457 // This can happen if the crawled folder has a lower status than one of the child folders
458 // (recursively). To avoid double crawlings, remove such a crawl request here
459 AutoLocker
lock(m_critSec
);
460 if (m_bItemsAddedSinceLastCrawl
)
462 m_foldersToUpdate
.erase(workingPath
);
470 bool CFolderCrawler::SetHoldoff(DWORD milliseconds
/* = 100*/)
472 long tick
= (long)GetTickCount();
473 bool ret
= ((tick
- m_crawlHoldoffReleasesAt
) > 0);
474 m_crawlHoldoffReleasesAt
= tick
+ milliseconds
;
478 void CFolderCrawler::BlockPath(const CTGitPath
& path
, DWORD ticks
)
480 ATLTRACE(_T("block path %s from being crawled\n"), path
.GetWinPath());
481 m_blockedPath
= path
;
483 m_blockReleasesAt
= GetTickCount()+10000;
485 m_blockReleasesAt
= GetTickCount()+ticks
;