Use forward slashes in the includes for consistency and also remove double slashes
[TortoiseGit.git] / src / Git / GitStatusListCtrl.cpp
blobf07138396aaae187ff70d80ea5ec81caa67a1fbd
1 // TortoiseGit - a Windows shell extension for easy version control
3 // Copyright (C) 2008-2017 - TortoiseGit
4 // Copyright (C) 2003-2008, 2013-2015 - TortoiseSVN
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 "resource.h"
23 #include "../TortoiseShell/resource.h"
24 #include "GitStatusListCtrl.h"
25 #include "MessageBox.h"
26 #include "MyMemDC.h"
27 #include "UnicodeUtils.h"
28 #include "AppUtils.h"
29 #include "PathUtils.h"
30 #include "TempFile.h"
31 #include "StringUtils.h"
32 #include "LoglistUtils.h"
33 #include "Git.h"
34 #include "GitRev.h"
35 #include "GitDiff.h"
36 #include "GitProgressDlg.h"
37 #include "SysImageList.h"
38 #include "TGitPath.h"
39 #include "registry.h"
40 #include "InputDlg.h"
41 #include "GitAdminDir.h"
42 #include "GitDataObject.h"
43 #include "ProgressCommands/AddProgressCommand.h"
44 #include "IconMenu.h"
45 #include "FormatMessageWrapper.h"
46 #include "BrowseFolder.h"
47 #include "SysInfo.h"
49 const UINT CGitStatusListCtrl::GITSLNM_ITEMCOUNTCHANGED
50 = ::RegisterWindowMessage(L"GITSLNM_ITEMCOUNTCHANGED");
51 const UINT CGitStatusListCtrl::GITSLNM_NEEDSREFRESH
52 = ::RegisterWindowMessage(L"GITSLNM_NEEDSREFRESH");
53 const UINT CGitStatusListCtrl::GITSLNM_ADDFILE
54 = ::RegisterWindowMessage(L"GITSLNM_ADDFILE");
55 const UINT CGitStatusListCtrl::GITSLNM_CHECKCHANGED
56 = ::RegisterWindowMessage(L"GITSLNM_CHECKCHANGED");
57 const UINT CGitStatusListCtrl::GITSLNM_ITEMCHANGED
58 = ::RegisterWindowMessage(L"GITSLNM_ITEMCHANGED");
60 struct icompare
62 bool operator() (const std::wstring& lhs, const std::wstring& rhs) const
64 // no logical comparison here: we need this sorted strictly
65 return _wcsicmp(lhs.c_str(), rhs.c_str()) < 0;
69 class CIShellFolderHook : public IShellFolder
71 public:
72 CIShellFolderHook(LPSHELLFOLDER sf, const CTGitPathList& pathlist)
74 sf->AddRef();
75 m_iSF = sf;
76 // it seems the paths in the HDROP need to be sorted, otherwise
77 // it might not work properly or even crash.
78 // to get the items sorted, we just add them to a set
79 for (int i = 0; i < pathlist.GetCount(); ++i)
80 sortedpaths.insert((LPCTSTR)g_Git.CombinePath(pathlist[i].GetWinPath()));
83 ~CIShellFolderHook() { m_iSF->Release(); }
85 // IUnknown methods --------
86 virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, __RPC__deref_out void** ppvObject) override { return m_iSF->QueryInterface(riid, ppvObject); }
87 virtual ULONG STDMETHODCALLTYPE AddRef(void) override { return m_iSF->AddRef(); }
88 virtual ULONG STDMETHODCALLTYPE Release(void) override { return m_iSF->Release(); }
90 // IShellFolder methods ----
91 virtual HRESULT STDMETHODCALLTYPE GetUIObjectOf(HWND hwndOwner, UINT cidl, PCUITEMID_CHILD_ARRAY apidl, REFIID riid, UINT* rgfReserved, void** ppv) override;
93 virtual HRESULT STDMETHODCALLTYPE CompareIDs(LPARAM lParam, __RPC__in PCUIDLIST_RELATIVE pidl1, __RPC__in PCUIDLIST_RELATIVE pidl2) override { return m_iSF->CompareIDs(lParam, pidl1, pidl2); }
94 virtual HRESULT STDMETHODCALLTYPE GetDisplayNameOf(__RPC__in_opt PCUITEMID_CHILD pidl, SHGDNF uFlags, __RPC__out STRRET* pName) override { return m_iSF->GetDisplayNameOf(pidl, uFlags, pName); }
95 virtual HRESULT STDMETHODCALLTYPE CreateViewObject(__RPC__in_opt HWND hwndOwner, __RPC__in REFIID riid, __RPC__deref_out_opt void** ppv) override { return m_iSF->CreateViewObject(hwndOwner, riid, ppv); }
96 virtual HRESULT STDMETHODCALLTYPE EnumObjects(__RPC__in_opt HWND hwndOwner, SHCONTF grfFlags, __RPC__deref_out_opt IEnumIDList** ppenumIDList) override { return m_iSF->EnumObjects(hwndOwner, grfFlags, ppenumIDList); }
97 virtual HRESULT STDMETHODCALLTYPE BindToObject(__RPC__in PCUIDLIST_RELATIVE pidl, __RPC__in_opt IBindCtx* pbc, __RPC__in REFIID riid, __RPC__deref_out_opt void** ppv) override { return m_iSF->BindToObject(pidl, pbc, riid, ppv); }
98 virtual HRESULT STDMETHODCALLTYPE ParseDisplayName(__RPC__in_opt HWND hwnd, __RPC__in_opt IBindCtx* pbc, __RPC__in_string LPWSTR pszDisplayName, __reserved ULONG* pchEaten, __RPC__deref_out_opt PIDLIST_RELATIVE* ppidl, __RPC__inout_opt ULONG* pdwAttributes) override { return m_iSF->ParseDisplayName(hwnd, pbc, pszDisplayName, pchEaten, ppidl, pdwAttributes); }
99 virtual HRESULT STDMETHODCALLTYPE GetAttributesOf(UINT cidl, __RPC__in_ecount_full_opt(cidl) PCUITEMID_CHILD_ARRAY apidl, __RPC__inout SFGAOF* rgfInOut) override { return m_iSF->GetAttributesOf(cidl, apidl, rgfInOut); }
100 virtual HRESULT STDMETHODCALLTYPE BindToStorage(__RPC__in PCUIDLIST_RELATIVE pidl, __RPC__in_opt IBindCtx* pbc, __RPC__in REFIID riid, __RPC__deref_out_opt void** ppv) override { return m_iSF->BindToStorage(pidl, pbc, riid, ppv); }
101 virtual HRESULT STDMETHODCALLTYPE SetNameOf(__in_opt HWND hwnd, __in PCUITEMID_CHILD pidl, __in LPCWSTR pszName, __in SHGDNF uFlags, __deref_opt_out PITEMID_CHILD* ppidlOut) override { return m_iSF->SetNameOf(hwnd, pidl, pszName, uFlags, ppidlOut); }
103 protected:
104 LPSHELLFOLDER m_iSF;
105 std::set<std::wstring, icompare> sortedpaths;
108 HRESULT STDMETHODCALLTYPE CIShellFolderHook::GetUIObjectOf(HWND hwndOwner, UINT cidl, PCUITEMID_CHILD_ARRAY apidl, REFIID riid, UINT* rgfReserved, void** ppv)
110 if (InlineIsEqualGUID(riid, IID_IDataObject))
112 HRESULT hres = m_iSF->GetUIObjectOf(hwndOwner, cidl, apidl, IID_IDataObject, nullptr, ppv);
113 if (FAILED(hres))
114 return hres;
116 IDataObject * idata = (LPDATAOBJECT)(*ppv);
117 // the IDataObject returned here doesn't have a HDROP, so we create one ourselves and add it to the IDataObject
118 // the HDROP is necessary for most context menu handlers
119 int nLength = 0;
120 for (auto it = sortedpaths.cbegin(); it != sortedpaths.cend(); ++it)
122 nLength += (int)it->size();
123 nLength += 1; // '\0' separator
125 int nBufferSize = sizeof(DROPFILES) + ((nLength + 5)*sizeof(TCHAR));
126 auto pBuffer = std::make_unique<char[]>(nBufferSize);
127 SecureZeroMemory(pBuffer.get(), nBufferSize);
128 DROPFILES* df = (DROPFILES*)pBuffer.get();
129 df->pFiles = sizeof(DROPFILES);
130 df->fWide = 1;
131 TCHAR* pFilenames = (TCHAR*)((BYTE*)(pBuffer.get()) + sizeof(DROPFILES));
132 TCHAR* pCurrentFilename = pFilenames;
134 for (auto it = sortedpaths.cbegin(); it != sortedpaths.cend(); ++it)
136 wcscpy_s(pCurrentFilename, it->size() + 1, it->c_str());
137 pCurrentFilename += it->size();
138 *pCurrentFilename = '\0'; // separator between file names
139 pCurrentFilename++;
141 *pCurrentFilename = '\0'; // terminate array
142 pCurrentFilename++;
143 *pCurrentFilename = '\0'; // terminate array
144 STGMEDIUM medium = { 0 };
145 medium.tymed = TYMED_HGLOBAL;
146 medium.hGlobal = GlobalAlloc(GMEM_ZEROINIT | GMEM_MOVEABLE, nBufferSize + 20);
147 if (medium.hGlobal)
149 LPVOID pMem = ::GlobalLock(medium.hGlobal);
150 if (pMem)
152 memcpy(pMem, pBuffer.get(), nBufferSize);
153 GlobalUnlock(medium.hGlobal);
154 FORMATETC formatetc = { 0 };
155 formatetc.cfFormat = CF_HDROP;
156 formatetc.dwAspect = DVASPECT_CONTENT;
157 formatetc.lindex = -1;
158 formatetc.tymed = TYMED_HGLOBAL;
159 medium.pUnkForRelease = nullptr;
160 hres = idata->SetData(&formatetc, &medium, TRUE);
161 return hres;
164 return E_OUTOFMEMORY;
166 else
168 // just pass it on to the base object
169 return m_iSF->GetUIObjectOf(hwndOwner, cidl, apidl, riid, rgfReserved, ppv);
173 IContextMenu2 * g_IContext2 = nullptr;
174 IContextMenu3 * g_IContext3 = nullptr;
175 CIShellFolderHook * g_pFolderhook = nullptr;
176 IShellFolder * g_psfDesktopFolder = nullptr;
177 LPITEMIDLIST * g_pidlArray = nullptr;
178 int g_pidlArrayItems = 0;
180 #define SHELL_MIN_CMD 10000
181 #define SHELL_MAX_CMD 20000
183 HRESULT CALLBACK dfmCallback(IShellFolder * /*psf*/, HWND /*hwnd*/, IDataObject * /*pdtobj*/, UINT uMsg, WPARAM /*wParam*/, LPARAM /*lParam*/)
185 switch (uMsg)
187 case DFM_MERGECONTEXTMENU:
188 return S_OK;
189 case DFM_INVOKECOMMAND:
190 case DFM_INVOKECOMMANDEX:
191 case DFM_GETDEFSTATICID: // Required for Windows 7 to pick a default
192 return S_FALSE;
194 return E_NOTIMPL;
197 BEGIN_MESSAGE_MAP(CGitStatusListCtrl, CResizableColumnsListCtrl<CListCtrl>)
198 ON_NOTIFY(HDN_ITEMCLICKA, 0, OnHdnItemclick)
199 ON_NOTIFY(HDN_ITEMCLICKW, 0, OnHdnItemclick)
200 ON_NOTIFY_REFLECT_EX(LVN_ITEMCHANGED, OnLvnItemchanged)
201 ON_WM_CONTEXTMENU()
202 ON_NOTIFY_REFLECT(NM_DBLCLK, OnNMDblclk)
203 ON_NOTIFY_REFLECT(LVN_GETINFOTIP, OnLvnGetInfoTip)
204 ON_NOTIFY_REFLECT(NM_CUSTOMDRAW, OnNMCustomdraw)
205 ON_NOTIFY_REFLECT(LVN_GETDISPINFO, OnLvnGetdispinfo)
206 ON_WM_SETCURSOR()
207 ON_WM_GETDLGCODE()
208 ON_NOTIFY_REFLECT(NM_RETURN, OnNMReturn)
209 ON_WM_KEYDOWN()
210 ON_WM_PAINT()
211 ON_WM_SYSCOLORCHANGE()
212 ON_NOTIFY_REFLECT(LVN_BEGINDRAG, OnBeginDrag)
213 END_MESSAGE_MAP()
215 CGitStatusListCtrl::CGitStatusListCtrl() : CResizableColumnsListCtrl<CListCtrl>()
216 , m_pbCanceled(nullptr)
217 , m_pStatLabel(nullptr)
218 , m_pSelectButton(nullptr)
219 , m_pConfirmButton(nullptr)
220 , m_bBusy(false)
221 , m_bWaitCursor(false)
222 , m_bEmpty(false)
223 , m_bShowIgnores(false)
224 , m_bIgnoreRemoveOnly(false)
225 , m_bCheckChildrenWithParent(false)
226 , m_bUnversionedLast(true)
227 , m_bHasChangeLists(false)
228 , m_bHasCheckboxes(false)
229 , m_bCheckIfGroupsExist(true)
230 , m_bFileDropsEnabled(false)
231 , m_bOwnDrag(false)
232 , m_dwDefaultColumns(0)
233 , m_bAscending(false)
234 , m_nSortedColumn(-1)
235 , m_bHasExternalsFromDifferentRepos(false)
236 , m_amend(false)
237 , m_bDoNotAutoselectSubmodules(false)
238 , m_bNoAutoselectMissing(false)
239 , m_bHasWC(true)
240 , m_hwndLogicalParent(nullptr)
241 , m_bHasUnversionedItems(FALSE)
242 , m_nTargetCount(0)
243 , m_bHasExternals(false)
244 , m_bHasIgnoreGroup(false)
245 , m_nUnversioned(0)
246 , m_nNormal(0)
247 , m_nModified(0)
248 , m_nAdded(0)
249 , m_nDeleted(0)
250 , m_nConflicted(0)
251 , m_nTotal(0)
252 , m_nSelected(0)
253 , m_nRenamed(0)
254 , m_nShownUnversioned(0)
255 , m_nShownModified(0)
256 , m_nShownAdded(0)
257 , m_nShownDeleted(0)
258 , m_nShownConflicted(0)
259 , m_nShownFiles(0)
260 , m_nShownSubmodules(0)
261 , m_dwShow(0)
262 , m_bShowFolders(false)
263 , m_bUpdate(false)
264 , m_dwContextMenus(0)
265 , m_nIconFolder(0)
266 , m_nRestoreOvl(0)
267 , m_pContextMenu(nullptr)
268 , m_hShellMenu(nullptr)
269 , m_nBackgroundImageID(0)
270 , m_FileLoaded(0)
271 , m_bIsRevertTheirMy(false)
272 , m_nLineAdded(0)
273 , m_nLineDeleted(0)
274 , m_nBlockItemChangeHandler(0)
276 m_critSec.Init();
277 m_bNoAutoselectMissing = CRegDWORD(L"Software\\TortoiseGit\\AutoselectMissingFiles", FALSE) == TRUE;
280 CGitStatusListCtrl::~CGitStatusListCtrl()
282 ClearStatusArray();
285 void CGitStatusListCtrl::ClearStatusArray()
287 #if 0
288 CAutoWriteLock locker(m_guard);
289 for (size_t i = 0; i < m_arStatusArray.size(); ++i)
291 delete m_arStatusArray[i];
293 m_arStatusArray.clear();
294 #endif
297 void CGitStatusListCtrl::Init(DWORD dwColumns, const CString& sColumnInfoContainer, unsigned __int64 dwContextMenus /* = GitSLC_POPALL */, bool bHasCheckboxes /* = true */, bool bHasWC /* = true */, DWORD allowedColumns /* = 0xffffffff */)
299 CAutoWriteLock locker(m_guard);
301 m_dwDefaultColumns = dwColumns | 1;
302 m_dwContextMenus = dwContextMenus;
303 m_bHasCheckboxes = bHasCheckboxes;
304 m_bHasWC = bHasWC;
305 m_bWaitCursor = true;
307 // set the extended style of the listcontrol
308 DWORD exStyle = LVS_EX_DOUBLEBUFFER | LVS_EX_INFOTIP | LVS_EX_SUBITEMIMAGES;
309 exStyle |= (bHasCheckboxes ? LVS_EX_CHECKBOXES : 0);
310 SetRedraw(false);
311 SetExtendedStyle(exStyle);
312 CResizableColumnsListCtrl::Init();
314 SetWindowTheme(m_hWnd, L"Explorer", nullptr);
316 m_nIconFolder = SYS_IMAGE_LIST().GetDirIconIndex();
317 m_nRestoreOvl = SYS_IMAGE_LIST().AddIcon((HICON)LoadImage(AfxGetResourceHandle(), MAKEINTRESOURCE(IDI_RESTOREOVL), IMAGE_ICON, 0, 0, LR_DEFAULTSIZE));
318 SYS_IMAGE_LIST().SetOverlayImage(m_nRestoreOvl, OVL_RESTORE);
319 SetImageList(&SYS_IMAGE_LIST(), LVSIL_SMALL);
321 // keep CSorter::operator() in sync!!
322 static UINT standardColumnNames[GITSLC_NUMCOLUMNS]
323 = { IDS_STATUSLIST_COLFILE
324 , IDS_STATUSLIST_COLFILENAME
325 , IDS_STATUSLIST_COLEXT
326 , IDS_STATUSLIST_COLSTATUS
327 , IDS_STATUSLIST_COLADD
328 , IDS_STATUSLIST_COLDEL
329 , IDS_STATUSLIST_COLLASTMODIFIED
330 , IDS_STATUSLIST_COLSIZE
333 m_ColumnManager.SetNames(standardColumnNames,GITSLC_NUMCOLUMNS);
334 m_ColumnManager.ReadSettings(m_dwDefaultColumns, 0xffffffff & ~(allowedColumns | m_dwDefaultColumns), sColumnInfoContainer, GITSLC_NUMCOLUMNS);
335 m_ColumnManager.SetRightAlign(4);
336 m_ColumnManager.SetRightAlign(5);
337 m_ColumnManager.SetRightAlign(7);
339 // enable file drops
340 if (!m_pDropTarget)
342 m_pDropTarget = std::make_unique<CGitStatusListCtrlDropTarget>(this);
343 RegisterDragDrop(m_hWnd, m_pDropTarget.get());
344 // create the supported formats:
345 FORMATETC ftetc = { 0 };
346 ftetc.dwAspect = DVASPECT_CONTENT;
347 ftetc.lindex = -1;
348 ftetc.tymed = TYMED_HGLOBAL;
349 ftetc.cfFormat = CF_HDROP;
350 m_pDropTarget->AddSuportedFormat(ftetc);
353 SetRedraw(true);
354 m_bWaitCursor = false;
357 bool CGitStatusListCtrl::SetBackgroundImage(UINT nID)
359 m_nBackgroundImageID = nID;
360 return CAppUtils::SetListCtrlBackgroundImage(GetSafeHwnd(), nID);
363 BOOL CGitStatusListCtrl::GetStatus ( const CTGitPathList* pathList
364 , bool bUpdate /* = FALSE */
365 , bool bShowIgnores /* = false */
366 , bool bShowUnRev /* = false */
367 , bool bShowLocalChangesIgnored /* = false */)
369 CAutoWriteLock locker(m_guard);
371 m_bEmpty = false;
372 m_bBusy = true;
373 m_bWaitCursor = true;
374 Invalidate();
376 m_bIsRevertTheirMy = g_Git.IsRebaseRunning() > 0;
378 int mask= CGitStatusListCtrl::FILELIST_MODIFY;
379 if(bShowIgnores)
380 mask|= CGitStatusListCtrl::FILELIST_IGNORE;
381 if(bShowUnRev)
382 mask|= CGitStatusListCtrl::FILELIST_UNVER;
383 if (bShowLocalChangesIgnored)
384 mask |= CGitStatusListCtrl::FILELIST_LOCALCHANGESIGNORED;
385 this->UpdateFileList(mask, bUpdate, pathList);
387 if (pathList && m_setDirectFiles.empty())
389 // remember files which are selected by users so that those can be preselected
390 for (int i = 0; i < pathList->GetCount(); ++i)
391 if (!(*pathList)[i].IsDirectory())
392 m_setDirectFiles.insert((*pathList)[i].GetGitPathString());
395 #if 0
396 int refetchcounter = 0;
397 BOOL bRet = TRUE;
398 Invalidate();
399 // force the cursor to change
400 POINT pt;
401 GetCursorPos(&pt);
402 SetCursorPos(pt.x, pt.y);
404 m_mapFilenameToChecked.clear();
405 //m_StatusUrlList.Clear();
406 bool bHasChangelists = (m_changelists.size() > 1 || (!m_changelists.empty() && !m_bHasIgnoreGroup));
407 m_changelists.clear();
408 for (size_t i=0; i < m_arStatusArray.size(); i++)
410 FileEntry * entry = m_arStatusArray[i];
411 if ( bHasChangelists && entry->checked)
413 // If change lists are present, remember all checked entries
414 CString path = entry->GetPath().GetGitPathString();
415 m_mapFilenameToChecked[path] = true;
417 if ( (entry->status==git_wc_status_unversioned || entry->status==git_wc_status_missing ) && entry->checked )
419 // The user manually selected an unversioned or missing file. We remember
420 // this so that the selection can be restored when refreshing.
421 CString path = entry->GetPath().GetGitPathString();
422 m_mapFilenameToChecked[path] = true;
424 else if ( entry->status > git_wc_status_normal && !entry->checked )
426 // The user manually deselected a versioned file. We remember
427 // this so that the deselection can be restored when refreshing.
428 CString path = entry->GetPath().GetGitPathString();
429 m_mapFilenameToChecked[path] = false;
433 // use a sorted path list to make sure we fetch the status of
434 // parent items first, *then* the child items (if any)
435 CTGitPathList sortedPathList = pathList;
436 sortedPathList.SortByPathname();
439 bRet = TRUE;
440 m_nTargetCount = 0;
441 m_bHasExternalsFromDifferentRepos = FALSE;
442 m_bHasExternals = FALSE;
443 m_bHasUnversionedItems = FALSE;
444 m_bHasChangeLists = false;
445 m_bShowIgnores = bShowIgnores;
446 m_nSortedColumn = 0;
447 m_bBlock = TRUE;
448 m_bBusy = true;
449 m_bEmpty = false;
450 Invalidate();
452 // first clear possible status data left from
453 // previous GetStatus() calls
454 ClearStatusArray();
456 m_StatusFileList = sortedPathList;
458 // Since Git_client_status() returns all files, even those in
459 // folders included with Git:externals we need to check if all
460 // files we get here belongs to the same repository.
461 // It is possible to commit changes in an external folder, as long
462 // as the folder belongs to the same repository (but another path),
463 // but it is not possible to commit all files if the externals are
464 // from a different repository.
466 // To check if all files belong to the same repository, we compare the
467 // UUID's - if they're identical then the files belong to the same
468 // repository and can be committed. But if they're different, then
469 // tell the user to commit all changes in the external folders
470 // first and exit.
471 CStringA sUUID; // holds the repo UUID
472 CTGitPathList arExtPaths; // list of Git:external paths
474 GitConfig config;
476 m_sURL.Empty();
478 m_nTargetCount = sortedPathList.GetCount();
480 GitStatus status(m_pbCanceled);
481 if(m_nTargetCount > 1 && sortedPathList.AreAllPathsFilesInOneDirectory())
483 // This is a special case, where we've been given a list of files
484 // all from one folder
485 // We handle them by setting a status filter, then requesting the Git status of
486 // all the files in the directory, filtering for the ones we're interested in
487 status.SetFilter(sortedPathList);
489 // if all selected entries are files, we don't do a recursive status
490 // fetching. But if only one is a directory, we have to recurse.
491 git_depth_t depth = git_depth_files;
492 // We have set a filter. If all selected items were files or we fetch
493 // the status not recursively, then we have to include
494 // ignored items too - the user has selected them
495 bool bShowIgnoresRight = true;
496 for (int fcindex=0; fcindex<sortedPathList.GetCount(); ++fcindex)
498 if (sortedPathList[fcindex].IsDirectory())
500 depth = git_depth_infinity;
501 bShowIgnoresRight = false;
502 break;
505 if(!FetchStatusForSingleTarget(config, status, sortedPathList.GetCommonDirectory(), bUpdate, sUUID, arExtPaths, true, depth, bShowIgnoresRight))
506 bRet = FALSE;
508 else
510 for(int nTarget = 0; nTarget < m_nTargetCount; nTarget++)
512 // check whether the path we want the status for is already fetched due to status-fetching
513 // of a parent path.
514 // this check is only done for file paths, because folder paths could be included already
515 // but not recursively
516 if (sortedPathList[nTarget].IsDirectory() || !GetListEntry(sortedPathList[nTarget]))
518 if(!FetchStatusForSingleTarget(config, status, sortedPathList[nTarget], bUpdate, sUUID,
519 arExtPaths, false, git_depth_infinity, bShowIgnores))
521 bRet = FALSE;
527 // remove the 'helper' files of conflicted items from the list.
528 // otherwise they would appear as unversioned files.
529 for (INT_PTR cind = 0; cind < m_ConflictFileList.GetCount(); ++cind)
531 for (size_t i=0; i < m_arStatusArray.size(); i++)
533 if (m_arStatusArray[i]->GetPath().IsEquivalentTo(m_ConflictFileList[cind]))
535 delete m_arStatusArray[i];
536 m_arStatusArray.erase(m_arStatusArray.cbegin() + i);
537 break;
541 refetchcounter++;
542 } while(!BuildStatistics() && (refetchcounter < 2) && (*m_pbCanceled==false));
544 m_bBlock = FALSE;
545 m_bBusy = false;
546 GetCursorPos(&pt);
547 SetCursorPos(pt.x, pt.y);
548 return bRet;
549 #endif
550 m_bBusy = false;
551 m_bWaitCursor = false;
552 BuildStatistics();
553 return TRUE;
556 // Get the show-flags bitmap value which corresponds to a particular Git status
557 DWORD CGitStatusListCtrl::GetShowFlagsFromGitStatus(git_wc_status_kind status)
559 switch (status)
561 case git_wc_status_none:
562 case git_wc_status_unversioned:
563 return GITSLC_SHOWUNVERSIONED;
564 case git_wc_status_ignored:
565 if (!m_bShowIgnores)
566 return GITSLC_SHOWDIRECTS;
567 return GITSLC_SHOWDIRECTS|GITSLC_SHOWIGNORED;
568 case git_wc_status_normal:
569 return GITSLC_SHOWNORMAL;
570 case git_wc_status_added:
571 return GITSLC_SHOWADDED;
572 case git_wc_status_deleted:
573 return GITSLC_SHOWREMOVED;
574 case git_wc_status_modified:
575 return GITSLC_SHOWMODIFIED;
576 case git_wc_status_conflicted:
577 return GITSLC_SHOWCONFLICTED;
578 default:
579 // we should NEVER get here!
580 ASSERT(FALSE);
581 break;
583 return 0;
586 void CGitStatusListCtrl::Show(unsigned int dwShow, unsigned int dwCheck /*=0*/, bool /*bShowFolders*/ /* = true */,BOOL UpdateStatusList,bool UseStoredCheckStatus)
588 m_bWaitCursor = true;
589 m_bBusy = true;
590 m_bEmpty = false;
591 Invalidate();
593 WORD langID = (WORD)CRegStdDWORD(L"Software\\TortoiseGit\\LanguageID", GetUserDefaultLangID());
594 SetRedraw(FALSE);
596 CAutoWriteLock locker(m_guard);
597 DeleteAllItems();
598 m_nSelected = 0;
600 m_nShownUnversioned = 0;
601 m_nShownModified = 0;
602 m_nShownAdded = 0;
603 m_nShownDeleted = 0;
604 m_nShownConflicted = 0;
605 m_nShownFiles = 0;
606 m_nShownSubmodules = 0;
608 m_dwShow = dwShow;
610 if (UpdateStatusList)
612 m_arStatusArray.clear();
613 for (int i = 0; i < m_StatusFileList.GetCount(); ++i)
614 m_arStatusArray.push_back(&m_StatusFileList[i]);
616 for (int i = 0; i < m_UnRevFileList.GetCount(); ++i)
617 m_arStatusArray.push_back(&m_UnRevFileList[i]);
619 for (int i = 0; i < m_IgnoreFileList.GetCount(); ++i)
620 m_arStatusArray.push_back(&m_IgnoreFileList[i]);
622 PrepareGroups();
623 if (m_nSortedColumn >= 0)
625 CSorter predicate(&m_ColumnManager, m_nSortedColumn, m_bAscending);
626 std::stable_sort(m_arStatusArray.begin(), m_arStatusArray.end(), predicate);
629 int index = 0;
630 for (size_t i = 0; i < m_arStatusArray.size(); ++i)
632 //set default checkbox status
633 auto entry = const_cast<CTGitPath*>(m_arStatusArray[i]);
634 CString path = entry->GetGitPathString();
635 if (!m_mapFilenameToChecked.empty() && m_mapFilenameToChecked.find(path) != m_mapFilenameToChecked.end())
636 entry->m_Checked = m_mapFilenameToChecked[path];
637 else if (!UseStoredCheckStatus)
639 bool autoSelectSubmodules = !(entry->IsDirectory() && m_bDoNotAutoselectSubmodules);
640 if (((entry->m_Action & dwCheck) && !(m_bNoAutoselectMissing && entry->m_Action & CTGitPath::LOGACTIONS_MISSING) || dwShow & GITSLC_SHOWDIRECTFILES && m_setDirectFiles.find(path) != m_setDirectFiles.end()) && autoSelectSubmodules)
641 entry->m_Checked = true;
642 else
643 entry->m_Checked = false;
644 m_mapFilenameToChecked[path] = entry->m_Checked;
647 if (entry->m_Action & dwShow)
649 AddEntry(entry, langID, index);
650 index++;
655 AdjustColumnWidths();
657 SetRedraw(TRUE);
658 GetStatisticsString();
660 CHeaderCtrl * pHeader = GetHeaderCtrl();
661 HDITEM HeaderItem = {0};
662 HeaderItem.mask = HDI_FORMAT;
663 for (int i=0; i<pHeader->GetItemCount(); ++i)
665 pHeader->GetItem(i, &HeaderItem);
666 HeaderItem.fmt &= ~(HDF_SORTDOWN | HDF_SORTUP);
667 pHeader->SetItem(i, &HeaderItem);
669 if (m_nSortedColumn >= 0)
671 pHeader->GetItem(m_nSortedColumn, &HeaderItem);
672 HeaderItem.fmt |= (m_bAscending ? HDF_SORTUP : HDF_SORTDOWN);
673 pHeader->SetItem(m_nSortedColumn, &HeaderItem);
676 RestoreScrollPos();
678 m_bWaitCursor = false;
679 m_bBusy = false;
680 m_bEmpty = (GetItemCount() == 0);
681 Invalidate();
683 this->BuildStatistics();
685 #if 0
687 m_bShowFolders = bShowFolders;
689 int nTopIndex = GetTopIndex();
690 POSITION posSelectedEntry = GetFirstSelectedItemPosition();
691 int nSelectedEntry = 0;
692 if (posSelectedEntry)
693 nSelectedEntry = GetNextSelectedItem(posSelectedEntry);
694 SetRedraw(FALSE);
695 DeleteAllItems();
697 PrepareGroups();
699 m_arListArray.clear();
701 m_arListArray.reserve(m_arStatusArray.size());
702 SetItemCount (static_cast<int>(m_arStatusArray.size()));
704 int listIndex = 0;
705 for (size_t i=0; i < m_arStatusArray.size(); ++i)
707 FileEntry * entry = m_arStatusArray[i];
708 if ((entry->inexternal) && (!(dwShow & SVNSLC_SHOWINEXTERNALS)))
709 continue;
710 if ((entry->differentrepo || entry->isNested) && (! (dwShow & SVNSLC_SHOWEXTERNALFROMDIFFERENTREPO)))
711 continue;
712 if (entry->IsFolder() && (!bShowFolders))
713 continue; // don't show folders if they're not wanted.
715 #if 0
716 git_wc_status_kind status = GitStatus::GetMoreImportant(entry->status, entry->remotestatus);
717 DWORD showFlags = GetShowFlagsFromGitStatus(status);
718 if (entry->switched)
719 showFlags |= SVNSLC_SHOWSWITCHED;
720 if (!entry->changelist.IsEmpty())
721 showFlags |= SVNSLC_SHOWINCHANGELIST;
722 #endif
723 bool bAllowCheck = ((entry->changelist.Compare(GITSLC_IGNORECHANGELIST) != 0)
724 && (m_bCheckIfGroupsExist || (m_changelists.empty() || (m_changelists.size() == 1 && m_bHasIgnoreGroup))));
726 // status_ignored is a special case - we must have the 'direct' flag set to add a status_ignored item
727 #if 0
728 if (status != Git_wc_status_ignored || (entry->direct) || (dwShow & GitSLC_SHOWIGNORED))
730 if ((!entry->IsFolder()) && (status == Git_wc_status_deleted) && (dwShow & SVNSLC_SHOWREMOVEDANDPRESENT))
732 if (PathFileExists(entry->GetPath().GetWinPath()))
734 m_arListArray.push_back(i);
735 if ((dwCheck & SVNSLC_SHOWREMOVEDANDPRESENT)||((dwCheck & SVNSLC_SHOWDIRECTS)&&(entry->direct)))
737 if (bAllowCheck)
738 entry->checked = true;
740 AddEntry(entry, langID, listIndex++);
743 else if ((dwShow & showFlags)||((dwShow & SVNSLC_SHOWDIRECTFILES)&&(entry->direct)&&(!entry->IsFolder())))
745 m_arListArray.push_back(i);
746 if ((dwCheck & showFlags)||((dwCheck & SVNSLC_SHOWDIRECTS)&&(entry->direct)))
748 if (bAllowCheck)
749 entry->checked = true;
751 AddEntry(entry, langID, listIndex++);
753 else if ((dwShow & showFlags)||((dwShow & SVNSLC_SHOWDIRECTFOLDER)&&(entry->direct)&&entry->IsFolder()))
755 m_arListArray.push_back(i);
756 if ((dwCheck & showFlags)||((dwCheck & SVNSLC_SHOWDIRECTS)&&(entry->direct)))
758 if (bAllowCheck)
759 entry->checked = true;
761 AddEntry(entry, langID, listIndex++);
764 #endif
767 SetItemCount(listIndex);
769 m_ColumnManager.UpdateRelevance (m_arStatusArray, m_arListArray);
771 AdjustColumnWidths();
773 SetRedraw(TRUE);
774 GetStatisticsString();
776 CHeaderCtrl * pHeader = GetHeaderCtrl();
777 HDITEM HeaderItem = {0};
778 HeaderItem.mask = HDI_FORMAT;
779 for (int i=0; i<pHeader->GetItemCount(); ++i)
781 pHeader->GetItem(i, &HeaderItem);
782 HeaderItem.fmt &= ~(HDF_SORTDOWN | HDF_SORTUP);
783 pHeader->SetItem(i, &HeaderItem);
785 if (m_nSortedColumn)
787 pHeader->GetItem(m_nSortedColumn, &HeaderItem);
788 HeaderItem.fmt |= (m_bAscending ? HDF_SORTUP : HDF_SORTDOWN);
789 pHeader->SetItem(m_nSortedColumn, &HeaderItem);
792 if (nSelectedEntry)
794 SetItemState(nSelectedEntry, LVIS_SELECTED, LVIS_SELECTED);
795 EnsureVisible(nSelectedEntry, false);
797 else
799 // Restore the item at the top of the list.
800 for (int i=0;GetTopIndex() != nTopIndex;i++)
802 if ( !EnsureVisible(nTopIndex+i,false) )
803 break;
807 m_bEmpty = (GetItemCount() == 0);
808 Invalidate();
809 #endif
812 void CGitStatusListCtrl::StoreScrollPos()
814 m_sScrollPos.enabled = true;
815 m_sScrollPos.nTopIndex = GetTopIndex();
816 m_sScrollPos.selMark = GetSelectionMark();
817 POSITION posSelectedEntry = GetFirstSelectedItemPosition();
818 m_sScrollPos.nSelectedEntry = 0;
819 if (posSelectedEntry)
820 m_sScrollPos.nSelectedEntry = GetNextSelectedItem(posSelectedEntry);
823 void CGitStatusListCtrl::RestoreScrollPos()
825 if (!m_sScrollPos.enabled || CRegDWORD(L"Software\\TortoiseGit\\RememberFileListPosition", TRUE) != TRUE)
826 return;
828 if (m_sScrollPos.nSelectedEntry)
830 SetItemState(m_sScrollPos.nSelectedEntry, LVIS_SELECTED, LVIS_SELECTED);
831 EnsureVisible(m_sScrollPos.nSelectedEntry, false);
833 else
835 // Restore the item at the top of the list.
836 for (int i = 0; GetTopIndex() != m_sScrollPos.nTopIndex; ++i)
838 if (!EnsureVisible(m_sScrollPos.nTopIndex + i, false))
839 break;
842 if (m_sScrollPos.selMark >= 0)
844 SetSelectionMark(m_sScrollPos.selMark);
845 SetItemState(m_sScrollPos.selMark, LVIS_FOCUSED, LVIS_FOCUSED);
847 m_sScrollPos.enabled = false;
850 int CGitStatusListCtrl::GetColumnIndex(int mask)
852 for (int i = 0; i < 32; ++i)
853 if(mask&0x1)
854 return i;
855 else
856 mask=mask>>1;
857 return -1;
860 CString CGitStatusListCtrl::GetCellText(int listIndex, int column)
862 static CString from(MAKEINTRESOURCE(IDS_STATUSLIST_FROM));
863 static bool abbreviateRenamings(((DWORD)CRegDWORD(L"Software\\TortoiseGit\\AbbreviateRenamings", FALSE)) == TRUE);
864 static bool relativeTimes = (CRegDWORD(L"Software\\TortoiseGit\\RelativeTimes", FALSE) != FALSE);
865 static const CString empty;
867 CAutoReadLock locker(m_guard);
868 const auto* entry = GetListEntry(listIndex);
869 if (!entry)
870 return empty;
872 switch (column)
874 case 0: // relative path
875 if (!(entry->m_Action & (CTGitPath::LOGACTIONS_REPLACED | CTGitPath::LOGACTIONS_COPY) && !entry->GetGitOldPathString().IsEmpty()))
876 return entry->GetGitPathString();
878 if (!abbreviateRenamings)
880 CString entryname = entry->GetGitPathString();
881 entryname += L' ';
882 // relative path
883 entryname.AppendFormat(from, (LPCTSTR)entry->GetGitOldPathString());
884 return entryname;
887 return entry->GetAbbreviatedRename();
889 case 1: // GITSLC_COLFILENAME
890 return entry->GetFileOrDirectoryName();
892 case 2: // GITSLC_COLEXT
893 return entry->GetFileExtension();
895 case 3: // GITSLC_COLSTATUS
896 return entry->GetActionName();
898 case 4: // GITSLC_COLADD
899 return entry->m_StatAdd;
901 case 5: // GITSLC_COLDEL
902 return entry->m_StatDel;
904 case 6: // GITSLC_COLMODIFICATIONDATE
905 if (!(entry->m_Action & CTGitPath::LOGACTIONS_DELETED) && m_ColumnManager.IsRelevant(GetColumnIndex(GITSLC_COLMODIFICATIONDATE)))
907 CString modificationDate;
908 __int64 filetime = entry->GetLastWriteTime();
909 if (filetime)
911 FILETIME* f = (FILETIME*)(__int64*)&filetime;
912 modificationDate = CLoglistUtils::FormatDateAndTime(CTime(CGit::filetime_to_time_t(f)), DATE_SHORTDATE, true, relativeTimes);
914 return modificationDate;
916 return empty;
918 case 7: // GITSLC_COLSIZE
919 if (!(entry->IsDirectory() || !m_ColumnManager.IsRelevant(GetColumnIndex(GITSLC_COLSIZE))))
921 TCHAR buf[100] = { 0 };
922 StrFormatByteSize64(entry->GetFileSize(), buf, 100);
923 return buf;
925 return empty;
927 #if 0
928 default: // user-defined properties
929 if (column < m_ColumnManager.GetColumnCount())
931 assert(m_ColumnManager.IsUserProp(column));
933 const CString& name = m_ColumnManager.GetName(column);
934 auto propEntry = m_PropertyMap.find(entry->GetPath());
935 if (propEntry != m_PropertyMap.end())
937 if (propEntry->second.HasProperty(name))
939 const CString& propVal = propEntry->second[name];
940 return propVal.IsEmpty()
941 ? m_sNoPropValueText
942 : propVal;
946 #endif
948 return empty;
951 void CGitStatusListCtrl::AddEntry(CTGitPath * GitPath, WORD /*langID*/, int listIndex)
953 CAutoWriteLock locker(m_guard);
954 ScopedInDecrement blocker(m_nBlockItemChangeHandler);
955 CString path = GitPath->GetGitPathString();
957 int index = listIndex;
958 // Load the icons *now* so the icons are cached when showing them later in the
959 // WM_PAINT handler.
960 // Problem is that (at least on Win10), loading the icons in the WM_PAINT
961 // handler triggers an OLE operation, which should not happen in WM_PAINT at all
962 // (see ..\VC\atlmfc\src\mfc\olemsgf.cpp, COleMessageFilter::OnMessagePending() for details about this)
963 // By loading the icons here, they get cached and the OLE operation won't happen
964 // later in the WM_PAINT handler.
965 // This solves the 'hang' which happens in the commit dialog if images are
966 // shown in the file list.
967 int icon_idx = 0;
968 if (GitPath->IsDirectory())
970 icon_idx = m_nIconFolder;
971 m_nShownSubmodules++;
973 else
975 icon_idx = SYS_IMAGE_LIST().GetPathIconIndex(*GitPath);
976 m_nShownFiles++;
978 switch (GitPath->m_Action)
980 case CTGitPath::LOGACTIONS_ADDED:
981 case CTGitPath::LOGACTIONS_COPY:
982 m_nShownAdded++;
983 break;
984 case CTGitPath::LOGACTIONS_DELETED:
985 case CTGitPath::LOGACTIONS_MISSING:
986 case CTGitPath::LOGACTIONS_DELETED | CTGitPath::LOGACTIONS_MISSING:
987 m_nShownDeleted++;
988 break;
989 case CTGitPath::LOGACTIONS_REPLACED:
990 case CTGitPath::LOGACTIONS_MODIFIED:
991 case CTGitPath::LOGACTIONS_MERGED:
992 m_nShownModified++;
993 break;
994 case CTGitPath::LOGACTIONS_UNMERGED:
995 case CTGitPath::LOGACTIONS_UNMERGED | CTGitPath::LOGACTIONS_ADDED:
996 case CTGitPath::LOGACTIONS_UNMERGED | CTGitPath::LOGACTIONS_MODIFIED:
997 m_nShownConflicted++;
998 break;
999 case CTGitPath::LOGACTIONS_UNVER:
1000 m_nShownUnversioned++;
1001 break;
1002 default:
1003 m_nShownUnversioned++;
1004 break;
1007 LVITEM lvItem = { 0 };
1008 lvItem.iItem = listIndex;
1009 lvItem.lParam = (LPARAM)GitPath;
1010 lvItem.mask = LVIF_TEXT | LVIF_IMAGE | LVIF_STATE | LVIF_PARAM;
1011 lvItem.pszText = LPSTR_TEXTCALLBACK;
1012 lvItem.stateMask = LVIS_OVERLAYMASK;
1013 if (m_restorepaths.find(GitPath->GetWinPathString()) != m_restorepaths.end())
1014 lvItem.state = INDEXTOOVERLAYMASK(OVL_RESTORE);
1015 lvItem.iImage = icon_idx;
1016 InsertItem(&lvItem);
1018 SetCheck(index, GitPath->m_Checked);
1019 if (GitPath->m_Checked)
1020 m_nSelected++;
1022 if ((GitPath->m_Action & CTGitPath::LOGACTIONS_SKIPWORKTREE) || (GitPath->m_Action & CTGitPath::LOGACTIONS_ASSUMEVALID))
1023 SetItemGroup(index, 3);
1024 else if (GitPath->m_Action & CTGitPath::LOGACTIONS_IGNORE)
1025 SetItemGroup(index, 2);
1026 else if( GitPath->m_Action & CTGitPath::LOGACTIONS_UNVER)
1027 SetItemGroup(index,1);
1028 else
1029 SetItemGroup(index, GitPath->m_ParentNo&(PARENT_MASK|MERGE_MASK));
1032 bool CGitStatusListCtrl::SetItemGroup(int item, int groupindex)
1034 CAutoWriteLock locker(m_guard);
1035 // if (!(m_dwContextMenus & SVNSLC_POPCHANGELISTS))
1036 // return false;
1037 if (groupindex < 0)
1038 return false;
1039 LVITEM i = {0};
1040 i.mask = LVIF_GROUPID;
1041 i.iItem = item;
1042 i.iSubItem = 0;
1043 i.iGroupId = groupindex;
1045 return !!SetItem(&i);
1048 void CGitStatusListCtrl::OnHdnItemclick(NMHDR *pNMHDR, LRESULT *pResult)
1050 LPNMHEADER phdr = reinterpret_cast<LPNMHEADER>(pNMHDR);
1051 *pResult = 0;
1053 CAutoReadWeakLock lock(m_guard);
1054 if (!lock.IsAcquired())
1055 return;
1057 if (m_arStatusArray.empty())
1058 return;
1060 if (m_nSortedColumn == phdr->iItem)
1061 m_bAscending = !m_bAscending;
1062 else
1063 m_bAscending = TRUE;
1064 m_nSortedColumn = phdr->iItem;
1065 Show(m_dwShow, 0, m_bShowFolders,false,true);
1068 BOOL CGitStatusListCtrl::OnLvnItemchanged(NMHDR *pNMHDR, LRESULT *pResult)
1070 LPNMLISTVIEW pNMLV = reinterpret_cast<LPNMLISTVIEW>(pNMHDR);
1071 *pResult = 0;
1072 CWnd* pParent = GetLogicalParent();
1073 if (pParent && pParent->GetSafeHwnd())
1074 pParent->SendMessage(GITSLNM_ITEMCHANGED, pNMLV->iItem);
1076 if (m_nBlockItemChangeHandler)
1077 return FALSE;
1079 if ((pNMLV->uNewState==0)||(pNMLV->uNewState & LVIS_SELECTED)||(pNMLV->uNewState & LVIS_FOCUSED))
1080 return FALSE;
1082 CAutoWriteWeakLock writeLock(m_guard);
1083 if (!writeLock.IsAcquired())
1085 NotifyCheck();
1086 return FALSE;
1089 bool bSelected = !!(ListView_GetItemState(m_hWnd, pNMLV->iItem, LVIS_SELECTED) & LVIS_SELECTED);
1090 int nListItems = GetItemCount();
1092 // was the item checked?
1094 if (GetCheck(pNMLV->iItem))
1097 ScopedInDecrement blocker(m_nBlockItemChangeHandler);
1098 CheckEntry(pNMLV->iItem, nListItems);
1100 if (bSelected)
1102 ScopedInDecrement blocker(m_nBlockItemChangeHandler);
1103 POSITION pos = GetFirstSelectedItemPosition();
1104 int index;
1105 while ((index = GetNextSelectedItem(pos)) >= 0)
1107 if (index != pNMLV->iItem)
1108 CheckEntry(index, nListItems);
1112 else
1115 ScopedInDecrement blocker(m_nBlockItemChangeHandler);
1116 UncheckEntry(pNMLV->iItem, nListItems);
1118 if (bSelected)
1120 ScopedInDecrement blocker(m_nBlockItemChangeHandler);
1121 POSITION pos = GetFirstSelectedItemPosition();
1122 int index;
1123 while ((index = GetNextSelectedItem(pos)) >= 0)
1125 if (index != pNMLV->iItem)
1126 UncheckEntry(index, nListItems);
1131 GetStatisticsString();
1132 NotifyCheck();
1134 return FALSE;
1137 void CGitStatusListCtrl::CheckEntry(int index, int /*nListItems*/)
1139 CAutoWriteLock locker(m_guard);
1140 auto path = GetListEntry(index);
1141 if (!path)
1142 return;
1143 m_mapFilenameToChecked[path->GetGitPathString()] = true;
1144 SetCheck(index, TRUE);
1145 // if an unversioned item was checked, then we need to check if
1146 // the parent folders are unversioned too. If the parent folders actually
1147 // are unversioned, then check those too.
1148 #if 0
1149 if (entry->status == git_wc_status_unversioned)
1151 // we need to check the parent folder too
1152 const CTGitPath& folderpath = entry->path;
1153 for (int i=0; i< nListItems; ++i)
1155 FileEntry * testEntry = GetListEntry(i);
1156 ASSERT(testEntry);
1157 if (!testEntry)
1158 continue;
1159 if (!testEntry->checked)
1161 if (testEntry->path.IsAncestorOf(folderpath) && (!testEntry->path.IsEquivalentTo(folderpath)))
1163 SetEntryCheck(testEntry,i,true);
1164 m_nSelected++;
1169 bool bShift = !!(GetAsyncKeyState(VK_SHIFT) & 0x8000);
1170 if ( (entry->status == git_wc_status_deleted) || (m_bCheckChildrenWithParent) || (bShift) )
1172 // if a deleted folder gets checked, we have to check all
1173 // children of that folder too.
1174 if (entry->path.IsDirectory())
1175 SetCheckOnAllDescendentsOf(entry, true);
1177 // if a deleted file or folder gets checked, we have to
1178 // check all parents of this item too.
1179 for (int i=0; i<nListItems; ++i)
1181 FileEntry * testEntry = GetListEntry(i);
1182 ASSERT(testEntry);
1183 if (!testEntry)
1184 continue;
1185 if (!testEntry->checked)
1187 if (testEntry->path.IsAncestorOf(entry->path) && (!testEntry->path.IsEquivalentTo(entry->path)))
1189 if ((testEntry->status == git_wc_status_deleted)||(m_bCheckChildrenWithParent))
1191 SetEntryCheck(testEntry,i,true);
1192 m_nSelected++;
1193 // now we need to check all children of this parent folder
1194 SetCheckOnAllDescendentsOf(testEntry, true);
1200 #endif
1201 if ( !path->m_Checked )
1203 path->m_Checked = TRUE;
1204 m_nSelected++;
1208 void CGitStatusListCtrl::UncheckEntry(int index, int /*nListItems*/)
1210 CAutoWriteLock locker(m_guard);
1211 auto path = GetListEntry(index);
1212 if (!path)
1213 return;
1214 SetCheck(index, FALSE);
1215 m_mapFilenameToChecked[path->GetGitPathString()] = false;
1216 // item was unchecked
1217 #if 0
1218 if (entry->path.IsDirectory())
1220 // disable all files within an unselected folder, except when unchecking a folder with property changes
1221 bool bShift = !!(GetAsyncKeyState(VK_SHIFT) & 0x8000);
1222 if ( (entry->status != git_wc_status_modified) || (bShift) )
1224 SetCheckOnAllDescendentsOf(entry, false);
1227 else if (entry->status == git_wc_status_deleted)
1229 // a "deleted" file was unchecked, so uncheck all parent folders
1230 // and all children of those parents
1231 for (int i=0; i<nListItems; i++)
1233 FileEntry * testEntry = GetListEntry(i);
1234 ASSERT(testEntry);
1235 if (!testEntry)
1236 continue;
1237 if (testEntry->checked)
1239 if (testEntry->path.IsAncestorOf(entry->path))
1241 if (testEntry->status == git_wc_status_deleted)
1243 SetEntryCheck(testEntry,i,false);
1244 m_nSelected--;
1246 SetCheckOnAllDescendentsOf(testEntry, false);
1252 #endif
1253 if ( path->m_Checked )
1255 path->m_Checked = FALSE;
1256 m_nSelected--;
1259 void CGitStatusListCtrl::BuildStatistics()
1261 CAutoReadLock locker(m_guard);
1263 // now gather some statistics
1264 m_nUnversioned = 0;
1265 m_nNormal = 0;
1266 m_nModified = 0;
1267 m_nAdded = 0;
1268 m_nDeleted = 0;
1269 m_nConflicted = 0;
1270 m_nTotal = 0;
1271 m_nSelected = 0;
1272 m_nLineAdded = 0;
1273 m_nLineDeleted = 0;
1274 m_nRenamed = 0;
1276 for (size_t i = 0; i < m_arStatusArray.size(); ++i)
1278 int status = m_arStatusArray[i]->m_Action;
1280 m_nLineAdded += _wtol(m_arStatusArray[i]->m_StatAdd);
1281 m_nLineDeleted += _wtol(m_arStatusArray[i]->m_StatDel);
1283 if(status&(CTGitPath::LOGACTIONS_ADDED|CTGitPath::LOGACTIONS_COPY))
1284 m_nAdded++;
1286 if(status&CTGitPath::LOGACTIONS_DELETED)
1287 m_nDeleted++;
1289 if(status&(CTGitPath::LOGACTIONS_REPLACED|CTGitPath::LOGACTIONS_MODIFIED))
1290 m_nModified++;
1292 if(status&CTGitPath::LOGACTIONS_UNMERGED)
1293 m_nConflicted++;
1295 if(status&(CTGitPath::LOGACTIONS_IGNORE|CTGitPath::LOGACTIONS_UNVER))
1296 m_nUnversioned++;
1298 if(status&(CTGitPath::LOGACTIONS_REPLACED))
1299 m_nRenamed++;
1301 if (m_arStatusArray[i]->m_Checked)
1302 m_nSelected++;
1306 int CGitStatusListCtrl::GetGroupFromPoint(POINT * ppt)
1308 // the point must be relative to the upper left corner of the control
1310 if (!ppt)
1311 return -1;
1312 if (!IsGroupViewEnabled())
1313 return -1;
1315 POINT pt = *ppt;
1316 pt.x = 10;
1317 UINT flags = 0;
1318 int nItem = -1;
1319 RECT rc;
1320 GetWindowRect(&rc);
1321 while (((flags & LVHT_BELOW) == 0)&&(pt.y < rc.bottom))
1323 nItem = HitTest(pt, &flags);
1324 if ((flags & LVHT_ONITEM)||(flags & LVHT_EX_GROUP_HEADER))
1326 // the first item below the point
1328 // check if the point is too much right (i.e. if the point
1329 // is farther to the right than the width of the item)
1330 RECT r;
1331 GetItemRect(nItem, &r, LVIR_LABEL);
1332 if (ppt->x > r.right)
1333 return -1;
1335 LVITEM lv = {0};
1336 lv.mask = LVIF_GROUPID;
1337 lv.iItem = nItem;
1338 GetItem(&lv);
1339 int groupID = lv.iGroupId;
1340 // now we search upwards and check if the item above this one
1341 // belongs to another group. If it belongs to the same group,
1342 // we're not over a group header
1343 while (pt.y >= 0)
1345 pt.y -= 2;
1346 nItem = HitTest(pt, &flags);
1347 if ((flags & LVHT_ONITEM)&&(nItem >= 0))
1349 // the first item below the point
1350 LVITEM lv2 = {0};
1351 lv2.mask = LVIF_GROUPID;
1352 lv2.iItem = nItem;
1353 GetItem(&lv2);
1354 if (lv2.iGroupId != groupID)
1355 return groupID;
1356 else
1357 return -1;
1360 if (pt.y < 0)
1361 return groupID;
1362 return -1;
1364 pt.y += 2;
1366 return -1;
1369 void CGitStatusListCtrl::OnContextMenuGroup(CWnd * /*pWnd*/, CPoint point)
1371 POINT clientpoint = point;
1372 ScreenToClient(&clientpoint);
1373 if ((IsGroupViewEnabled())&&(GetGroupFromPoint(&clientpoint) >= 0))
1375 CAutoReadWeakLock readLock(m_guard);
1376 if (!readLock.IsAcquired())
1377 return;
1379 CMenu popup;
1380 if (popup.CreatePopupMenu())
1382 CString temp;
1383 temp.LoadString(IDS_STATUSLIST_CHECKGROUP);
1384 popup.AppendMenu(MF_STRING | MF_ENABLED, IDGITLC_CHECKGROUP, temp);
1385 temp.LoadString(IDS_STATUSLIST_UNCHECKGROUP);
1386 popup.AppendMenu(MF_STRING | MF_ENABLED, IDGITLC_UNCHECKGROUP, temp);
1387 int cmd = popup.TrackPopupMenu(TPM_RETURNCMD | TPM_LEFTALIGN | TPM_NONOTIFY, point.x, point.y, this);
1388 bool bCheck = false;
1389 switch (cmd)
1391 case IDGITLC_CHECKGROUP:
1392 bCheck = true;
1393 // fall through here
1394 case IDGITLC_UNCHECKGROUP:
1396 int group = GetGroupFromPoint(&clientpoint);
1397 // go through all items and check/uncheck those assigned to the group
1398 // but block the OnLvnItemChanged handler
1399 for (int i=0; i<GetItemCount(); ++i)
1401 LVITEM lv = { 0 };
1402 lv.mask = LVIF_GROUPID;
1403 lv.iItem = i;
1404 GetItem(&lv);
1406 if (lv.iGroupId == group)
1408 auto entry = GetListEntry(i);
1409 if (entry)
1411 bool bOldCheck = entry->m_Checked;
1412 SetEntryCheck(entry, i, bCheck);
1413 if (bCheck != bOldCheck)
1415 if (bCheck)
1416 m_nSelected++;
1417 else
1418 m_nSelected--;
1424 GetStatisticsString();
1425 NotifyCheck();
1427 break;
1433 void CGitStatusListCtrl::OnContextMenuList(CWnd * pWnd, CPoint point)
1435 bool bShift = !!(GetAsyncKeyState(VK_SHIFT) & 0x8000);
1437 CAutoWriteWeakLock writeLock(m_guard);
1438 if (!writeLock.IsAcquired())
1439 return;
1441 auto selectedCount = GetSelectedCount();
1442 int selIndex = GetSelectionMark();
1443 int selSubitem = -1;
1444 if (selectedCount > 0)
1446 CPoint pt = point;
1447 ScreenToClient(&pt);
1448 LVHITTESTINFO hittest = { 0 };
1449 hittest.flags = LVHT_ONITEM;
1450 hittest.pt = pt;
1451 if (this->SubItemHitTest(&hittest) >= 0)
1452 selSubitem = hittest.iSubItem;
1454 if ((point.x == -1) && (point.y == -1))
1456 CRect rect;
1457 GetItemRect(selIndex, &rect, LVIR_LABEL);
1458 ClientToScreen(&rect);
1459 point = rect.CenterPoint();
1461 if (selectedCount == 0 && m_bHasCheckboxes)
1463 // nothing selected could mean the context menu is requested for
1464 // a group header
1465 OnContextMenuGroup(pWnd, point);
1467 else if (selIndex >= 0)
1469 auto filepath = GetListEntry(selIndex);
1470 if (!filepath)
1471 return;
1473 //const CTGitPath& filepath = entry->path;
1474 int wcStatus = filepath->m_Action;
1475 // entry is selected, now show the popup menu
1476 CIconMenu popup;
1477 CMenu changelistSubMenu;
1478 CMenu ignoreSubMenu;
1479 CIconMenu clipSubMenu;
1480 CMenu shellMenu;
1481 if (popup.CreatePopupMenu())
1483 //Add Menu for version controlled file
1485 if (selectedCount > 0 && (wcStatus & CTGitPath::LOGACTIONS_UNMERGED))
1487 if (selectedCount == 1 && (m_dwContextMenus & GITSLC_POPCONFLICT))
1489 popup.AppendMenuIcon(IDGITLC_EDITCONFLICT, IDS_MENUCONFLICT, IDI_CONFLICT);
1490 popup.SetDefaultItem(IDGITLC_EDITCONFLICT, FALSE);
1492 if (m_dwContextMenus & GITSLC_POPRESOLVE)
1494 popup.AppendMenuIcon(IDGITLC_RESOLVECONFLICT, IDS_STATUSLIST_CONTEXT_RESOLVED, IDI_RESOLVE);
1495 CString tmp, mineTitle, theirsTitle;
1496 CAppUtils::GetConflictTitles(nullptr, mineTitle, theirsTitle, m_bIsRevertTheirMy);
1497 tmp.Format(IDS_SVNPROGRESS_MENURESOLVEUSING, (LPCTSTR)theirsTitle);
1498 if (m_bIsRevertTheirMy)
1500 popup.AppendMenuIcon(IDGITLC_RESOLVEMINE, tmp, IDI_RESOLVE);
1501 tmp.Format(IDS_SVNPROGRESS_MENURESOLVEUSING, (LPCTSTR)mineTitle);
1502 popup.AppendMenuIcon(IDGITLC_RESOLVETHEIRS, tmp, IDI_RESOLVE);
1504 else
1506 popup.AppendMenuIcon(IDGITLC_RESOLVETHEIRS, tmp, IDI_RESOLVE);
1507 tmp.Format(IDS_SVNPROGRESS_MENURESOLVEUSING, (LPCTSTR)mineTitle);
1508 popup.AppendMenuIcon(IDGITLC_RESOLVEMINE, tmp, IDI_RESOLVE);
1511 if ((m_dwContextMenus & GITSLC_POPCONFLICT)||(m_dwContextMenus & GITSLC_POPRESOLVE))
1512 popup.AppendMenu(MF_SEPARATOR);
1515 if (selectedCount > 0)
1517 if (wcStatus & (CTGitPath::LOGACTIONS_UNVER | CTGitPath::LOGACTIONS_IGNORE))
1519 if (m_dwContextMenus & GITSLC_POPADD)
1520 popup.AppendMenuIcon(IDGITLC_ADD, IDS_STATUSLIST_CONTEXT_ADD, IDI_ADD);
1521 if (m_dwContextMenus & GITSLC_POPCOMMIT)
1522 popup.AppendMenuIcon(IDGITLC_COMMIT, IDS_STATUSLIST_CONTEXT_COMMIT, IDI_COMMIT);
1526 if (!(wcStatus & (CTGitPath::LOGACTIONS_UNVER | CTGitPath::LOGACTIONS_IGNORE)) && !((wcStatus & CTGitPath::LOGACTIONS_MISSING) && wcStatus != (CTGitPath::LOGACTIONS_MISSING | CTGitPath::LOGACTIONS_DELETED) && wcStatus != (CTGitPath::LOGACTIONS_MISSING | CTGitPath::LOGACTIONS_DELETED | CTGitPath::LOGACTIONS_MODIFIED)) && selectedCount > 0)
1528 bool bEntryAdded = false;
1529 if (m_dwContextMenus & GITSLC_POPCOMPAREWITHBASE)
1531 if(filepath->m_ParentNo & MERGE_MASK)
1532 popup.AppendMenuIcon(IDGITLC_COMPARE, IDS_TREE_DIFF, IDI_DIFF);
1533 else
1534 popup.AppendMenuIcon(IDGITLC_COMPARE, IDS_LOG_COMPAREWITHBASE, IDI_DIFF);
1536 if (!(wcStatus & (CTGitPath::LOGACTIONS_UNMERGED)) || selectedCount != 1)
1537 popup.SetDefaultItem(IDGITLC_COMPARE, FALSE);
1538 bEntryAdded = true;
1541 if (!g_Git.IsInitRepos() && (m_dwContextMenus & GITSLC_POPGNUDIFF))
1543 popup.AppendMenuIcon(IDGITLC_GNUDIFF1, IDS_LOG_POPUP_GNUDIFF, IDI_DIFF);
1544 bEntryAdded = true;
1547 if ((m_dwContextMenus & this->GetContextMenuBit(IDGITLC_COMPAREWC)) && m_bHasWC)
1549 if ((!m_CurrentVersion.IsEmpty()) && m_CurrentVersion != GIT_REV_ZERO)
1551 popup.AppendMenuIcon(IDGITLC_COMPAREWC, IDS_LOG_POPUP_COMPARE, IDI_DIFF);
1552 bEntryAdded = true;
1556 if (bEntryAdded)
1557 popup.AppendMenu(MF_SEPARATOR);
1560 if (!m_Rev1.IsEmpty() && !m_Rev2.IsEmpty())
1562 if (m_dwContextMenus & this->GetContextMenuBit(IDGITLC_COMPARETWOREVISIONS))
1564 popup.AppendMenuIcon(IDGITLC_COMPARETWOREVISIONS, IDS_LOG_POPUP_COMPARETWO, IDI_DIFF);
1565 popup.SetDefaultItem(IDGITLC_COMPARETWOREVISIONS, FALSE);
1567 if (m_dwContextMenus & this->GetContextMenuBit(IDGITLC_GNUDIFF2REVISIONS))
1568 popup.AppendMenuIcon(IDGITLC_GNUDIFF2REVISIONS, IDS_LOG_POPUP_GNUDIFF, IDI_DIFF);
1571 //Select Multi item
1572 if (selectedCount > 0)
1574 if (selectedCount == 2 && (m_dwContextMenus & GITSLC_POPCOMPARETWOFILES))
1576 POSITION pos = GetFirstSelectedItemPosition();
1577 int index = GetNextSelectedItem(pos);
1578 if (index >= 0)
1580 auto entry2 = GetListEntry(index);
1581 bool firstEntryExistsAndIsFile = entry2 && !entry2->IsDirectory();
1582 index = GetNextSelectedItem(pos);
1583 if (index >= 0)
1585 entry2 = GetListEntry(index);
1586 if (firstEntryExistsAndIsFile && entry2 && !entry2->IsDirectory())
1587 popup.AppendMenuIcon(IDGITLC_COMPARETWOFILES, IDS_STATUSLIST_CONTEXT_COMPARETWOFILES, IDI_DIFF);
1593 if (selectedCount > 0 && (!(wcStatus & (CTGitPath::LOGACTIONS_UNVER | CTGitPath::LOGACTIONS_IGNORE))) && m_bHasWC)
1595 if ((m_dwContextMenus & GITSLC_POPCOMMIT) && (this->m_CurrentVersion.IsEmpty() || this->m_CurrentVersion == GIT_REV_ZERO) && !(wcStatus & (CTGitPath::LOGACTIONS_SKIPWORKTREE | CTGitPath::LOGACTIONS_ASSUMEVALID)))
1596 popup.AppendMenuIcon(IDGITLC_COMMIT, IDS_STATUSLIST_CONTEXT_COMMIT, IDI_COMMIT);
1598 if ((m_dwContextMenus & GITSLC_POPREVERT) && (this->m_CurrentVersion.IsEmpty() || this->m_CurrentVersion == GIT_REV_ZERO))
1599 popup.AppendMenuIcon(IDGITLC_REVERT, IDS_MENUREVERT, IDI_REVERT);
1601 if ((m_dwContextMenus & GITSLC_POPSKIPWORKTREE) && (this->m_CurrentVersion.IsEmpty() || this->m_CurrentVersion == GIT_REV_ZERO) && !(wcStatus & (CTGitPath::LOGACTIONS_ADDED | CTGitPath::LOGACTIONS_UNMERGED | CTGitPath::LOGACTIONS_SKIPWORKTREE)))
1602 popup.AppendMenuIcon(IDGITLC_SKIPWORKTREE, IDS_STATUSLIST_SKIPWORKTREE);
1604 if ((m_dwContextMenus & GITSLC_POPASSUMEVALID) && (this->m_CurrentVersion.IsEmpty() || this->m_CurrentVersion == GIT_REV_ZERO) && !(wcStatus & (CTGitPath::LOGACTIONS_ADDED | CTGitPath::LOGACTIONS_DELETED | CTGitPath::LOGACTIONS_UNMERGED | CTGitPath::LOGACTIONS_ASSUMEVALID)))
1605 popup.AppendMenuIcon(IDGITLC_ASSUMEVALID, IDS_MENUASSUMEVALID);
1607 if ((m_dwContextMenus & GITLC_POPUNSETIGNORELOCALCHANGES) && (this->m_CurrentVersion.IsEmpty() || this->m_CurrentVersion == GIT_REV_ZERO) && (wcStatus & (CTGitPath::LOGACTIONS_SKIPWORKTREE | CTGitPath::LOGACTIONS_ASSUMEVALID)))
1608 popup.AppendMenuIcon(IDGITLC_UNSETIGNORELOCALCHANGES, IDS_STATUSLIST_UNSETIGNORELOCALCHANGES);
1610 if (m_dwContextMenus & GITSLC_POPRESTORE && !filepath->IsDirectory())
1612 if (m_restorepaths.find(filepath->GetWinPathString()) == m_restorepaths.end())
1613 popup.AppendMenuIcon(IDGITLC_CREATERESTORE, IDS_MENUCREATERESTORE, IDI_RESTORE);
1614 else
1615 popup.AppendMenuIcon(IDGITLC_RESTOREPATH, IDS_MENURESTORE, IDI_RESTORE);
1618 if ((m_dwContextMenus & GetContextMenuBit(IDGITLC_REVERTTOREV)) && ( !this->m_CurrentVersion.IsEmpty() )
1619 && this->m_CurrentVersion != GIT_REV_ZERO && !(wcStatus & CTGitPath::LOGACTIONS_DELETED))
1621 popup.AppendMenuIcon(IDGITLC_REVERTTOREV, IDS_LOG_POPUP_REVERTTOREV, IDI_REVERT);
1624 if ((m_dwContextMenus & GetContextMenuBit(IDGITLC_REVERTTOPARENT)) && ( !this->m_CurrentVersion.IsEmpty() )
1625 && this->m_CurrentVersion != GIT_REV_ZERO && !(wcStatus & CTGitPath::LOGACTIONS_ADDED))
1627 popup.AppendMenuIcon(IDGITLC_REVERTTOPARENT, IDS_LOG_POPUP_REVERTTOPARENT, IDI_REVERT);
1631 if (selectedCount == 1 && !(wcStatus & (CTGitPath::LOGACTIONS_UNVER | CTGitPath::LOGACTIONS_IGNORE)))
1633 if (m_dwContextMenus & GITSLC_POPSHOWLOG)
1634 popup.AppendMenuIcon(IDGITLC_LOG, IDS_REPOBROWSE_SHOWLOG, IDI_LOG);
1635 if (m_dwContextMenus & GITSLC_POPSHOWLOGSUBMODULE && filepath->IsDirectory())
1636 popup.AppendMenuIcon(IDGITLC_LOGSUBMODULE, IDS_LOG_SUBMODULE, IDI_LOG);
1637 if (m_dwContextMenus & GITSLC_POPSHOWLOGOLDNAME && (wcStatus & (CTGitPath::LOGACTIONS_REPLACED|CTGitPath::LOGACTIONS_COPY) && !filepath->GetGitOldPathString().IsEmpty()))
1638 popup.AppendMenuIcon(IDGITLC_LOGOLDNAME, IDS_STATUSLIST_SHOWLOGOLDNAME, IDI_LOG);
1639 if ((m_dwContextMenus & GITSLC_POPBLAME) && !filepath->IsDirectory() && !(wcStatus & CTGitPath::LOGACTIONS_DELETED) && !((wcStatus & CTGitPath::LOGACTIONS_ADDED) && (m_CurrentVersion.IsEmpty() || m_CurrentVersion == GIT_REV_ZERO)) && m_bHasWC)
1640 popup.AppendMenuIcon(IDGITLC_BLAME, IDS_MENUBLAME, IDI_BLAME);
1643 if (selectedCount > 0)
1645 if ((m_dwContextMenus & GetContextMenuBit(IDGITLC_EXPORT)) && !(wcStatus & (CTGitPath::LOGACTIONS_DELETED | CTGitPath::LOGACTIONS_MISSING)))
1646 popup.AppendMenuIcon(IDGITLC_EXPORT, IDS_LOG_POPUP_EXPORT, IDI_EXPORT);
1649 if (selectedCount == 1)
1651 if (m_dwContextMenus & this->GetContextMenuBit(IDGITLC_SAVEAS) && ! filepath->IsDirectory() && !(wcStatus & CTGitPath::LOGACTIONS_DELETED))
1652 popup.AppendMenuIcon(IDGITLC_SAVEAS, IDS_LOG_POPUP_SAVE, IDI_SAVEAS);
1654 if (m_dwContextMenus & GITSLC_POPOPEN && !filepath->IsDirectory() && !(wcStatus & (CTGitPath::LOGACTIONS_DELETED | CTGitPath::LOGACTIONS_MISSING)))
1656 popup.AppendMenuIcon(IDGITLC_VIEWREV, IDS_LOG_POPUP_VIEWREV);
1657 popup.AppendMenuIcon(IDGITLC_OPEN, IDS_REPOBROWSE_OPEN, IDI_OPEN);
1658 popup.AppendMenuIcon(IDGITLC_OPENWITH, IDS_LOG_POPUP_OPENWITH, IDI_OPEN);
1661 if (m_dwContextMenus & GITSLC_POPEXPLORE && !(wcStatus & CTGitPath::LOGACTIONS_DELETED) && m_bHasWC)
1662 popup.AppendMenuIcon(IDGITLC_EXPLORE, IDS_STATUSLIST_CONTEXT_EXPLORE, IDI_EXPLORER);
1664 if (m_dwContextMenus & GITSLC_PREPAREDIFF && !(wcStatus & CTGitPath::LOGACTIONS_DELETED))
1666 popup.AppendMenu(MF_SEPARATOR);
1667 popup.AppendMenuIcon(IDGITLC_PREPAREDIFF, IDS_PREPAREDIFF, IDI_DIFF);
1668 if (!m_sMarkForDiffFilename.IsEmpty())
1670 CString diffWith;
1671 if (filepath->GetGitPathString() == m_sMarkForDiffFilename)
1672 diffWith = m_sMarkForDiffVersion;
1673 else
1675 PathCompactPathEx(CStrBuf(diffWith, 2 * GIT_HASH_SIZE), m_sMarkForDiffFilename, 2 * GIT_HASH_SIZE, 0);
1676 diffWith += L':' + m_sMarkForDiffVersion.Left(g_Git.GetShortHASHLength());
1678 CString menuEntry;
1679 menuEntry.Format(IDS_MENUDIFFNOW, (LPCTSTR)diffWith);
1680 popup.AppendMenuIcon(IDGITLC_PREPAREDIFF_COMPARE, menuEntry, IDI_DIFF);
1684 if (selectedCount > 0)
1686 // if (((wcStatus == git_wc_status_unversioned)||(wcStatus == git_wc_status_ignored))&&(m_dwContextMenus & SVNSLC_POPDELETE))
1687 // popup.AppendMenuIcon(IDSVNLC_DELETE, IDS_MENUREMOVE, IDI_DELETE);
1688 // if ((wcStatus != Git_wc_status_unversioned)&&(wcStatus != git_wc_status_ignored)&&(wcStatus != Git_wc_status_deleted)&&(wcStatus != Git_wc_status_added)&&(m_dwContextMenus & GitSLC_POPDELETE))
1689 // {
1690 // if (bShift)
1691 // popup.AppendMenuIcon(IDGitLC_REMOVE, IDS_MENUREMOVEKEEP, IDI_DELETE);
1692 // else
1693 // popup.AppendMenuIcon(IDGitLC_REMOVE, IDS_MENUREMOVE, IDI_DELETE);
1694 // }
1695 if ((wcStatus & (CTGitPath::LOGACTIONS_UNVER | CTGitPath::LOGACTIONS_IGNORE | CTGitPath::LOGACTIONS_MISSING))/*||(wcStatus == git_wc_status_deleted)*/)
1697 if (m_dwContextMenus & GITSLC_POPDELETE)
1698 popup.AppendMenuIcon(IDGITLC_DELETE, IDS_MENUREMOVE, IDI_DELETE);
1700 if ( (wcStatus & CTGitPath::LOGACTIONS_UNVER || wcStatus & CTGitPath::LOGACTIONS_DELETED) )
1702 if (m_dwContextMenus & GITSLC_POPIGNORE)
1704 CTGitPathList ignorelist;
1705 FillListOfSelectedItemPaths(ignorelist);
1706 //check if all selected entries have the same extension
1707 bool bSameExt = true;
1708 CString sExt;
1709 for (int i=0; i<ignorelist.GetCount(); ++i)
1711 if (sExt.IsEmpty() && (i==0))
1712 sExt = ignorelist[i].GetFileExtension();
1713 else if (sExt.CompareNoCase(ignorelist[i].GetFileExtension())!=0)
1714 bSameExt = false;
1716 if (bSameExt)
1718 if (ignoreSubMenu.CreateMenu())
1720 CString ignorepath;
1721 if (ignorelist.GetCount()==1)
1722 ignorepath = ignorelist[0].GetFileOrDirectoryName();
1723 else
1724 ignorepath.Format(IDS_MENUIGNOREMULTIPLE, ignorelist.GetCount());
1725 ignoreSubMenu.AppendMenu(MF_STRING | MF_ENABLED, IDGITLC_IGNORE, ignorepath);
1726 ignorepath = L'*' + sExt;
1727 ignoreSubMenu.AppendMenu(MF_STRING | MF_ENABLED, IDGITLC_IGNOREMASK, ignorepath);
1728 if (ignorelist.GetCount() == 1 && !ignorelist[0].GetContainingDirectory().GetGitPathString().IsEmpty())
1729 ignoreSubMenu.AppendMenu(MF_STRING | MF_ENABLED, IDGITLC_IGNOREFOLDER, ignorelist[0].GetContainingDirectory().GetGitPathString());
1730 CString temp;
1731 temp.LoadString(IDS_MENUIGNORE);
1732 popup.InsertMenu((UINT)-1, MF_BYPOSITION | MF_POPUP, (UINT_PTR)ignoreSubMenu.m_hMenu, temp);
1735 else
1737 CString temp;
1738 if (ignorelist.GetCount()==1)
1739 temp.LoadString(IDS_MENUIGNORE);
1740 else
1741 temp.Format(IDS_MENUIGNOREMULTIPLE, ignorelist.GetCount());
1742 popup.AppendMenuIcon(IDGITLC_IGNORE, temp, IDI_IGNORE);
1743 temp.Format(IDS_MENUIGNOREMULTIPLEMASK, ignorelist.GetCount());
1744 popup.AppendMenuIcon(IDGITLC_IGNOREMASK, temp, IDI_IGNORE);
1752 if (selectedCount > 0)
1754 popup.AppendMenu(MF_SEPARATOR);
1756 if (clipSubMenu.CreatePopupMenu())
1758 CString temp;
1759 clipSubMenu.AppendMenuIcon(IDGITLC_COPYFULL, IDS_STATUSLIST_CONTEXT_COPYFULLPATHS, IDI_COPYCLIP);
1760 clipSubMenu.AppendMenuIcon(IDGITLC_COPYRELPATHS, IDS_STATUSLIST_CONTEXT_COPYRELPATHS, IDI_COPYCLIP);
1761 clipSubMenu.AppendMenuIcon(IDGITLC_COPYFILENAMES, IDS_STATUSLIST_CONTEXT_COPYFILENAMES, IDI_COPYCLIP);
1762 clipSubMenu.AppendMenuIcon(IDGITLC_COPYEXT, IDS_STATUSLIST_CONTEXT_COPYEXT, IDI_COPYCLIP);
1763 if (selSubitem >= 0)
1765 temp.Format(IDS_STATUSLIST_CONTEXT_COPYCOL, (LPCWSTR)m_ColumnManager.GetName(selSubitem));
1766 clipSubMenu.AppendMenuIcon(IDGITLC_COPYCOL, temp, IDI_COPYCLIP);
1768 temp.LoadString(IDS_LOG_POPUP_COPYTOCLIPBOARD);
1769 popup.InsertMenu((UINT)-1, MF_BYPOSITION | MF_POPUP, (UINT_PTR)clipSubMenu.m_hMenu, temp);
1772 #if 0
1773 if ((m_dwContextMenus & SVNSLC_POPCHANGELISTS))
1774 &&(wcStatus != git_wc_status_unversioned)&&(wcStatus != git_wc_status_none))
1776 popup.AppendMenu(MF_SEPARATOR);
1777 // changelist commands
1778 size_t numChangelists = GetNumberOfChangelistsInSelection();
1779 if (numChangelists > 0)
1780 popup.AppendMenuIcon(IDSVNLC_REMOVEFROMCS, IDS_STATUSLIST_CONTEXT_REMOVEFROMCS);
1781 if ((!entry->IsFolder())&&(changelistSubMenu.CreateMenu()))
1783 CString temp;
1784 temp.LoadString(IDS_STATUSLIST_CONTEXT_CREATECS);
1785 changelistSubMenu.AppendMenu(MF_STRING | MF_ENABLED, IDSVNLC_CREATECS, temp);
1787 if (entry->changelist.Compare(SVNSLC_IGNORECHANGELIST))
1789 changelistSubMenu.AppendMenu(MF_SEPARATOR);
1790 changelistSubMenu.AppendMenu(MF_STRING | MF_ENABLED, IDSVNLC_CREATEIGNORECS, SVNSLC_IGNORECHANGELIST);
1793 if (!m_changelists.empty())
1795 // find the changelist names
1796 bool bNeedSeparator = true;
1797 int cmdID = IDSVNLC_MOVETOCS;
1798 for (auto it = m_changelists.cbegin(); it != m_changelists.cend(); ++it)
1800 if ((entry->changelist.Compare(it->first))&&(it->first.Compare(SVNSLC_IGNORECHANGELIST)))
1802 if (bNeedSeparator)
1804 changelistSubMenu.AppendMenu(MF_SEPARATOR);
1805 bNeedSeparator = false;
1807 changelistSubMenu.AppendMenu(MF_STRING | MF_ENABLED, cmdID, it->first);
1808 cmdID++;
1812 temp.LoadString(IDS_STATUSLIST_CONTEXT_MOVETOCS);
1813 popup.AppendMenu(MF_POPUP|MF_STRING, (UINT_PTR)changelistSubMenu.GetSafeHmenu(), temp);
1816 #endif
1819 m_hShellMenu = nullptr;
1820 if (selectedCount > 0 && !(wcStatus & (CTGitPath::LOGACTIONS_DELETED | CTGitPath::LOGACTIONS_MISSING)) && m_bHasWC && (m_CurrentVersion.IsEmpty() || m_CurrentVersion == GIT_REV_ZERO) && shellMenu.CreatePopupMenu())
1822 // insert the shell context menu
1823 popup.AppendMenu(MF_SEPARATOR);
1824 popup.InsertMenu((UINT)-1, MF_BYPOSITION | MF_POPUP, (UINT_PTR)shellMenu.m_hMenu, CString(MAKEINTRESOURCE(IDS_STATUSLIST_CONTEXT_SHELL)));
1825 m_hShellMenu = shellMenu.m_hMenu;
1828 int cmd = popup.TrackPopupMenu(TPM_RETURNCMD | TPM_LEFTALIGN | TPM_RIGHTBUTTON, point.x, point.y, this);
1829 g_IContext2 = nullptr;
1830 g_IContext3 = nullptr;
1831 if (m_pContextMenu)
1833 if (cmd >= SHELL_MIN_CMD && cmd <= SHELL_MAX_CMD) // see if returned idCommand belongs to shell menu entries)
1835 CMINVOKECOMMANDINFOEX cmi = { 0 };
1836 cmi.cbSize = sizeof(CMINVOKECOMMANDINFOEX);
1837 cmi.fMask = CMIC_MASK_UNICODE | CMIC_MASK_PTINVOKE;
1838 if (GetKeyState(VK_CONTROL) < 0)
1839 cmi.fMask |= CMIC_MASK_CONTROL_DOWN;
1840 if (bShift)
1841 cmi.fMask |= CMIC_MASK_SHIFT_DOWN;
1842 cmi.hwnd = m_hWnd;
1843 cmi.lpVerb = MAKEINTRESOURCEA(cmd - SHELL_MIN_CMD);
1844 cmi.lpVerbW = MAKEINTRESOURCEW(cmd - SHELL_MIN_CMD);
1845 cmi.nShow = SW_SHOWNORMAL;
1846 cmi.ptInvoke = point;
1848 m_pContextMenu->InvokeCommand((LPCMINVOKECOMMANDINFO)&cmi);
1850 cmd = 0;
1852 m_pContextMenu->Release();
1853 m_pContextMenu = nullptr;
1855 if (g_pFolderhook)
1857 delete g_pFolderhook;
1858 g_pFolderhook = nullptr;
1860 if (g_psfDesktopFolder)
1862 g_psfDesktopFolder->Release();
1863 g_psfDesktopFolder = nullptr;
1865 for (int i = 0; i < g_pidlArrayItems; i++)
1867 if (g_pidlArray[i])
1868 CoTaskMemFree(g_pidlArray[i]);
1870 if (g_pidlArray)
1871 CoTaskMemFree(g_pidlArray);
1872 g_pidlArray = nullptr;
1873 g_pidlArrayItems = 0;
1875 m_bWaitCursor = true;
1876 bShift = !!(GetAsyncKeyState(VK_SHIFT) & 0x8000);
1877 //int iItemCountBeforeMenuCmd = GetItemCount();
1878 //bool bForce = false;
1879 switch (cmd)
1881 case IDGITLC_VIEWREV:
1882 OpenFile(filepath, ALTERNATIVEEDITOR);
1883 break;
1885 case IDGITLC_OPEN:
1886 OpenFile(filepath,OPEN);
1887 break;
1889 case IDGITLC_OPENWITH:
1890 OpenFile(filepath,OPEN_WITH);
1891 break;
1893 case IDGITLC_EXPLORE:
1894 CAppUtils::ExploreTo(GetSafeHwnd(), g_Git.CombinePath(filepath));
1895 break;
1897 case IDGITLC_PREPAREDIFF:
1898 m_sMarkForDiffFilename = filepath->GetGitPathString();
1899 m_sMarkForDiffVersion = m_CurrentVersion;
1900 break;
1902 case IDGITLC_PREPAREDIFF_COMPARE:
1904 CTGitPath savedFile(m_sMarkForDiffFilename);
1905 CGitDiff::Diff(filepath, &savedFile, m_CurrentVersion, m_sMarkForDiffVersion, false, false, 0, bShift);
1907 break;
1909 case IDGITLC_CREATERESTORE:
1911 POSITION pos = GetFirstSelectedItemPosition();
1912 while (pos)
1914 int index = GetNextSelectedItem(pos);
1915 auto entry2 = GetListEntry(index);
1916 if (!entry2 || entry2->IsDirectory())
1917 continue;
1918 if (m_restorepaths.find(entry2->GetWinPathString()) != m_restorepaths.end())
1919 continue;
1920 CTGitPath tempFile = CTempFiles::Instance().GetTempFilePath(false);
1921 // delete the temp file: the temp file has the FILE_ATTRIBUTE_TEMPORARY flag set
1922 // and copying the real file over it would leave that temp flag.
1923 DeleteFile(tempFile.GetWinPath());
1924 if (CopyFile(g_Git.CombinePath(entry2), tempFile.GetWinPath(), FALSE))
1926 m_restorepaths[entry2->GetWinPathString()] = tempFile.GetWinPathString();
1927 SetItemState(index, INDEXTOOVERLAYMASK(OVL_RESTORE), LVIS_OVERLAYMASK);
1930 Invalidate();
1932 break;
1934 case IDGITLC_RESTOREPATH:
1936 if (CMessageBox::Show(m_hWnd, IDS_STATUSLIST_RESTOREPATH, IDS_APPNAME, 2, IDI_QUESTION, IDS_RESTOREBUTTON, IDS_ABORTBUTTON) == 2)
1937 break;
1938 POSITION pos = GetFirstSelectedItemPosition();
1939 while (pos)
1941 int index = GetNextSelectedItem(pos);
1942 auto entry2 = GetListEntry(index);
1943 if (!entry2)
1944 continue;
1945 if (m_restorepaths.find(entry2->GetWinPathString()) == m_restorepaths.end())
1946 continue;
1947 if (CopyFile(m_restorepaths[entry2->GetWinPathString()], g_Git.CombinePath(entry2), FALSE))
1949 m_restorepaths.erase(entry2->GetWinPathString());
1950 SetItemState(index, 0, LVIS_OVERLAYMASK);
1953 Invalidate();
1955 break;
1957 // Compare current version and work copy.
1958 case IDGITLC_COMPAREWC:
1960 if (!CheckMultipleDiffs())
1961 break;
1962 POSITION pos = GetFirstSelectedItemPosition();
1963 while ( pos )
1965 int index = GetNextSelectedItem(pos);
1966 StartDiffWC(index);
1969 break;
1971 // Compare with base version. when current version is zero, compare workcopy and HEAD.
1972 case IDGITLC_COMPARE:
1974 if (!CheckMultipleDiffs())
1975 break;
1976 POSITION pos = GetFirstSelectedItemPosition();
1977 while ( pos )
1979 int index = GetNextSelectedItem(pos);
1980 StartDiff(index);
1983 break;
1985 case IDGITLC_COMPARETWOREVISIONS:
1987 if (!CheckMultipleDiffs())
1988 break;
1989 POSITION pos = GetFirstSelectedItemPosition();
1990 while ( pos )
1992 int index = GetNextSelectedItem(pos);
1993 StartDiffTwo(index);
1996 break;
1998 case IDGITLC_COMPARETWOFILES:
2000 POSITION pos = GetFirstSelectedItemPosition();
2001 if (pos)
2003 auto firstfilepath = GetListEntry(GetNextSelectedItem(pos));
2004 if (!firstfilepath)
2005 break;
2007 auto secondfilepath = GetListEntry(GetNextSelectedItem(pos));
2008 if (!secondfilepath)
2009 break;
2011 CString sCmd;
2012 if (m_CurrentVersion.IsEmpty() || m_CurrentVersion == GIT_REV_ZERO)
2013 sCmd.Format(L"/command:diff /path:\"%s\" /endrev:%s /path2:\"%s\" /startrev:%s /hwnd:%p", firstfilepath->GetWinPath(), firstfilepath->Exists() ? GIT_REV_ZERO : L"HEAD", secondfilepath->GetWinPath(), secondfilepath->Exists() ? GIT_REV_ZERO : L"HEAD", (void*)m_hWnd);
2014 else
2015 sCmd.Format(L"/command:diff /path:\"%s\" /startrev:%s /path2:\"%s\" /endrev:%s /hwnd:%p", firstfilepath->GetWinPath(), firstfilepath->Exists() ? (LPCTSTR)(m_CurrentVersion + L"~1") : (LPCTSTR)m_CurrentVersion, secondfilepath->GetWinPath(), secondfilepath->Exists() ? (LPCTSTR)(m_CurrentVersion + L"~1") : (LPCTSTR)m_CurrentVersion, (void*)m_hWnd);
2016 if (bShift)
2017 sCmd += L" /alternative";
2018 CAppUtils::RunTortoiseGitProc(sCmd);
2021 break;
2023 case IDGITLC_GNUDIFF1:
2025 if (!CheckMultipleDiffs())
2026 break;
2027 POSITION pos = GetFirstSelectedItemPosition();
2028 while (pos)
2030 auto selectedFilepath = GetListEntry(GetNextSelectedItem(pos));
2031 if (m_CurrentVersion.IsEmpty() || m_CurrentVersion == GIT_REV_ZERO)
2033 CString fromwhere;
2034 if (m_amend)
2035 fromwhere = L"~1";
2036 CAppUtils::StartShowUnifiedDiff(m_hWnd, *selectedFilepath, GitRev::GetHead() + fromwhere, *selectedFilepath, GitRev::GetWorkingCopy(), bShift);
2038 else
2040 if ((selectedFilepath->m_ParentNo & (PARENT_MASK | MERGE_MASK)) == 0)
2041 CAppUtils::StartShowUnifiedDiff(m_hWnd, *selectedFilepath, m_CurrentVersion + L"~1", *selectedFilepath, m_CurrentVersion, bShift);
2042 else
2044 CString str;
2045 if (!(selectedFilepath->m_ParentNo & MERGE_MASK))
2046 str.Format(L"%s^%d", (LPCTSTR)m_CurrentVersion, (selectedFilepath->m_ParentNo & PARENT_MASK) + 1);
2048 CAppUtils::StartShowUnifiedDiff(m_hWnd, *selectedFilepath, str, *selectedFilepath, m_CurrentVersion, bShift, false, false, false, !!(selectedFilepath->m_ParentNo & MERGE_MASK));
2053 break;
2055 case IDGITLC_GNUDIFF2REVISIONS:
2057 if (!CheckMultipleDiffs())
2058 break;
2059 POSITION pos = GetFirstSelectedItemPosition();
2060 while (pos)
2062 auto entry = GetListEntry(GetNextSelectedItem(pos));
2063 CAppUtils::StartShowUnifiedDiff(m_hWnd, *entry, m_Rev2, *entry, m_Rev1, bShift);
2066 break;
2068 case IDGITLC_ADD:
2070 CTGitPathList paths;
2071 FillListOfSelectedItemPaths(paths, true);
2073 CGitProgressDlg progDlg;
2074 AddProgressCommand addCommand;
2075 progDlg.SetCommand(&addCommand);
2076 addCommand.SetShowCommitButtonAfterAdd((m_dwContextMenus & GITSLC_POPCOMMIT) != 0);
2077 addCommand.SetPathList(paths);
2078 progDlg.SetItemCount(paths.GetCount());
2079 progDlg.DoModal();
2081 // reset unchecked status
2082 POSITION pos = GetFirstSelectedItemPosition();
2083 int index;
2084 while ((index = GetNextSelectedItem(pos)) >= 0)
2085 m_mapFilenameToChecked.erase(GetListEntry(index)->GetGitPathString());
2087 if (GetLogicalParent() && GetLogicalParent()->GetSafeHwnd())
2088 GetLogicalParent()->SendMessage(GITSLNM_NEEDSREFRESH);
2090 SetRedraw(TRUE);
2092 break;
2094 case IDGITLC_DELETE:
2095 DeleteSelectedFiles();
2096 break;
2098 case IDGITLC_BLAME:
2100 CAppUtils::LaunchTortoiseBlame(g_Git.CombinePath(filepath), m_CurrentVersion);
2102 break;
2104 case IDGITLC_LOG:
2105 case IDGITLC_LOGSUBMODULE:
2107 CString sCmd;
2108 sCmd.Format(L"/command:log /path:\"%s\"", (LPCTSTR)g_Git.CombinePath(filepath));
2109 if (cmd == IDGITLC_LOG && filepath->IsDirectory())
2110 sCmd += L" /submodule";
2111 if (!m_sDisplayedBranch.IsEmpty())
2112 sCmd += L" /range:\"" + m_sDisplayedBranch + L'"';
2113 CAppUtils::RunTortoiseGitProc(sCmd, false, !(cmd == IDGITLC_LOGSUBMODULE));
2115 break;
2117 case IDGITLC_LOGOLDNAME:
2119 CTGitPath oldName(filepath->GetGitOldPathString());
2120 CString sCmd;
2121 sCmd.Format(L"/command:log /path:\"%s\"", (LPCTSTR)g_Git.CombinePath(oldName));
2122 if (!m_sDisplayedBranch.IsEmpty())
2123 sCmd += L" /range:\"" + m_sDisplayedBranch + L'"';
2124 CAppUtils::RunTortoiseGitProc(sCmd);
2126 break;
2128 case IDGITLC_EDITCONFLICT:
2130 if (CAppUtils::ConflictEdit(*filepath, bShift, m_bIsRevertTheirMy, GetLogicalParent() ? GetLogicalParent()->GetSafeHwnd() : nullptr))
2132 CString conflictedFile = g_Git.CombinePath(filepath);
2133 if (!PathFileExists(conflictedFile) && GetLogicalParent() && GetLogicalParent()->GetSafeHwnd())
2135 GetLogicalParent()->SendMessage(GITSLNM_NEEDSREFRESH);
2136 break;
2138 StoreScrollPos();
2139 Show(m_dwShow, 0, m_bShowFolders, 0, true);
2142 break;
2144 case IDGITLC_RESOLVETHEIRS: //follow up
2145 case IDGITLC_RESOLVEMINE: //follow up
2146 case IDGITLC_RESOLVECONFLICT:
2148 if (CMessageBox::Show(m_hWnd, IDS_PROC_RESOLVE, IDS_APPNAME, MB_ICONQUESTION | MB_YESNO)==IDYES)
2150 bool needsFullRefresh = false;
2151 POSITION pos = GetFirstSelectedItemPosition();
2152 while (pos != 0)
2154 int index;
2155 index = GetNextSelectedItem(pos);
2156 auto fentry = GetListEntry(index);
2157 if (!fentry)
2158 continue;
2160 CAppUtils::resolve_with resolveWith = CAppUtils::RESOLVE_WITH_CURRENT;
2161 if (((!this->m_bIsRevertTheirMy) && cmd == IDGITLC_RESOLVETHEIRS) || ((this->m_bIsRevertTheirMy) && cmd == IDGITLC_RESOLVEMINE))
2162 resolveWith = CAppUtils::RESOLVE_WITH_THEIRS;
2163 else if (((!this->m_bIsRevertTheirMy) && cmd == IDGITLC_RESOLVEMINE) || ((this->m_bIsRevertTheirMy) && cmd == IDGITLC_RESOLVETHEIRS))
2164 resolveWith = CAppUtils::RESOLVE_WITH_MINE;
2165 if (CAppUtils::ResolveConflict(*fentry, resolveWith) == 0 && fentry->m_Action & CTGitPath::LOGACTIONS_UNMERGED)
2166 needsFullRefresh = true;
2168 if (needsFullRefresh && CRegDWORD(L"Software\\TortoiseGit\\RefreshFileListAfterResolvingConflict", TRUE) == TRUE)
2170 CWnd* pParent = GetLogicalParent();
2171 if (pParent && pParent->GetSafeHwnd())
2172 pParent->SendMessage(GITSLNM_NEEDSREFRESH);
2173 SetRedraw(TRUE);
2174 break;
2176 StoreScrollPos();
2177 Show(m_dwShow, 0, m_bShowFolders,0,true);
2180 break;
2182 case IDGITLC_IGNORE:
2184 CTGitPathList ignorelist;
2185 //std::vector<CString> toremove;
2186 FillListOfSelectedItemPaths(ignorelist, true);
2188 if(!CAppUtils::IgnoreFile(ignorelist,false))
2189 break;
2191 SetRedraw(FALSE);
2192 CWnd* pParent = GetLogicalParent();
2193 if (pParent && pParent->GetSafeHwnd())
2195 pParent->SendMessage(GITSLNM_NEEDSREFRESH);
2197 SetRedraw(TRUE);
2199 break;
2201 case IDGITLC_IGNOREMASK:
2203 CString common;
2204 CString ext=filepath->GetFileExtension();
2205 CTGitPathList ignorelist;
2206 FillListOfSelectedItemPaths(ignorelist, true);
2208 if (!CAppUtils::IgnoreFile(ignorelist,true))
2209 break;
2211 SetRedraw(FALSE);
2212 CWnd* pParent = GetLogicalParent();
2213 if (pParent && pParent->GetSafeHwnd())
2215 pParent->SendMessage(GITSLNM_NEEDSREFRESH);
2218 SetRedraw(TRUE);
2220 break;
2222 case IDGITLC_IGNOREFOLDER:
2224 CTGitPathList ignorelist;
2225 ignorelist.AddPath(filepath->GetContainingDirectory());
2227 if (!CAppUtils::IgnoreFile(ignorelist, false))
2228 break;
2230 SetRedraw(FALSE);
2231 CWnd *pParent = GetLogicalParent();
2232 if (pParent && pParent->GetSafeHwnd())
2233 pParent->SendMessage(GITSLNM_NEEDSREFRESH);
2235 SetRedraw(TRUE);
2237 break;
2238 case IDGITLC_COMMIT:
2240 CTGitPathList targetList;
2241 FillListOfSelectedItemPaths(targetList);
2242 CTGitPath tempFile = CTempFiles::Instance().GetTempFilePath(false);
2243 VERIFY(targetList.WriteToFile(tempFile.GetWinPathString()));
2244 CString commandline = L"/command:commit /pathfile:\"";
2245 commandline += tempFile.GetWinPathString();
2246 commandline += L'"';
2247 commandline += L" /deletepathfile";
2248 CAppUtils::RunTortoiseGitProc(commandline);
2250 break;
2251 case IDGITLC_REVERT:
2253 // If at least one item is not in the status "added"
2254 // we ask for a confirmation
2255 BOOL bConfirm = FALSE;
2256 POSITION pos = GetFirstSelectedItemPosition();
2257 int index;
2258 while ((index = GetNextSelectedItem(pos)) >= 0)
2260 auto fentry = GetListEntry(index);
2261 if(fentry && fentry->m_Action &CTGitPath::LOGACTIONS_MODIFIED && !fentry->IsDirectory())
2263 bConfirm = TRUE;
2264 break;
2268 CString str;
2269 str.Format(IDS_PROC_WARNREVERT, selectedCount);
2271 if (!bConfirm || MessageBox(str, L"TortoiseGit", MB_YESNO | MB_ICONQUESTION) == IDYES)
2273 CTGitPathList targetList;
2274 FillListOfSelectedItemPaths(targetList);
2276 // make sure that the list is reverse sorted, so that
2277 // children are removed before any parents
2278 targetList.SortByPathname(true);
2280 // put all reverted files in the trashbin, except the ones with 'added'
2281 // status because they are not restored by the revert.
2282 CTGitPathList delList;
2283 POSITION pos2 = GetFirstSelectedItemPosition();
2284 int index2;
2285 while ((index2 = GetNextSelectedItem(pos2)) >= 0)
2287 auto entry = GetListEntry(index2);
2288 if (entry&&(!(entry->m_Action& CTGitPath::LOGACTIONS_ADDED))
2289 && (!(entry->m_Action& CTGitPath::LOGACTIONS_REPLACED)) && !entry->IsDirectory())
2291 CTGitPath fullpath;
2292 fullpath.SetFromWin(g_Git.CombinePath(entry));
2293 delList.AddPath(fullpath);
2296 if (DWORD(CRegDWORD(L"Software\\TortoiseGit\\RevertWithRecycleBin", TRUE)))
2297 delList.DeleteAllFiles(true);
2299 CString revertToCommit = L"HEAD";
2300 if (m_amend)
2301 revertToCommit = L"HEAD~1";
2302 CString err;
2303 if (g_Git.Revert(revertToCommit, targetList, err))
2304 MessageBox(L"Revert failed:\n" + err, L"TortoiseGit", MB_ICONERROR);
2305 else
2307 bool updateStatusList = false;
2308 for (int i = 0 ; i < targetList.GetCount(); ++i)
2310 int nListboxEntries = GetItemCount();
2311 for (int nItem=0; nItem<nListboxEntries; ++nItem)
2313 auto path = GetListEntry(nItem);
2314 if (path->GetGitPathString()==targetList[i].GetGitPathString() && !path->IsDirectory())
2316 if(path->m_Action & CTGitPath::LOGACTIONS_ADDED)
2318 path->m_Action = CTGitPath::LOGACTIONS_UNVER;
2319 SetEntryCheck(path,nItem,false);
2320 updateStatusList = true;
2321 #if 0 // revert an added file and some entry will be cloned (part 1/2)
2322 SetItemGroup(nItem,1);
2323 this->m_StatusFileList.RemoveItem(*path);
2324 this->m_UnRevFileList.AddPath(*path);
2325 //this->m_IgnoreFileList.RemoveItem(*path);
2326 #endif
2328 else
2330 if (GetCheck(nItem))
2331 m_nSelected--;
2332 RemoveListEntry(nItem);
2334 break;
2336 else if (path->GetGitPathString()==targetList[i].GetGitPathString() && path->IsDirectory() && path->IsWCRoot())
2338 CString sCmd;
2339 sCmd.Format(L"/command:revert /path:\"%s\"", (LPCTSTR)path->GetGitPathString());
2340 CCommonAppUtils::RunTortoiseGitProc(sCmd);
2344 SetRedraw(TRUE);
2345 #if 0 // revert an added file and some entry will be cloned (part 2/2)
2346 Show(m_dwShow, 0, m_bShowFolders,updateStatusList,true);
2347 NotifyCheck();
2348 #else
2349 if (updateStatusList && GetLogicalParent() && GetLogicalParent()->GetSafeHwnd())
2350 GetLogicalParent()->SendMessage(GITSLNM_NEEDSREFRESH);
2351 #endif
2355 break;
2357 case IDGITLC_ASSUMEVALID:
2358 SetGitIndexFlagsForSelectedFiles(IDS_PROC_MARK_ASSUMEVALID, BST_CHECKED, BST_INDETERMINATE);
2359 break;
2360 case IDGITLC_SKIPWORKTREE:
2361 SetGitIndexFlagsForSelectedFiles(IDS_PROC_MARK_SKIPWORKTREE, BST_INDETERMINATE, BST_CHECKED);
2362 break;
2363 case IDGITLC_UNSETIGNORELOCALCHANGES:
2364 SetGitIndexFlagsForSelectedFiles(IDS_PROC_UNSET_IGNORELOCALCHANGES, BST_UNCHECKED, BST_UNCHECKED);
2365 break;
2366 case IDGITLC_COPYFULL:
2367 case IDGITLC_COPYRELPATHS:
2368 case IDGITLC_COPYFILENAMES:
2369 CopySelectedEntriesToClipboard(GITSLC_COLFILENAME, cmd);
2370 break;
2371 case IDGITLC_COPYEXT:
2372 CopySelectedEntriesToClipboard((DWORD)-1, 0);
2373 break;
2374 case IDGITLC_COPYCOL:
2375 CopySelectedEntriesToClipboard((DWORD)1 << selSubitem, 0);
2376 break;
2377 case IDGITLC_EXPORT:
2378 FilesExport();
2379 break;
2380 case IDGITLC_SAVEAS:
2381 FileSaveAs(filepath);
2382 break;
2384 case IDGITLC_REVERTTOREV:
2385 RevertSelectedItemToVersion();
2386 break;
2387 case IDGITLC_REVERTTOPARENT:
2388 RevertSelectedItemToVersion(true);
2389 break;
2390 #if 0
2391 case IDSVNLC_COMMIT:
2393 CTSVNPathList targetList;
2394 FillListOfSelectedItemPaths(targetList);
2395 CTSVNPath tempFile = CTempFiles::Instance().GetTempFilePath(false);
2396 VERIFY(targetList.WriteToFile(tempFile.GetWinPathString()));
2397 CString commandline = CPathUtils::GetAppDirectory();
2398 commandline += L"TortoiseGitProc.exe /command:commit /pathfile:\"";
2399 commandline += tempFile.GetWinPathString();
2400 commandline += L'"';
2401 commandline += L" /deletepathfile";
2402 CAppUtils::LaunchApplication(commandline, nullptr, false);
2404 break;
2405 case IDSVNLC_CREATEIGNORECS:
2406 CreateChangeList(SVNSLC_IGNORECHANGELIST);
2407 break;
2408 case IDSVNLC_CREATECS:
2410 CCreateChangelistDlg dlg;
2411 if (dlg.DoModal() == IDOK)
2412 CreateChangeList(dlg.m_sName);
2414 break;
2415 default:
2417 if (cmd < IDSVNLC_MOVETOCS)
2418 break;
2419 CTSVNPathList changelistItems;
2420 FillListOfSelectedItemPaths(changelistItems);
2422 // find the changelist name
2423 CString sChangelist;
2424 int cmdID = IDSVNLC_MOVETOCS;
2425 SetRedraw(FALSE);
2426 for (auto it = m_changelists.cbegin(); it != m_changelists.cend(); ++it)
2428 if ((it->first.Compare(SVNSLC_IGNORECHANGELIST))&&(entry->changelist.Compare(it->first)))
2430 if (cmd == cmdID)
2431 sChangelist = it->first;
2432 cmdID++;
2435 if (!sChangelist.IsEmpty())
2437 SVN git;
2438 if (git.AddToChangeList(changelistItems, sChangelist, git_depth_empty))
2440 // The changelists were moved, but we now need to run through the selected items again
2441 // and update their changelist
2442 POSITION pos = GetFirstSelectedItemPosition();
2443 int index;
2444 while ((index = GetNextSelectedItem(pos)) >= 0)
2446 FileEntry * e = GetListEntry(index);
2447 e->changelist = sChangelist;
2448 if (!e->IsFolder())
2450 if (m_changelists.find(e->changelist)!=m_changelists.end())
2451 SetItemGroup(index, m_changelists[e->changelist]);
2452 else
2453 SetItemGroup(index, 0);
2457 else
2458 MessageBox(git.GetLastErrorMessage(), L"TortoiseGit", MB_ICONERROR);
2460 SetRedraw(TRUE);
2462 break;
2463 #endif
2465 } // switch (cmd)
2466 m_bWaitCursor = false;
2467 GetStatisticsString();
2468 //int iItemCountAfterMenuCmd = GetItemCount();
2469 //if (iItemCountAfterMenuCmd != iItemCountBeforeMenuCmd)
2471 // CWnd* pParent = GetParent();
2472 // if (pParent && pParent->GetSafeHwnd())
2473 // {
2474 // pParent->SendMessage(SVNSLNM_ITEMCOUNTCHANGED);
2475 // }
2477 } // if (popup.CreatePopupMenu())
2478 } // if (selIndex >= 0)
2481 void CGitStatusListCtrl::SetGitIndexFlagsForSelectedFiles(UINT message, BOOL assumevalid, BOOL skipworktree)
2483 if (CMessageBox::Show(GetSafeHwnd(), message, IDS_APPNAME, MB_YESNO | MB_DEFBUTTON2 | MB_ICONQUESTION) != IDYES)
2484 return;
2486 CAutoReadLock locker(m_guard);
2488 CAutoRepository repository(g_Git.GetGitRepository());
2489 if (!repository)
2491 MessageBox(g_Git.GetLibGit2LastErr(), L"TortoiseGit", MB_ICONERROR);
2492 return;
2495 CAutoIndex gitindex;
2496 if (git_repository_index(gitindex.GetPointer(), repository))
2498 MessageBox(g_Git.GetLibGit2LastErr(), L"TortoiseGit", MB_ICONERROR);
2499 return;
2502 POSITION pos = GetFirstSelectedItemPosition();
2503 int index = -1;
2504 while ((index = GetNextSelectedItem(pos)) >= 0)
2506 auto path = GetListEntry(index);
2507 if (path == nullptr)
2508 continue;
2510 size_t idx;
2511 if (!git_index_find(&idx, gitindex, CUnicodeUtils::GetMulti(path->GetGitPathString(), CP_UTF8)))
2513 git_index_entry *e = const_cast<git_index_entry *>(git_index_get_byindex(gitindex, idx)); // HACK
2514 if (assumevalid == BST_UNCHECKED)
2515 e->flags &= ~GIT_IDXENTRY_VALID;
2516 else if (assumevalid == BST_CHECKED)
2517 e->flags |= GIT_IDXENTRY_VALID;
2518 if (skipworktree == BST_UNCHECKED)
2519 e->flags_extended &= ~GIT_IDXENTRY_SKIP_WORKTREE;
2520 else if (skipworktree == BST_CHECKED)
2521 e->flags_extended |= GIT_IDXENTRY_SKIP_WORKTREE;
2522 git_index_add(gitindex, e);
2524 else
2525 MessageBox(g_Git.GetLibGit2LastErr(), L"TortoiseGit", MB_ICONERROR);
2528 if (git_index_write(gitindex))
2530 MessageBox(g_Git.GetLibGit2LastErr(), L"TortoiseGit", MB_ICONERROR);
2531 return;
2534 if (nullptr != GetLogicalParent() && nullptr != GetLogicalParent()->GetSafeHwnd())
2535 GetLogicalParent()->SendMessage(GITSLNM_NEEDSREFRESH);
2537 SetRedraw(TRUE);
2540 void CGitStatusListCtrl::OnContextMenu(CWnd* pWnd, CPoint point)
2542 __super::OnContextMenu(pWnd, point);
2543 if (pWnd == this)
2544 OnContextMenuList(pWnd, point);
2547 void CGitStatusListCtrl::OnNMDblclk(NMHDR *pNMHDR, LRESULT *pResult)
2549 LPNMLISTVIEW pNMLV = reinterpret_cast<LPNMLISTVIEW>(pNMHDR);
2550 *pResult = 0;
2552 CAutoReadWeakLock readLock(m_guard);
2553 if (!readLock.IsAcquired())
2554 return;
2556 if (pNMLV->iItem < 0)
2557 return;
2559 auto file = GetListEntry(pNMLV->iItem);
2561 if (file->m_Action & (CTGitPath::LOGACTIONS_UNVER | CTGitPath::LOGACTIONS_IGNORE)) {
2562 StartDiffWC(pNMLV->iItem);
2563 return;
2565 if( file->m_Action&CTGitPath::LOGACTIONS_UNMERGED )
2567 if (CAppUtils::ConflictEdit(*file, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000), m_bIsRevertTheirMy, GetLogicalParent() ? GetLogicalParent()->GetSafeHwnd() : nullptr))
2569 CString conflictedFile = g_Git.CombinePath(file);
2570 if (!PathFileExists(conflictedFile) && GetLogicalParent() && GetLogicalParent()->GetSafeHwnd())
2572 GetLogicalParent()->SendMessage(GITSLNM_NEEDSREFRESH);
2573 return;
2575 StoreScrollPos();
2576 Show(m_dwShow, 0, m_bShowFolders, 0, true);
2579 else if ((file->m_Action & CTGitPath::LOGACTIONS_MISSING) && file->m_Action != (CTGitPath::LOGACTIONS_MISSING | CTGitPath::LOGACTIONS_DELETED) && file->m_Action != (CTGitPath::LOGACTIONS_MISSING | CTGitPath::LOGACTIONS_DELETED | CTGitPath::LOGACTIONS_MODIFIED))
2580 return;
2581 else
2583 if (!m_Rev1.IsEmpty() && !m_Rev2.IsEmpty())
2584 StartDiffTwo(pNMLV->iItem);
2585 else
2586 StartDiff(pNMLV->iItem);
2589 void CGitStatusListCtrl::StartDiffTwo(int fileindex)
2591 if(fileindex<0)
2592 return;
2594 CAutoReadLock locker(m_guard);
2595 auto ptr = GetListEntry(fileindex);
2596 if (!ptr)
2597 return;
2598 CTGitPath file1 = *ptr;
2600 if (file1.m_Action & CTGitPath::LOGACTIONS_ADDED)
2601 CGitDiff::DiffNull(&file1, m_Rev1, true, 0, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000));
2602 else if (file1.m_Action & CTGitPath::LOGACTIONS_DELETED)
2603 CGitDiff::DiffNull(&file1, m_Rev2, false, 0, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000));
2604 else
2605 CGitDiff::Diff(&file1, &file1, m_Rev1, m_Rev2, false, false, 0, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000));
2608 void CGitStatusListCtrl::StartDiffWC(int fileindex)
2610 if(fileindex<0)
2611 return;
2613 CAutoReadLock locker(m_guard);
2614 if (m_CurrentVersion.IsEmpty())
2615 m_CurrentVersion == GIT_REV_ZERO;
2617 auto ptr = GetListEntry(fileindex);
2618 if (!ptr)
2619 return;
2620 CTGitPath file1 = *ptr;
2621 file1.m_Action = 0; // reset action, so that diff is not started as added/deleted file; see issue #1757
2623 CGitDiff::Diff(&file1, &file1, GIT_REV_ZERO, m_CurrentVersion, false, false, 0, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000));
2626 void CGitStatusListCtrl::StartDiff(int fileindex)
2628 if(fileindex<0)
2629 return;
2631 CAutoReadLock locker(m_guard);
2632 auto ptr = GetListEntry(fileindex);
2633 if (!ptr)
2634 return;
2635 CTGitPath file1 = *ptr;
2636 CTGitPath file2;
2637 if(file1.m_Action & (CTGitPath::LOGACTIONS_REPLACED|CTGitPath::LOGACTIONS_COPY))
2638 file2.SetFromGit(file1.GetGitOldPathString());
2639 else
2640 file2=file1;
2642 if(this->m_CurrentVersion.IsEmpty() || m_CurrentVersion== GIT_REV_ZERO)
2644 CString fromwhere;
2645 if(m_amend && (file1.m_Action & CTGitPath::LOGACTIONS_ADDED) == 0)
2646 fromwhere = L"~1";
2647 if( g_Git.IsInitRepos())
2648 CGitDiff::DiffNull(GetListEntry(fileindex),
2649 GIT_REV_ZERO, true, 0, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000));
2650 else if( file1.m_Action&CTGitPath::LOGACTIONS_ADDED )
2651 CGitDiff::DiffNull(GetListEntry(fileindex),
2652 m_CurrentVersion + fromwhere, true, 0, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000));
2653 else if( file1.m_Action&CTGitPath::LOGACTIONS_DELETED )
2654 CGitDiff::DiffNull(GetListEntry(fileindex),
2655 GitRev::GetHead() + fromwhere, false, 0, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000));
2656 else
2657 CGitDiff::Diff(&file1,&file2,
2658 CString(GIT_REV_ZERO),
2659 GitRev::GetHead() + fromwhere, false, false, 0, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000));
2661 else
2663 CGitHash hash;
2664 CString fromwhere = m_CurrentVersion + L"~1";
2665 if(m_amend)
2666 fromwhere = m_CurrentVersion + L"~2";
2667 bool revfail = !!g_Git.GetHash(hash, fromwhere);
2668 if (revfail || (file1.m_Action & file1.LOGACTIONS_ADDED))
2669 CGitDiff::DiffNull(&file1, m_CurrentVersion, true, 0, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000));
2670 else if (file1.m_Action & file1.LOGACTIONS_DELETED)
2672 if (file1.m_ParentNo > 0)
2673 fromwhere.Format(L"%s^%d", (LPCTSTR)m_CurrentVersion, file1.m_ParentNo + 1);
2675 CGitDiff::DiffNull(&file1, fromwhere, false, 0, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000));
2677 else
2679 if( file1.m_ParentNo & MERGE_MASK)
2681 CTGitPath base, theirs, mine, merge;
2683 CString temppath;
2684 GetTempPath(temppath);
2685 temppath.TrimRight(L'\\');
2687 mine.SetFromGit(temppath + L'\\' + file1.GetFileOrDirectoryName() + L".LOCAL" + file1.GetFileExtension());
2688 theirs.SetFromGit(temppath + L'\\' + file1.GetFileOrDirectoryName() + L".REMOTE" + file1.GetFileExtension());
2689 base.SetFromGit(temppath + L'\\' + file1.GetFileOrDirectoryName() + L".BASE" + file1.GetFileExtension());
2691 CFile tempfile;
2692 //create a empty file, incase stage is not three
2693 tempfile.Open(mine.GetWinPathString(),CFile::modeCreate|CFile::modeReadWrite);
2694 tempfile.Close();
2695 tempfile.Open(theirs.GetWinPathString(),CFile::modeCreate|CFile::modeReadWrite);
2696 tempfile.Close();
2697 tempfile.Open(base.GetWinPathString(),CFile::modeCreate|CFile::modeReadWrite);
2698 tempfile.Close();
2700 merge.SetFromGit(temppath + L'\\' + file1.GetFileOrDirectoryName() + L".Merged" + file1.GetFileExtension());
2702 int parent1=-1, parent2 =-1;
2703 for (size_t i = 0; i < m_arStatusArray.size(); ++i)
2705 if(m_arStatusArray[i]->GetGitPathString() == file1.GetGitPathString())
2707 if(m_arStatusArray[i]->m_ParentNo & MERGE_MASK)
2710 else
2712 if(parent1<0)
2713 parent1 = m_arStatusArray[i]->m_ParentNo & PARENT_MASK;
2714 else if (parent2 <0)
2715 parent2 = m_arStatusArray[i]->m_ParentNo & PARENT_MASK;
2720 if(g_Git.GetOneFile(m_CurrentVersion, file1, (CString&)merge.GetWinPathString()))
2721 CMessageBox::Show(GetSafeHwnd(), IDS_STATUSLIST_FAILEDGETMERGEFILE, IDS_APPNAME, MB_OK | MB_ICONERROR);
2723 if(parent1>=0)
2725 CString str;
2726 str.Format(L"%s^%d", (LPCTSTR)this->m_CurrentVersion, parent1 + 1);
2728 if(g_Git.GetOneFile(str, file1, (CString&)mine.GetWinPathString()))
2729 CMessageBox::Show(GetSafeHwnd(), IDS_STATUSLIST_FAILEDGETMERGEFILE, IDS_APPNAME, MB_OK | MB_ICONERROR);
2732 if(parent2>=0)
2734 CString str;
2735 str.Format(L"%s^%d", (LPCTSTR)this->m_CurrentVersion, parent2 + 1);
2737 if(g_Git.GetOneFile(str, file1, (CString&)theirs.GetWinPathString()))
2738 CMessageBox::Show(GetSafeHwnd(), IDS_STATUSLIST_FAILEDGETMERGEFILE, IDS_APPNAME, MB_OK | MB_ICONERROR);
2741 if(parent1>=0 && parent2>=0)
2743 CString cmd, output;
2744 cmd.Format(L"git.exe merge-base %s^%d %s^%d", (LPCTSTR)this->m_CurrentVersion, parent1 + 1,
2745 (LPCTSTR)m_CurrentVersion, parent2 + 1);
2747 if (!g_Git.Run(cmd, &output, nullptr, CP_UTF8))
2749 if (g_Git.GetOneFile(output.Left(2 * GIT_HASH_SIZE), file1, (CString&)base.GetWinPathString()))
2750 CMessageBox::Show(GetSafeHwnd(), IDS_STATUSLIST_FAILEDGETBASEFILE, IDS_APPNAME, MB_OK | MB_ICONERROR);
2753 CAppUtils::StartExtMerge(!!(GetAsyncKeyState(VK_SHIFT) & 0x8000), base, theirs, mine, merge, L"BASE", L"REMOTE", L"LOCAL");
2755 else
2757 CString str;
2758 if( (file1.m_ParentNo&PARENT_MASK) == 0)
2759 str = L"~1";
2760 else
2761 str.Format(L"^%d", (file1.m_ParentNo & PARENT_MASK) + 1);
2762 CGitDiff::Diff(&file1,&file2,
2763 m_CurrentVersion,
2764 m_CurrentVersion + str, false, false, 0, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000));
2770 CString CGitStatusListCtrl::GetStatisticsString(bool simple)
2772 CString sNormal = CString(MAKEINTRESOURCE(IDS_STATUSNORMAL));
2773 CString sAdded = CString(MAKEINTRESOURCE(IDS_STATUSADDED));
2774 CString sDeleted = CString(MAKEINTRESOURCE(IDS_STATUSDELETED));
2775 CString sModified = CString(MAKEINTRESOURCE(IDS_STATUSMODIFIED));
2776 CString sConflicted = CString(MAKEINTRESOURCE(IDS_STATUSCONFLICTED));
2777 CString sUnversioned = CString(MAKEINTRESOURCE(IDS_STATUSUNVERSIONED));
2778 CString sRenamed = CString(MAKEINTRESOURCE(IDS_STATUSREPLACED));
2779 CString sToolTip;
2780 if(simple)
2782 sToolTip.Format(IDS_STATUSLIST_STATUSLINE1,
2783 this->m_nLineAdded,this->m_nLineDeleted,
2784 (LPCTSTR)sModified, m_nModified,
2785 (LPCTSTR)sAdded, m_nAdded,
2786 (LPCTSTR)sDeleted, m_nDeleted,
2787 (LPCTSTR)sRenamed, m_nRenamed
2790 else
2792 sToolTip.Format(IDS_STATUSLIST_STATUSLINE2,
2793 this->m_nLineAdded,this->m_nLineDeleted,
2794 (LPCTSTR)sNormal, m_nNormal,
2795 (LPCTSTR)sUnversioned, m_nUnversioned,
2796 (LPCTSTR)sModified, m_nModified,
2797 (LPCTSTR)sAdded, m_nAdded,
2798 (LPCTSTR)sDeleted, m_nDeleted,
2799 (LPCTSTR)sConflicted, m_nConflicted
2802 CString sStats;
2803 sStats.Format(IDS_COMMITDLG_STATISTICSFORMAT, m_nSelected, GetItemCount());
2804 if (m_pStatLabel)
2805 m_pStatLabel->SetWindowText(sStats);
2807 if (m_pSelectButton)
2809 if (m_nSelected == 0)
2810 m_pSelectButton->SetCheck(BST_UNCHECKED);
2811 else if (m_nSelected != GetItemCount())
2812 m_pSelectButton->SetCheck(BST_INDETERMINATE);
2813 else
2814 m_pSelectButton->SetCheck(BST_CHECKED);
2817 if (m_pConfirmButton)
2818 m_pConfirmButton->EnableWindow(m_nSelected>0);
2820 return sToolTip;
2823 CString CGitStatusListCtrl::GetCommonDirectory(bool bStrict)
2825 CAutoReadLock locker(m_guard);
2826 if (!bStrict)
2828 // not strict means that the selected folder has priority
2829 if (!m_StatusFileList.GetCommonDirectory().IsEmpty())
2830 return m_StatusFileList.GetCommonDirectory().GetWinPath();
2833 CTGitPathList list;
2834 int nListItems = GetItemCount();
2835 for (int i=0; i<nListItems; ++i)
2837 auto* entry = GetListEntry(i);
2838 if (entry->IsEmpty())
2839 continue;
2840 list.AddPath(*entry);
2842 return list.GetCommonRoot().GetWinPath();
2845 void CGitStatusListCtrl::SelectAll(bool bSelect, bool /*bIncludeNoCommits*/)
2847 CWaitCursor waitCursor;
2848 // block here so the LVN_ITEMCHANGED messages
2849 // get ignored
2850 ScopedInDecrement blocker(m_nBlockItemChangeHandler);
2853 CAutoWriteLock locker(m_guard);
2854 SetRedraw(FALSE);
2856 int nListItems = GetItemCount();
2857 if (bSelect)
2858 m_nSelected = nListItems;
2859 else
2860 m_nSelected = 0;
2862 for (int i=0; i<nListItems; ++i)
2864 auto path = GetListEntry(i);
2865 if (!path)
2866 continue;
2867 //if ((bIncludeNoCommits)||(entry->GetChangeList().Compare(SVNSLC_IGNORECHANGELIST)))
2868 SetEntryCheck(path,i,bSelect);
2871 SetRedraw(TRUE);
2872 GetStatisticsString();
2873 NotifyCheck();
2876 void CGitStatusListCtrl::Check(DWORD dwCheck, bool check)
2878 CWaitCursor waitCursor;
2879 // block here so the LVN_ITEMCHANGED messages
2880 // get ignored
2882 CAutoWriteLock locker(m_guard);
2883 SetRedraw(FALSE);
2884 ScopedInDecrement blocker(m_nBlockItemChangeHandler);
2886 int nListItems = GetItemCount();
2888 for (int i = 0; i < nListItems; ++i)
2890 auto entry = GetListEntry(i);
2891 if (!entry)
2892 continue;
2894 DWORD showFlags = entry->m_Action;
2895 if (entry->IsDirectory())
2896 showFlags |= GITSLC_SHOWSUBMODULES;
2897 else
2898 showFlags |= GITSLC_SHOWFILES;
2900 if (check && (showFlags & dwCheck) && !GetCheck(i) && !(entry->IsDirectory() && m_bDoNotAutoselectSubmodules && !(dwCheck & GITSLC_SHOWSUBMODULES)))
2902 SetEntryCheck(entry, i, true);
2903 m_nSelected++;
2905 else if (!check && (showFlags & dwCheck) && GetCheck(i))
2907 SetEntryCheck(entry, i, false);
2908 m_nSelected--;
2912 SetRedraw(TRUE);
2913 GetStatisticsString();
2914 NotifyCheck();
2917 void CGitStatusListCtrl::OnLvnGetInfoTip(NMHDR *pNMHDR, LRESULT *pResult)
2919 LPNMLVGETINFOTIP pGetInfoTip = reinterpret_cast<LPNMLVGETINFOTIP>(pNMHDR);
2920 *pResult = 0;
2921 if (CRegDWORD(L"Software\\TortoiseGit\\ShowListFullPathTooltip", TRUE) != TRUE)
2922 return;
2924 CAutoReadWeakLock readLock(m_guard);
2925 if (!readLock.IsAcquired())
2926 return;
2928 auto entry = GetListEntry(pGetInfoTip->iItem);
2930 if (entry)
2931 if (pGetInfoTip->cchTextMax > entry->GetGitPathString().GetLength() + g_Git.m_CurrentDir.GetLength())
2933 CString str = g_Git.CombinePath(entry->GetWinPathString());
2934 wcsncpy_s(pGetInfoTip->pszText, pGetInfoTip->cchTextMax, str.GetBuffer(), pGetInfoTip->cchTextMax - 1);
2938 void CGitStatusListCtrl::OnNMCustomdraw(NMHDR *pNMHDR, LRESULT *pResult)
2940 NMLVCUSTOMDRAW* pLVCD = reinterpret_cast<NMLVCUSTOMDRAW*>( pNMHDR );
2942 // Take the default processing unless we set this to something else below.
2943 *pResult = CDRF_DODEFAULT;
2945 // First thing - check the draw stage. If it's the control's prepaint
2946 // stage, then tell Windows we want messages for every item.
2948 switch (pLVCD->nmcd.dwDrawStage)
2950 case CDDS_PREPAINT:
2951 *pResult = CDRF_NOTIFYITEMDRAW;
2952 break;
2953 case CDDS_ITEMPREPAINT:
2955 // This is the prepaint stage for an item. Here's where we set the
2956 // item's text color. Our return value will tell Windows to draw the
2957 // item itself, but it will use the new color we set here.
2959 // Tell Windows to paint the control itself.
2960 *pResult = CDRF_DODEFAULT;
2961 CAutoReadWeakLock readLock(m_guard, 0);
2962 if (!readLock.IsAcquired())
2963 return;
2965 COLORREF crText = GetSysColor(COLOR_WINDOWTEXT);
2967 if (m_arStatusArray.size() > (DWORD_PTR)pLVCD->nmcd.dwItemSpec)
2969 auto entry = GetListEntry((int)pLVCD->nmcd.dwItemSpec);
2970 if (!entry)
2971 return;
2973 // coloring
2974 // ========
2975 // black : unversioned, normal
2976 // purple : added
2977 // blue : modified
2978 // brown : missing, deleted, replaced
2979 // green : merged (or potential merges)
2980 // red : conflicts or sure conflicts
2981 if(entry->m_Action & CTGitPath::LOGACTIONS_GRAY)
2982 crText = RGB(128,128,128);
2983 else if(entry->m_Action & CTGitPath::LOGACTIONS_UNMERGED)
2984 crText = m_Colors.GetColor(CColors::Conflict);
2985 else if(entry->m_Action & (CTGitPath::LOGACTIONS_MODIFIED))
2986 crText = m_Colors.GetColor(CColors::Modified);
2987 else if(entry->m_Action & (CTGitPath::LOGACTIONS_ADDED|CTGitPath::LOGACTIONS_COPY))
2988 crText = m_Colors.GetColor(CColors::Added);
2989 else if(entry->m_Action & CTGitPath::LOGACTIONS_DELETED)
2990 crText = m_Colors.GetColor(CColors::Deleted);
2991 else if(entry->m_Action & CTGitPath::LOGACTIONS_REPLACED)
2992 crText = m_Colors.GetColor(CColors::Renamed);
2993 else if(entry->m_Action & CTGitPath::LOGACTIONS_MERGED)
2994 crText = m_Colors.GetColor(CColors::Merged);
2995 else
2996 crText = GetSysColor(COLOR_WINDOWTEXT);
2997 // Store the color back in the NMLVCUSTOMDRAW struct.
2998 pLVCD->clrText = crText;
3001 break;
3005 void CGitStatusListCtrl::OnLvnGetdispinfo(NMHDR* pNMHDR, LRESULT* pResult)
3007 auto pDispInfo = reinterpret_cast<NMLVDISPINFO*>(pNMHDR);
3008 *pResult = 0;
3010 // Create a pointer to the item
3011 LV_ITEM* pItem = &(pDispInfo)->item;
3013 CAutoReadWeakLock readLock(m_guard, 0);
3014 if (readLock.IsAcquired())
3016 if (pItem->mask & LVIF_TEXT)
3018 CString text = GetCellText(pItem->iItem, pItem->iSubItem);
3019 lstrcpyn(pItem->pszText, text, pItem->cchTextMax - 1);
3022 else
3023 pItem->mask = 0;
3026 BOOL CGitStatusListCtrl::OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message)
3028 if (pWnd != this)
3029 return CListCtrl::OnSetCursor(pWnd, nHitTest, message);
3030 if (!m_bWaitCursor && !m_bBusy)
3032 HCURSOR hCur = LoadCursor(nullptr, IDC_ARROW);
3033 SetCursor(hCur);
3034 return CListCtrl::OnSetCursor(pWnd, nHitTest, message);
3036 HCURSOR hCur = LoadCursor(nullptr, IDC_WAIT);
3037 SetCursor(hCur);
3038 return TRUE;
3041 void CGitStatusListCtrl::RemoveListEntry(int index)
3043 CAutoWriteLock locker(m_guard);
3044 DeleteItem(index);
3046 m_arStatusArray.erase(m_arStatusArray.cbegin() + index);
3048 #if 0
3049 delete m_arStatusArray[m_arListArray[index]];
3050 m_arStatusArray.erase(m_arStatusArray.begin()+m_arListArray[index]);
3051 m_arListArray.erase(m_arListArray.begin()+index);
3052 for (int i=index; i< (int)m_arListArray.size(); ++i)
3054 m_arListArray[i]--;
3056 #endif
3059 ///< Set a checkbox on an entry in the listbox
3060 // NEVER, EVER call SetCheck directly, because you'll end-up with the checkboxes and the 'checked' flag getting out of sync
3061 void CGitStatusListCtrl::SetEntryCheck(CTGitPath* pEntry, int listboxIndex, bool bCheck)
3063 CAutoWriteLock locker(m_guard);
3064 pEntry->m_Checked = bCheck;
3065 m_mapFilenameToChecked[pEntry->GetGitPathString()] = bCheck;
3066 SetCheck(listboxIndex, bCheck);
3069 void CGitStatusListCtrl::ResetChecked(const CTGitPath& entry)
3071 CAutoWriteLock locker(m_guard);
3072 CTGitPath adjustedEntry;
3073 if (g_Git.m_CurrentDir[g_Git.m_CurrentDir.GetLength() - 1] == L'\\')
3074 adjustedEntry.SetFromWin(entry.GetWinPathString().Right(entry.GetWinPathString().GetLength() - g_Git.m_CurrentDir.GetLength()));
3075 else
3076 adjustedEntry.SetFromWin(entry.GetWinPathString().Right(entry.GetWinPathString().GetLength() - g_Git.m_CurrentDir.GetLength() - 1));
3077 if (entry.IsDirectory())
3079 STRING_VECTOR toDelete;
3080 for (auto it = m_mapFilenameToChecked.begin(); it != m_mapFilenameToChecked.end(); ++it)
3082 if (adjustedEntry.IsAncestorOf(it->first))
3083 toDelete.emplace_back(it->first);
3085 for (const auto& file : toDelete)
3086 m_mapFilenameToChecked.erase(file);
3087 return;
3089 m_mapFilenameToChecked.erase(adjustedEntry.GetGitPathString());
3092 #if 0
3093 void CGitStatusListCtrl::SetCheckOnAllDescendentsOf(const FileEntry* parentEntry, bool bCheck)
3095 CAutoWriteLock locker(m_guard);
3096 int nListItems = GetItemCount();
3097 for (int j=0; j< nListItems ; ++j)
3099 FileEntry * childEntry = GetListEntry(j);
3100 ASSERT(childEntry);
3101 if (!childEntry || childEntry == parentEntry)
3102 continue;
3103 if (childEntry->checked != bCheck)
3105 if (parentEntry->path.IsAncestorOf(childEntry->path))
3107 SetEntryCheck(childEntry,j,bCheck);
3108 if(bCheck)
3109 m_nSelected++;
3110 else
3111 m_nSelected--;
3116 #endif
3118 void CGitStatusListCtrl::WriteCheckedNamesToPathList(CTGitPathList& pathList)
3120 pathList.Clear();
3121 CAutoReadLock locker(m_guard);
3122 int nListItems = GetItemCount();
3123 for (int i = 0; i< nListItems; ++i)
3125 auto entry = GetListEntry(i);
3126 if (entry->m_Checked)
3127 pathList.AddPath(*entry);
3129 pathList.SortByPathname();
3133 /// Build a path list of all the selected items in the list (NOTE - SELECTED, not CHECKED)
3134 void CGitStatusListCtrl::FillListOfSelectedItemPaths(CTGitPathList& pathList, bool /*bNoIgnored*/)
3136 pathList.Clear();
3138 CAutoReadLock locker(m_guard);
3139 POSITION pos = GetFirstSelectedItemPosition();
3140 int index;
3141 while ((index = GetNextSelectedItem(pos)) >= 0)
3143 auto entry = GetListEntry(index);
3144 //if ((bNoIgnored)&&(entry->status == git_wc_status_ignored))
3145 // continue;
3146 pathList.AddPath(*entry);
3150 UINT CGitStatusListCtrl::OnGetDlgCode()
3152 // we want to process the return key and not have that one
3153 // routed to the default pushbutton
3154 return CListCtrl::OnGetDlgCode() | DLGC_WANTALLKEYS;
3157 void CGitStatusListCtrl::OnNMReturn(NMHDR * /*pNMHDR*/, LRESULT *pResult)
3159 *pResult = 0;
3160 CAutoReadWeakLock readLock(m_guard);
3161 if (!readLock.IsAcquired())
3162 return;
3163 if (!CheckMultipleDiffs())
3164 return;
3165 bool needsRefresh = false;
3166 bool resolvedTreeConfict = false;
3167 POSITION pos = GetFirstSelectedItemPosition();
3168 while ( pos )
3170 int index = GetNextSelectedItem(pos);
3171 if (index < 0)
3172 return;
3173 auto file = GetListEntry(index);
3174 if (file == nullptr)
3175 return;
3176 if (file->m_Action & (CTGitPath::LOGACTIONS_UNVER | CTGitPath::LOGACTIONS_IGNORE))
3177 StartDiffWC(index);
3178 else if ((file->m_Action & CTGitPath::LOGACTIONS_UNMERGED))
3180 if (CAppUtils::ConflictEdit(*file, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000), m_bIsRevertTheirMy, GetLogicalParent() ? GetLogicalParent()->GetSafeHwnd() : nullptr))
3182 CString conflictedFile = g_Git.CombinePath(file);
3183 needsRefresh = needsRefresh || !PathFileExists(conflictedFile);
3184 resolvedTreeConfict = resolvedTreeConfict || (file->m_Action & CTGitPath::LOGACTIONS_UNMERGED) == 0;
3187 else if ((file->m_Action & CTGitPath::LOGACTIONS_MISSING) && file->m_Action != (CTGitPath::LOGACTIONS_MISSING | CTGitPath::LOGACTIONS_DELETED) && file->m_Action != (CTGitPath::LOGACTIONS_MISSING | CTGitPath::LOGACTIONS_DELETED | CTGitPath::LOGACTIONS_MODIFIED))
3188 continue;
3189 else
3191 if (!m_Rev1.IsEmpty() && !m_Rev2.IsEmpty())
3192 StartDiffTwo(index);
3193 else
3194 StartDiff(index);
3197 if (needsRefresh && GetLogicalParent() && GetLogicalParent()->GetSafeHwnd())
3198 GetLogicalParent()->SendMessage(GITSLNM_NEEDSREFRESH);
3199 else if (resolvedTreeConfict)
3201 StoreScrollPos();
3202 Show(m_dwShow, 0, m_bShowFolders, 0, true);
3206 void CGitStatusListCtrl::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags)
3208 // Since we catch all keystrokes (to have the enter key processed here instead
3209 // of routed to the default pushbutton) we have to make sure that other
3210 // keys like Tab and Esc still do what they're supposed to do
3211 // Tab = change focus to next/previous control
3212 // Esc = quit the dialog
3213 switch (nChar)
3215 case (VK_TAB):
3217 ::PostMessage(GetLogicalParent()->GetSafeHwnd(), WM_NEXTDLGCTL, GetKeyState(VK_SHIFT)&0x8000, 0);
3218 return;
3220 break;
3221 case (VK_ESCAPE):
3223 ::SendMessage(GetLogicalParent()->GetSafeHwnd(), WM_CLOSE, 0, 0);
3225 break;
3228 CListCtrl::OnKeyDown(nChar, nRepCnt, nFlags);
3231 void CGitStatusListCtrl::PreSubclassWindow()
3233 __super::PreSubclassWindow();
3234 EnableToolTips(TRUE);
3235 SetWindowTheme(GetSafeHwnd(), L"Explorer", nullptr);
3238 void CGitStatusListCtrl::OnPaint()
3240 LRESULT defres = Default();
3241 if ((m_bBusy) || (m_bEmpty))
3243 CString str;
3244 if (m_bBusy)
3246 if (m_sBusy.IsEmpty())
3247 str.LoadString(IDS_STATUSLIST_BUSYMSG);
3248 else
3249 str = m_sBusy;
3251 else
3253 if (m_sEmpty.IsEmpty())
3254 str.LoadString(IDS_STATUSLIST_EMPTYMSG);
3255 else
3256 str = m_sEmpty;
3258 COLORREF clrText = ::GetSysColor(COLOR_WINDOWTEXT);
3259 COLORREF clrTextBk;
3260 if (IsWindowEnabled())
3261 clrTextBk = ::GetSysColor(COLOR_WINDOW);
3262 else
3263 clrTextBk = ::GetSysColor(COLOR_3DFACE);
3265 CRect rc;
3266 GetClientRect(&rc);
3267 CHeaderCtrl* pHC = GetHeaderCtrl();
3268 if (pHC)
3270 CRect rcH;
3271 pHC->GetItemRect(0, &rcH);
3272 rc.top += rcH.bottom;
3274 CDC* pDC = GetDC();
3276 CMyMemDC memDC(pDC, &rc);
3278 memDC.SetTextColor(clrText);
3279 memDC.SetBkColor(clrTextBk);
3280 memDC.BitBlt(rc.left, rc.top, rc.Width(), rc.Height(), pDC, rc.left, rc.top, SRCCOPY);
3281 rc.top += 10;
3282 CGdiObject * oldfont = memDC.SelectStockObject(DEFAULT_GUI_FONT);
3283 memDC.DrawText(str, rc, DT_CENTER | DT_VCENTER |
3284 DT_WORDBREAK | DT_NOPREFIX | DT_NOCLIP);
3285 memDC.SelectObject(oldfont);
3287 ReleaseDC(pDC);
3289 if (defres)
3291 // the Default() call did not process the WM_PAINT message!
3292 // Validate the update region ourselves to avoid
3293 // an endless loop repainting
3294 CRect rc;
3295 GetUpdateRect(&rc, FALSE);
3296 if (!rc.IsRectEmpty())
3297 ValidateRect(rc);
3301 void CGitStatusListCtrl::OnBeginDrag(NMHDR* pNMHDR, LRESULT* pResult)
3303 LPNMLISTVIEW pNMLV = reinterpret_cast<LPNMLISTVIEW>(pNMHDR);
3305 CAutoReadLock locker(m_guard);
3307 CTGitPathList pathList;
3308 FillListOfSelectedItemPaths(pathList);
3309 if (pathList.IsEmpty())
3310 return;
3312 auto pdsrc = std::make_unique<CIDropSource>();
3313 if (!pdsrc)
3314 return;
3315 pdsrc->AddRef();
3317 GitDataObject* pdobj = new GitDataObject(pathList, m_Rev2.IsEmpty() ? m_CurrentVersion : m_Rev2);
3318 if (!pdobj)
3319 return;
3320 pdobj->AddRef();
3322 CDragSourceHelper dragsrchelper;
3324 SetRedraw(false);
3325 dragsrchelper.InitializeFromWindow(m_hWnd, pNMLV->ptAction, pdobj);
3326 SetRedraw(true);
3327 //dragsrchelper.InitializeFromBitmap()
3328 pdsrc->m_pIDataObj = pdobj;
3329 pdsrc->m_pIDataObj->AddRef();
3331 // Initiate the Drag & Drop
3332 DWORD dwEffect;
3333 m_bOwnDrag = true;
3334 ::DoDragDrop(pdobj, pdsrc.get(), DROPEFFECT_MOVE | DROPEFFECT_COPY, &dwEffect);
3335 m_bOwnDrag = false;
3336 pdsrc->Release();
3337 pdsrc.release();
3338 pdobj->Release();
3340 *pResult = 0;
3343 bool CGitStatusListCtrl::EnableFileDrop()
3345 m_bFileDropsEnabled = true;
3346 return true;
3349 bool CGitStatusListCtrl::HasPath(const CTGitPath& path)
3351 CAutoReadLock locker(m_guard);
3352 CTGitPath adjustedEntry;
3353 if (g_Git.m_CurrentDir[g_Git.m_CurrentDir.GetLength() - 1] == L'\\')
3354 adjustedEntry.SetFromWin(path.GetWinPathString().Right(path.GetWinPathString().GetLength() - g_Git.m_CurrentDir.GetLength()));
3355 else
3356 adjustedEntry.SetFromWin(path.GetWinPathString().Right(path.GetWinPathString().GetLength() - g_Git.m_CurrentDir.GetLength() - 1));
3357 for (size_t i=0; i < m_arStatusArray.size(); ++i)
3359 if (m_arStatusArray[i]->IsEquivalentTo(adjustedEntry))
3360 return true;
3363 return false;
3366 BOOL CGitStatusListCtrl::PreTranslateMessage(MSG* pMsg)
3368 if (pMsg->message == WM_KEYDOWN)
3370 switch (pMsg->wParam)
3372 case 'A':
3374 if (GetAsyncKeyState(VK_CONTROL)&0x8000)
3376 // select all entries
3377 for (int i=0; i<GetItemCount(); ++i)
3378 SetItemState(i, LVIS_SELECTED, LVIS_SELECTED);
3379 return TRUE;
3382 break;
3383 case 'C':
3384 case VK_INSERT:
3386 if (GetAsyncKeyState(VK_CONTROL)&0x8000)
3388 // copy all selected paths to the clipboard
3389 if (GetAsyncKeyState(VK_SHIFT)&0x8000)
3390 CopySelectedEntriesToClipboard(GITSLC_COLFILENAME | GITSLC_COLSTATUS, IDGITLC_COPYRELPATHS);
3391 else
3392 CopySelectedEntriesToClipboard(GITSLC_COLFILENAME, IDGITLC_COPYRELPATHS);
3393 return TRUE;
3396 break;
3397 case VK_DELETE:
3399 if ((GetSelectedCount() > 0) && (m_dwContextMenus & GITSLC_POPDELETE))
3401 CAutoReadLock locker(m_guard);
3402 auto filepath = GetListEntry(GetSelectionMark());
3403 if (filepath != nullptr && (filepath->m_Action & (CTGitPath::LOGACTIONS_UNVER | CTGitPath::LOGACTIONS_IGNORE)))
3404 DeleteSelectedFiles();
3407 break;
3411 return __super::PreTranslateMessage(pMsg);
3414 bool CGitStatusListCtrl::CopySelectedEntriesToClipboard(DWORD dwCols, int cmd)
3416 if (GetSelectedCount() == 0)
3417 return false;
3419 CString sClipboard;
3421 bool bMultipleColumnSelected = ((dwCols & dwCols - 1) != 0); // multiple columns are selected (clear least signifient bit and check for zero)
3423 #define ADDTOCLIPBOARDSTRING(x) sClipboard += (sClipboard.IsEmpty() || (sClipboard.Right(1)==L"\n")) ? (x) : ('\t' + x)
3424 #define ADDNEWLINETOCLIPBOARDSTRING() sClipboard += (sClipboard.IsEmpty()) ? L"" : L"\r\n"
3426 // first add the column titles as the first line
3427 DWORD selection = 0;
3428 int count = m_ColumnManager.GetColumnCount();
3429 for (int column = 0; column < count; ++column)
3431 if ((dwCols == -1 && m_ColumnManager.IsVisible(column)) || (column < GITSLC_NUMCOLUMNS && (dwCols & (1 << column))))
3433 if (bMultipleColumnSelected)
3434 ADDTOCLIPBOARDSTRING(m_ColumnManager.GetName(column));
3436 selection |= 1 << column;
3440 if (bMultipleColumnSelected)
3441 ADDNEWLINETOCLIPBOARDSTRING();
3443 // maybe clear first line when only one column is selected (btw by select not by dwCols) is simpler(not faster) way
3444 // but why no title on single column output ?
3445 // if (selection & selection-1) == 0 ) sClipboard = "";
3447 CAutoReadLock locker(m_guard);
3449 POSITION pos = GetFirstSelectedItemPosition();
3450 while (pos)
3452 int index = GetNextSelectedItem(pos);
3453 // we selected only cols we want, so not other then select test needed
3454 for (int column = 0; column < count; ++column)
3456 if (cmd && (GITSLC_COLFILENAME & (1 << column)))
3458 auto* entry = GetListEntry(index);
3459 if (entry)
3461 CString sPath;
3462 switch (cmd)
3464 case IDGITLC_COPYFULL:
3465 sPath = g_Git.CombinePath(entry);
3466 break;
3467 case IDGITLC_COPYRELPATHS:
3468 sPath = entry->GetGitPathString();
3469 break;
3470 case IDGITLC_COPYFILENAMES:
3471 sPath = entry->GetFileOrDirectoryName();
3472 break;
3474 ADDTOCLIPBOARDSTRING(sPath);
3477 else if (selection & (1 << column))
3478 ADDTOCLIPBOARDSTRING(GetCellText(index, column));
3481 ADDNEWLINETOCLIPBOARDSTRING();
3484 return CStringUtils::WriteAsciiStringToClipboard(sClipboard);
3487 size_t CGitStatusListCtrl::GetNumberOfChangelistsInSelection()
3489 #if 0
3490 CAutoReadLock locker(m_guard);
3491 std::set<CString> changelists;
3492 POSITION pos = GetFirstSelectedItemPosition();
3493 int index;
3494 while ((index = GetNextSelectedItem(pos)) >= 0)
3496 FileEntry * entry = GetListEntry(index);
3497 if (!entry->changelist.IsEmpty())
3498 changelists.insert(entry->changelist);
3500 return changelists.size();
3501 #endif
3502 return 0;
3505 bool CGitStatusListCtrl::PrepareGroups(bool bForce /* = false */)
3507 CAutoWriteLock locker(m_guard);
3508 bool bHasGroups=false;
3509 int max =0;
3511 for (size_t i = 0; i < m_arStatusArray.size(); ++i)
3513 int ParentNo = m_arStatusArray[i]->m_ParentNo&PARENT_MASK;
3514 if( ParentNo > max)
3515 max=m_arStatusArray[i]->m_ParentNo&PARENT_MASK;
3518 if (((m_dwShow & GITSLC_SHOWUNVERSIONED) && !m_UnRevFileList.IsEmpty()) ||
3519 ((m_dwShow & GITSLC_SHOWIGNORED) && !m_IgnoreFileList.IsEmpty()) ||
3520 (m_dwShow & (GITSLC_SHOWASSUMEVALID | GITSLC_SHOWSKIPWORKTREE) && !m_LocalChangesIgnoredFileList.IsEmpty()) ||
3521 max>0 || bForce)
3523 bHasGroups = true;
3526 RemoveAllGroups();
3527 EnableGroupView(bHasGroups);
3529 TCHAR groupname[1024] = { 0 };
3530 int groupindex = 0;
3532 if(bHasGroups)
3534 LVGROUP grp = {0};
3535 grp.cbSize = sizeof(LVGROUP);
3536 grp.mask = LVGF_ALIGN | LVGF_GROUPID | LVGF_HEADER;
3537 groupindex=0;
3539 //if(m_UnRevFileList.GetCount()>0)
3540 if(max >0)
3542 wcsncpy_s(groupname, 1024, (LPCTSTR)CString(MAKEINTRESOURCE(IDS_STATUSLIST_GROUP_MERGEDFILES)), 1023);
3543 grp.pszHeader = groupname;
3544 grp.iGroupId = MERGE_MASK;
3545 grp.uAlign = LVGA_HEADER_LEFT;
3546 InsertGroup(0, &grp);
3548 CAutoRepository repository(g_Git.GetGitRepository());
3549 if (!repository)
3550 MessageBox(CGit::GetLibGit2LastErr(L"Could not open repository."), L"TortoiseGit", MB_OK | MB_ICONERROR);
3551 for (int i = 0; i <= max; ++i)
3553 CString str;
3554 str.Format(IDS_STATUSLIST_GROUP_DIFFWITHPARENT, i+1);
3555 if (repository)
3557 CString rev;
3558 rev.Format(L"%s^%d", (LPCTSTR)m_CurrentVersion, i + 1);
3559 CGitHash hash;
3560 if (!CGit::GetHash(repository, hash, rev))
3561 str += L": " + hash.ToString().Left(g_Git.GetShortHASHLength());
3563 grp.pszHeader = str.GetBuffer();
3564 str.ReleaseBuffer();
3565 grp.iGroupId = i;
3566 grp.uAlign = LVGA_HEADER_LEFT;
3567 InsertGroup(i+1, &grp);
3570 else
3572 wcsncpy_s(groupname, 1024, (LPCTSTR)CString(MAKEINTRESOURCE(IDS_STATUSLIST_GROUP_MODIFIEDFILES)), 1023);
3573 grp.pszHeader = groupname;
3574 grp.iGroupId = groupindex;
3575 grp.uAlign = LVGA_HEADER_LEFT;
3576 InsertGroup(groupindex++, &grp);
3579 wcsncpy_s(groupname, 1024, (LPCTSTR)CString(MAKEINTRESOURCE(IDS_STATUSLIST_GROUP_NOTVERSIONEDFILES)), 1023);
3580 grp.pszHeader = groupname;
3581 grp.iGroupId = groupindex;
3582 grp.uAlign = LVGA_HEADER_LEFT;
3583 InsertGroup(groupindex++, &grp);
3586 //if(m_IgnoreFileList.GetCount()>0)
3588 wcsncpy_s(groupname, 1024, (LPCTSTR)CString(MAKEINTRESOURCE(IDS_STATUSLIST_GROUP_IGNOREDFILES)), 1023);
3589 grp.pszHeader = groupname;
3590 grp.iGroupId = groupindex;
3591 grp.uAlign = LVGA_HEADER_LEFT;
3592 InsertGroup(groupindex++, &grp);
3596 wcsncpy_s(groupname, 1024, (LPCTSTR)CString(MAKEINTRESOURCE(IDS_STATUSLIST_GROUP_IGNORELOCALCHANGES)), 1023);
3597 grp.pszHeader = groupname;
3598 grp.iGroupId = groupindex;
3599 grp.uAlign = LVGA_HEADER_LEFT;
3600 InsertGroup(groupindex++, &grp);
3605 #if 0
3606 m_bHasIgnoreGroup = false;
3608 // now add the items which don't belong to a group
3609 LVGROUP grp = {0};
3610 grp.cbSize = sizeof(LVGROUP);
3611 grp.mask = LVGF_ALIGN | LVGF_GROUPID | LVGF_HEADER;
3612 CString sUnassignedName(MAKEINTRESOURCE(IDS_STATUSLIST_UNASSIGNED_CHANGESET));
3613 wcsncpy_s(groupname, 1024, (LPCTSTR)sUnassignedName, 1023);
3614 grp.pszHeader = groupname;
3615 grp.iGroupId = groupindex;
3616 grp.uAlign = LVGA_HEADER_LEFT;
3617 InsertGroup(groupindex++, &grp);
3619 for (auto it = m_changelists.cbegin(); it != m_changelists.cend(); ++it)
3621 if (it->first.Compare(SVNSLC_IGNORECHANGELIST)!=0)
3623 LVGROUP grp = {0};
3624 grp.cbSize = sizeof(LVGROUP);
3625 grp.mask = LVGF_ALIGN | LVGF_GROUPID | LVGF_HEADER;
3626 wcsncpy_s(groupname, 1024, it->first, 1023);
3627 grp.pszHeader = groupname;
3628 grp.iGroupId = groupindex;
3629 grp.uAlign = LVGA_HEADER_LEFT;
3630 it->second = InsertGroup(groupindex++, &grp);
3632 else
3633 m_bHasIgnoreGroup = true;
3636 if (m_bHasIgnoreGroup)
3638 // and now add the group 'ignore-on-commit'
3639 std::map<CString,int>::iterator it = m_changelists.find(SVNSLC_IGNORECHANGELIST);
3640 if (it != m_changelists.end())
3642 grp.cbSize = sizeof(LVGROUP);
3643 grp.mask = LVGF_ALIGN | LVGF_GROUPID | LVGF_HEADER;
3644 wcsncpy_s(groupname, 1024, SVNSLC_IGNORECHANGELIST, 1023);
3645 grp.pszHeader = groupname;
3646 grp.iGroupId = groupindex;
3647 grp.uAlign = LVGA_HEADER_LEFT;
3648 it->second = InsertGroup(groupindex, &grp);
3651 #endif
3652 return bHasGroups;
3655 void CGitStatusListCtrl::NotifyCheck()
3657 CWnd* pParent = GetLogicalParent();
3658 if (pParent && pParent->GetSafeHwnd())
3660 pParent->SendMessage(GITSLNM_CHECKCHANGED, m_nSelected);
3664 int CGitStatusListCtrl::UpdateFileList(const CTGitPathList* list)
3666 CAutoWriteLock locker(m_guard);
3667 m_CurrentVersion = GIT_REV_ZERO;
3669 g_Git.GetWorkingTreeChanges(m_StatusFileList, m_amend, list);
3671 BOOL bDeleteChecked = FALSE;
3672 int deleteFromIndex = 0;
3673 bool needsRefresh = false;
3674 for (int i = 0; i < m_StatusFileList.GetCount(); ++i)
3676 auto gitpatch = const_cast<CTGitPath*>(&m_StatusFileList[i]);
3677 gitpatch->m_Checked = TRUE;
3679 if ((gitpatch->m_Action & (CTGitPath::LOGACTIONS_ADDED | CTGitPath::LOGACTIONS_REPLACED | CTGitPath::LOGACTIONS_MODIFIED)) && !gitpatch->Exists())
3681 if (!bDeleteChecked)
3683 CString message;
3684 message.Format(IDS_ASK_REMOVE_FROM_INDEX, gitpatch->GetWinPath());
3685 deleteFromIndex = CMessageBox::ShowCheck(GetSafeHwnd(), message, L"TortoiseGit", 1, IDI_EXCLAMATION, CString(MAKEINTRESOURCE(IDS_RESTORE_FROM_INDEX)), CString(MAKEINTRESOURCE(IDS_REMOVE_FROM_INDEX)), CString(MAKEINTRESOURCE(IDS_IGNOREBUTTON)), nullptr, CString(MAKEINTRESOURCE(IDS_DO_SAME_FOR_REST)), &bDeleteChecked);
3687 if (deleteFromIndex == 1)
3689 CString err;
3690 if (g_Git.Run(L"git.exe checkout -- \"" + gitpatch->GetWinPathString() + L'"', &err, CP_UTF8))
3691 MessageBox(L"Restoring from index failed:\n" + err, L"TortoiseGit", MB_ICONERROR);
3692 else
3693 needsRefresh = true;
3695 else if (deleteFromIndex == 2)
3697 CString err;
3698 if (g_Git.Run(L"git.exe rm -f --cache -- \"" + gitpatch->GetWinPathString() + L'"', &err, CP_UTF8))
3699 MessageBox(L"Removing from index failed:\n" + err, L"TortoiseGit", MB_ICONERROR);
3700 else
3701 needsRefresh = true;
3705 m_arStatusArray.push_back(&m_StatusFileList[i]);
3708 if (needsRefresh)
3709 MessageBox(L"Due to changes to the index, please refresh the dialog (e.g., by pressing F5).", L"TortoiseGit", MB_ICONINFORMATION);
3711 return 0;
3714 int CGitStatusListCtrl::UpdateWithGitPathList(CTGitPathList &list)
3716 CAutoWriteLock locker(m_guard);
3717 m_arStatusArray.clear();
3718 for (int i = 0; i < list.GetCount(); ++i)
3720 auto gitpath = const_cast<CTGitPath*>(&list[i]);
3722 if(gitpath ->m_Action & CTGitPath::LOGACTIONS_HIDE)
3723 continue;
3725 gitpath->m_Checked = TRUE;
3726 m_arStatusArray.push_back(&list[i]);
3728 return 0;
3731 int CGitStatusListCtrl::UpdateUnRevFileList(CTGitPathList &list)
3733 CAutoWriteLock locker(m_guard);
3734 m_UnRevFileList = list;
3735 for (int i = 0; i < m_UnRevFileList.GetCount(); ++i)
3737 auto gitpatch = const_cast<CTGitPath*>(&m_UnRevFileList[i]);
3738 gitpatch->m_Checked = FALSE;
3739 m_arStatusArray.push_back(&m_UnRevFileList[i]);
3741 return 0;
3744 int CGitStatusListCtrl::UpdateUnRevFileList(const CTGitPathList* List)
3746 CAutoWriteLock locker(m_guard);
3747 CString err;
3748 if (m_UnRevFileList.FillUnRev(CTGitPath::LOGACTIONS_UNVER, List, &err))
3750 MessageBox(L"Failed to get UnRev file list\n" + err, L"TortoiseGit", MB_OK | MB_ICONERROR);
3751 return -1;
3754 if (m_StatusFileList.m_Action & CTGitPath::LOGACTIONS_DELETED)
3756 int unrev = 0;
3757 int status = 0;
3758 while (unrev < m_UnRevFileList.GetCount() && status < m_StatusFileList.GetCount())
3760 auto cmp = CTGitPath::Compare(m_UnRevFileList[unrev], m_StatusFileList[status]);
3761 if (cmp < 1)
3763 ++unrev;
3764 continue;
3766 if (cmp == 1)
3767 m_UnRevFileList.RemovePath(m_StatusFileList[status]);
3768 ++status;
3772 for (int i = 0; i < m_UnRevFileList.GetCount(); ++i)
3774 auto gitpatch = const_cast<CTGitPath*>(&m_UnRevFileList[i]);
3775 gitpatch->m_Checked = FALSE;
3776 m_arStatusArray.push_back(&m_UnRevFileList[i]);
3778 return 0;
3781 int CGitStatusListCtrl::UpdateIgnoreFileList(const CTGitPathList* List)
3783 CAutoWriteLock locker(m_guard);
3784 CString err;
3785 if (m_IgnoreFileList.FillUnRev(CTGitPath::LOGACTIONS_IGNORE, List, &err))
3787 MessageBox(L"Failed to get Ignore file list\n" + err, L"TortoiseGit", MB_OK | MB_ICONERROR);
3788 return -1;
3791 for (int i = 0; i < m_IgnoreFileList.GetCount(); ++i)
3793 auto gitpatch = const_cast<CTGitPath*>(&m_IgnoreFileList[i]);
3794 gitpatch->m_Checked = FALSE;
3795 m_arStatusArray.push_back(&m_IgnoreFileList[i]);
3797 return 0;
3800 int CGitStatusListCtrl::UpdateLocalChangesIgnoredFileList(const CTGitPathList* list)
3802 CAutoWriteLock locker(m_guard);
3803 m_LocalChangesIgnoredFileList.FillBasedOnIndexFlags(GIT_IDXENTRY_VALID, GIT_IDXENTRY_SKIP_WORKTREE, list);
3804 for (int i = 0; i < m_LocalChangesIgnoredFileList.GetCount(); ++i)
3806 auto gitpatch = const_cast<CTGitPath*>(&m_LocalChangesIgnoredFileList[i]);
3807 gitpatch->m_Checked = FALSE;
3808 m_arStatusArray.push_back(&m_LocalChangesIgnoredFileList[i]);
3810 return 0;
3813 int CGitStatusListCtrl::UpdateFileList(int mask, bool once, const CTGitPathList* List)
3815 CAutoWriteLock locker(m_guard);
3816 if(mask&CGitStatusListCtrl::FILELIST_MODIFY)
3818 if(once || (!(m_FileLoaded&CGitStatusListCtrl::FILELIST_MODIFY)))
3820 UpdateFileList(List);
3821 m_FileLoaded|=CGitStatusListCtrl::FILELIST_MODIFY;
3824 if (mask & CGitStatusListCtrl::FILELIST_UNVER || mask & CGitStatusListCtrl::FILELIST_IGNORE)
3826 if(once || (!(m_FileLoaded&CGitStatusListCtrl::FILELIST_UNVER)))
3828 UpdateUnRevFileList(List);
3829 m_FileLoaded|=CGitStatusListCtrl::FILELIST_UNVER;
3831 if(mask&CGitStatusListCtrl::FILELIST_IGNORE && (once || (!(m_FileLoaded&CGitStatusListCtrl::FILELIST_IGNORE))))
3833 UpdateIgnoreFileList(List);
3834 m_FileLoaded |= CGitStatusListCtrl::FILELIST_IGNORE;
3837 if (mask & CGitStatusListCtrl::FILELIST_LOCALCHANGESIGNORED && (once || (!(m_FileLoaded & CGitStatusListCtrl::FILELIST_LOCALCHANGESIGNORED))))
3839 UpdateLocalChangesIgnoredFileList(List);
3840 m_FileLoaded |= CGitStatusListCtrl::FILELIST_LOCALCHANGESIGNORED;
3842 return 0;
3845 void CGitStatusListCtrl::Clear()
3847 CAutoWriteLock locker(m_guard);
3848 m_FileLoaded=0;
3849 this->DeleteAllItems();
3850 this->m_arListArray.clear();
3851 this->m_arStatusArray.clear();
3852 this->m_changelists.clear();
3855 bool CGitStatusListCtrl::CheckMultipleDiffs()
3857 UINT selCount = GetSelectedCount();
3858 if (selCount > max(3, (DWORD)CRegDWORD(L"Software\\TortoiseGit\\NumDiffWarning", 10)))
3860 CString message;
3861 message.Format(IDS_STATUSLIST_WARN_MAXDIFF, selCount);
3862 return MessageBox(message, L"TortoiseGit", MB_YESNO | MB_ICONQUESTION) == IDYES;
3864 return true;
3867 //////////////////////////////////////////////////////////////////////////
3868 bool CGitStatusListCtrlDropTarget::OnDrop(FORMATETC* pFmtEtc, STGMEDIUM& medium, DWORD* /*pdwEffect*/, POINTL pt)
3870 if (pFmtEtc->cfFormat == CF_HDROP && medium.tymed == TYMED_HGLOBAL)
3872 HDROP hDrop = (HDROP)GlobalLock(medium.hGlobal);
3873 if (hDrop)
3875 TCHAR szFileName[MAX_PATH] = {0};
3877 UINT cFiles = DragQueryFile(hDrop, 0xFFFFFFFF, nullptr, 0);
3879 POINT clientpoint;
3880 clientpoint.x = pt.x;
3881 clientpoint.y = pt.y;
3882 ScreenToClient(m_hTargetWnd, &clientpoint);
3883 if ((m_pGitStatusListCtrl->IsGroupViewEnabled()) && (m_pGitStatusListCtrl->GetGroupFromPoint(&clientpoint) >= 0))
3885 #if 0
3886 CTGitPathList changelistItems;
3887 for (UINT i = 0; i < cFiles; ++i)
3889 if (DragQueryFile(hDrop, i, szFileName, _countof(szFileName)))
3890 changelistItems.AddPath(CTGitPath(szFileName));
3892 // find the changelist name
3893 CString sChangelist;
3894 LONG_PTR nGroup = m_pGitStatusListCtrl->GetGroupFromPoint(&clientpoint);
3895 for (std::map<CString, int>::iterator it = m_pGitStatusListCtrl->m_changelists.begin(); it != m_pGitStatusListCtrl->m_changelists.end(); ++it)
3896 if (it->second == nGroup)
3897 sChangelist = it->first;
3899 if (!sChangelist.IsEmpty())
3901 CGit git;
3902 if (git.AddToChangeList(changelistItems, sChangelist, git_depth_empty))
3904 for (int l=0; l<changelistItems.GetCount(); ++l)
3906 int index = m_pGitStatusListCtrl->GetIndex(changelistItems[l]);
3907 if (index >= 0)
3909 auto e = m_pGitStatusListCtrl->GetListEntry(index);
3910 if (e)
3912 e->changelist = sChangelist;
3913 if (!e->IsFolder())
3915 if (m_pGitStatusListCtrl->m_changelists.find(e->changelist) != m_pGitStatusListCtrl->m_changelists.end())
3916 m_pGitStatusListCtrl->SetItemGroup(index, m_pGitStatusListCtrl->m_changelists[e->changelist]);
3917 else
3918 m_pGitStatusListCtrl->SetItemGroup(index, 0);
3922 else
3924 HWND hParentWnd = GetParent(m_hTargetWnd);
3925 if (hParentWnd)
3926 ::SendMessage(hParentWnd, CGitStatusListCtrl::GITSLNM_ADDFILE, 0, (LPARAM)changelistItems[l].GetWinPath());
3930 else
3931 CMessageBox::Show(m_pGitStatusListCtrl->m_hWnd, git.GetLastErrorMessage(), L"TortoiseGit", MB_ICONERROR);
3933 else
3935 SVN git;
3936 if (git.RemoveFromChangeList(changelistItems, CStringArray(), git_depth_empty))
3938 for (int l=0; l<changelistItems.GetCount(); ++l)
3940 int index = m_pGitStatusListCtrl->GetIndex(changelistItems[l]);
3941 if (index >= 0)
3943 auto e = m_pGitStatusListCtrl->GetListEntry(index);
3944 if (e)
3946 e->changelist = sChangelist;
3947 m_pGitStatusListCtrl->SetItemGroup(index, 0);
3950 else
3952 HWND hParentWnd = GetParent(m_hTargetWnd);
3953 if (hParentWnd)
3954 ::SendMessage(hParentWnd, CGitStatusListCtrl::GITSLNM_ADDFILE, 0, (LPARAM)changelistItems[l].GetWinPath());
3958 else
3959 CMessageBox::Show(m_pGitStatusListCtrl->m_hWnd, git.GetLastErrorMessage(), L"TortoiseGit", MB_ICONERROR);
3961 #endif
3963 else
3965 for (UINT i = 0; i < cFiles; ++i)
3967 if (!DragQueryFile(hDrop, i, szFileName, _countof(szFileName)))
3968 continue;
3970 HWND hParentWnd = GetParent(m_hTargetWnd);
3971 if (hParentWnd)
3972 ::SendMessage(hParentWnd, CGitStatusListCtrl::GITSLNM_ADDFILE, 0, (LPARAM)szFileName);
3976 GlobalUnlock(medium.hGlobal);
3978 return true; //let base free the medium
3981 HRESULT STDMETHODCALLTYPE CGitStatusListCtrlDropTarget::DragOver(DWORD grfKeyState, POINTL pt, DWORD __RPC_FAR* pdwEffect)
3983 CIDropTarget::DragOver(grfKeyState, pt, pdwEffect);
3984 *pdwEffect = DROPEFFECT_COPY;
3985 if (m_pGitStatusListCtrl)
3987 POINT clientpoint;
3988 clientpoint.x = pt.x;
3989 clientpoint.y = pt.y;
3990 ScreenToClient(m_hTargetWnd, &clientpoint);
3991 if ((m_pGitStatusListCtrl->IsGroupViewEnabled()) && (m_pGitStatusListCtrl->GetGroupFromPoint(&clientpoint) >= 0))
3992 *pdwEffect = DROPEFFECT_NONE;
3993 else if ((!m_pGitStatusListCtrl->m_bFileDropsEnabled) || (m_pGitStatusListCtrl->m_bOwnDrag))
3994 *pdwEffect = DROPEFFECT_NONE;
3996 return S_OK;
3999 void CGitStatusListCtrl::FilesExport()
4001 CAutoReadLock locker(m_guard);
4002 CString exportDir;
4003 // export all changed files to a folder
4004 CBrowseFolder browseFolder;
4005 browseFolder.m_style = BIF_EDITBOX | BIF_NEWDIALOGSTYLE | BIF_RETURNFSANCESTORS | BIF_RETURNONLYFSDIRS;
4006 if (browseFolder.Show(GetSafeHwnd(), exportDir) != CBrowseFolder::OK)
4007 return;
4009 POSITION pos = GetFirstSelectedItemPosition();
4010 int index;
4011 while ((index = GetNextSelectedItem(pos)) >= 0)
4013 auto fd = GetListEntry(index);
4014 // we cannot export directories or folders
4015 if ((fd->m_Action & CTGitPath::LOGACTIONS_DELETED) || fd->IsDirectory())
4016 continue;
4018 CPathUtils::MakeSureDirectoryPathExists(exportDir + L'\\' + fd->GetContainingDirectory().GetWinPathString());
4019 CString filename = exportDir + L'\\' + fd->GetWinPathString();
4020 if (m_CurrentVersion == GIT_REV_ZERO)
4022 if (!CopyFile(g_Git.CombinePath(fd), filename, false))
4024 MessageBox(CFormatMessageWrapper(), L"TortoiseGit", MB_OK | MB_ICONERROR);
4025 return;
4028 else
4030 if (g_Git.GetOneFile(m_CurrentVersion, *fd, filename))
4032 CString out;
4033 out.Format(IDS_STATUSLIST_CHECKOUTFILEFAILED, (LPCTSTR)fd->GetGitPathString(), (LPCTSTR)m_CurrentVersion, (LPCTSTR)filename);
4034 if (CMessageBox::Show(GetSafeHwnd(), g_Git.GetGitLastErr(out, CGit::GIT_CMD_GETONEFILE), L"TortoiseGit", 2, IDI_WARNING, CString(MAKEINTRESOURCE(IDS_IGNOREBUTTON)), CString(MAKEINTRESOURCE(IDS_ABORTBUTTON))) == 2)
4035 return;
4041 void CGitStatusListCtrl::FileSaveAs(CTGitPath *path)
4043 CAutoReadLock locker(m_guard);
4044 CString filename;
4045 filename.Format(L"%s\\%s-%s%s", (LPCTSTR)g_Git.CombinePath(path->GetContainingDirectory()), (LPCTSTR)path->GetBaseFilename(), (LPCTSTR)m_CurrentVersion.Left(g_Git.GetShortHASHLength()), (LPCTSTR)path->GetFileExtension());
4046 if (!CAppUtils::FileOpenSave(filename, nullptr, 0, 0, false, GetSafeHwnd()))
4047 return;
4048 if (m_CurrentVersion == GIT_REV_ZERO)
4050 if (!CopyFile(g_Git.CombinePath(path), filename, false))
4052 MessageBox(CFormatMessageWrapper(), L"TortoiseGit", MB_OK | MB_ICONERROR);
4053 return;
4056 else
4058 if (g_Git.GetOneFile(m_CurrentVersion, *path, filename))
4060 CString out;
4061 out.Format(IDS_STATUSLIST_CHECKOUTFILEFAILED, (LPCTSTR)path->GetGitPathString(), (LPCTSTR)m_CurrentVersion, (LPCTSTR)filename);
4062 CMessageBox::Show(GetSafeHwnd(), g_Git.GetGitLastErr(out, CGit::GIT_CMD_GETONEFILE), L"TortoiseGit", MB_OK);
4063 return;
4068 int CGitStatusListCtrl::RevertSelectedItemToVersion(bool parent)
4070 CAutoReadLock locker(m_guard);
4071 if(this->m_CurrentVersion.IsEmpty())
4072 return 0;
4073 if(this->m_CurrentVersion == GIT_REV_ZERO)
4074 return 0;
4076 POSITION pos = GetFirstSelectedItemPosition();
4077 int index;
4078 CString cmd,out;
4079 std::map<CString, int> versionMap;
4080 while ((index = GetNextSelectedItem(pos)) >= 0)
4082 auto fentry = GetListEntry(index);
4083 CString version;
4084 if (parent)
4086 int parentNo = fentry->m_ParentNo & PARENT_MASK;
4087 CString ref;
4088 ref.Format(L"%s^%d", (LPCTSTR)m_CurrentVersion, parentNo + 1);
4089 CGitHash hash;
4090 if (g_Git.GetHash(hash, ref))
4092 MessageBox(g_Git.GetGitLastErr(L"Could not get hash of ref \"" + ref + L"\"."), L"TortoiseGit", MB_ICONERROR);
4093 continue;
4096 version = hash.ToString();
4098 else
4099 version = m_CurrentVersion;
4101 CString filename = fentry->GetGitPathString();
4102 if (!fentry->GetGitOldPathString().IsEmpty())
4103 filename = fentry->GetGitOldPathString();
4104 cmd.Format(L"git.exe checkout %s -- \"%s\"", (LPCTSTR)version, (LPCTSTR)filename);
4105 out.Empty();
4106 if (g_Git.Run(cmd, &out, CP_UTF8))
4108 if (MessageBox(out, L"TortoiseGit", MB_ICONEXCLAMATION | MB_OKCANCEL) == IDCANCEL)
4109 continue;
4111 else
4112 versionMap[version]++;
4115 out.Empty();
4116 for (auto it = versionMap.cbegin(); it != versionMap.cend(); ++it)
4118 CString versionEntry;
4119 versionEntry.Format(IDS_STATUSLIST_FILESREVERTED, it->second, (LPCTSTR)it->first);
4120 out += versionEntry + L"\r\n";
4122 if (!out.IsEmpty())
4124 if (GetLogicalParent() && GetLogicalParent()->GetSafeHwnd())
4125 GetLogicalParent()->SendMessage(GITSLNM_NEEDSREFRESH);
4126 CMessageBox::Show(GetSafeHwnd(), out, L"TortoiseGit", MB_OK);
4128 return 0;
4131 void CGitStatusListCtrl::OpenFile(CTGitPath*filepath,int mode)
4133 CString file;
4134 if(this->m_CurrentVersion.IsEmpty() || m_CurrentVersion == GIT_REV_ZERO)
4135 file = g_Git.CombinePath(filepath);
4136 else
4138 file = CTempFiles::Instance().GetTempFilePath(false, *filepath, m_CurrentVersion).GetWinPathString();
4139 CString cmd,out;
4140 if(g_Git.GetOneFile(m_CurrentVersion, *filepath, file))
4142 out.Format(IDS_STATUSLIST_CHECKOUTFILEFAILED, (LPCTSTR)filepath->GetGitPathString(), (LPCTSTR)m_CurrentVersion, (LPCTSTR)file);
4143 CMessageBox::Show(GetSafeHwnd(), g_Git.GetGitLastErr(out, CGit::GIT_CMD_GETONEFILE), L"TortoiseGit", MB_OK);
4144 return;
4146 SetFileAttributes(file, FILE_ATTRIBUTE_READONLY);
4148 if(mode == ALTERNATIVEEDITOR)
4150 CAppUtils::LaunchAlternativeEditor(file);
4151 return;
4154 if (mode == OPEN)
4155 CAppUtils::ShellOpen(file, GetSafeHwnd());
4156 else
4157 CAppUtils::ShowOpenWithDialog(file, GetSafeHwnd());
4160 void CGitStatusListCtrl::DeleteSelectedFiles()
4162 CAutoWriteLock locker(m_guard);
4163 //Collect paths
4164 std::vector<int> selectIndex;
4166 POSITION pos = GetFirstSelectedItemPosition();
4167 int index;
4168 while ((index = GetNextSelectedItem(pos)) >= 0)
4169 selectIndex.push_back(index);
4171 CAutoRepository repo = g_Git.GetGitRepository();
4172 if (!repo)
4174 MessageBox(g_Git.GetLibGit2LastErr(L"Could not open repository."), L"TortoiseGit", MB_OK);
4175 return;
4177 CAutoIndex gitIndex;
4178 if (git_repository_index(gitIndex.GetPointer(), repo))
4180 g_Git.GetLibGit2LastErr(L"Could not open index.");
4181 return;
4183 int needWriteIndex = 0;
4185 //Create file-list ('\0' separated) for SHFileOperation
4186 CString filelist;
4187 for (size_t i = 0; i < selectIndex.size(); ++i)
4189 index = selectIndex[i];
4191 auto path = GetListEntry(index);
4192 if (path == nullptr)
4193 continue;
4195 // do not report errors as we could remove an unversioned file
4196 needWriteIndex += git_index_remove_bypath(gitIndex, CUnicodeUtils::GetUTF8(path->GetGitPathString())) == 0 ? 1 : 0;
4198 if (!path->Exists())
4199 continue;
4201 filelist += path->GetWinPathString();
4202 filelist += L'|';
4204 filelist += L'|';
4205 int len = filelist.GetLength();
4206 auto buf = std::make_unique<TCHAR[]>(len + 2);
4207 wcscpy_s(buf.get(), len + 2, filelist);
4208 CStringUtils::PipesToNulls(buf.get(), len + 2);
4209 SHFILEOPSTRUCT fileop;
4210 fileop.hwnd = this->m_hWnd;
4211 fileop.wFunc = FO_DELETE;
4212 fileop.pFrom = buf.get();
4213 fileop.pTo = nullptr;
4214 fileop.fFlags = FOF_NO_CONNECTED_ELEMENTS | ((GetAsyncKeyState(VK_SHIFT) & 0x8000) ? 0 : FOF_ALLOWUNDO);
4215 fileop.lpszProgressTitle = L"deleting file";
4216 int result = SHFileOperation(&fileop);
4218 if ((result == 0 || len == 1) && (!fileop.fAnyOperationsAborted))
4220 if (needWriteIndex && git_index_write(gitIndex))
4221 MessageBox(g_Git.GetLibGit2LastErr(L"Could not write index."), L"TortoiseGit", MB_OK);
4223 if (needWriteIndex)
4225 CWnd* pParent = GetLogicalParent();
4226 if (pParent && pParent->GetSafeHwnd())
4227 pParent->SendMessage(GITSLNM_NEEDSREFRESH);
4228 SetRedraw(TRUE);
4229 return;
4232 SetRedraw(FALSE);
4233 POSITION pos2 = nullptr;
4234 while ((pos2 = GetFirstSelectedItemPosition()) != nullptr)
4236 int index2 = GetNextSelectedItem(pos2);
4237 if (GetCheck(index2))
4238 m_nSelected--;
4239 m_nTotal--;
4241 RemoveListEntry(index2);
4243 SetRedraw(TRUE);
4247 BOOL CGitStatusListCtrl::OnWndMsg(UINT message, WPARAM wParam, LPARAM lParam, LRESULT* pResult)
4249 switch (message)
4251 case WM_MENUCHAR: // only supported by IContextMenu3
4252 if (g_IContext3)
4254 g_IContext3->HandleMenuMsg2(message, wParam, lParam, pResult);
4255 return TRUE;
4257 break;
4259 case WM_DRAWITEM:
4260 case WM_MEASUREITEM:
4261 if (wParam)
4262 break; // if wParam != 0 then the message is not menu-related
4264 case WM_INITMENU:
4265 case WM_INITMENUPOPUP:
4267 HMENU hMenu = (HMENU)wParam;
4268 if ((hMenu == m_hShellMenu) && (GetMenuItemCount(hMenu) == 0))
4270 // the shell submenu is populated only on request, i.e. right
4271 // before the submenu is shown
4272 if (g_pFolderhook)
4274 delete g_pFolderhook;
4275 g_pFolderhook = nullptr;
4277 CTGitPathList targetList;
4278 FillListOfSelectedItemPaths(targetList);
4279 if (!targetList.IsEmpty())
4281 // get IShellFolder interface of Desktop (root of shell namespace)
4282 if (g_psfDesktopFolder)
4283 g_psfDesktopFolder->Release();
4284 SHGetDesktopFolder(&g_psfDesktopFolder); // needed to obtain full qualified pidl
4286 // ParseDisplayName creates a PIDL from a file system path relative to the IShellFolder interface
4287 // but since we use the Desktop as our interface and the Desktop is the namespace root
4288 // that means that it's a fully qualified PIDL, which is what we need
4290 if (g_pidlArray)
4292 for (int i = 0; i < g_pidlArrayItems; i++)
4294 if (g_pidlArray[i])
4295 CoTaskMemFree(g_pidlArray[i]);
4297 CoTaskMemFree(g_pidlArray);
4298 g_pidlArray = nullptr;
4299 g_pidlArrayItems = 0;
4301 int nItems = targetList.GetCount();
4302 g_pidlArray = (LPITEMIDLIST *)CoTaskMemAlloc((nItems + 10) * sizeof(LPITEMIDLIST));
4303 SecureZeroMemory(g_pidlArray, (nItems + 10) * sizeof(LPITEMIDLIST));
4304 int succeededItems = 0;
4305 PIDLIST_RELATIVE pidl = nullptr;
4307 int bufsize = 1024;
4308 auto filepath = std::make_unique<WCHAR[]>(bufsize);
4309 for (int i = 0; i < nItems; i++)
4311 CString fullPath = g_Git.CombinePath(targetList[i].GetWinPath());
4312 if (bufsize < fullPath.GetLength())
4314 bufsize = fullPath.GetLength() + 3;
4315 filepath = std::make_unique<WCHAR[]>(bufsize);
4317 wcscpy_s(filepath.get(), bufsize, fullPath);
4318 if (SUCCEEDED(g_psfDesktopFolder->ParseDisplayName(nullptr, 0, filepath.get(), nullptr, &pidl, nullptr)))
4319 g_pidlArray[succeededItems++] = pidl; // copy pidl to pidlArray
4321 if (succeededItems == 0)
4323 CoTaskMemFree(g_pidlArray);
4324 g_pidlArray = nullptr;
4327 g_pidlArrayItems = succeededItems;
4329 if (g_pidlArrayItems)
4331 CString ext = targetList[0].GetFileExtension();
4333 ASSOCIATIONELEMENT const rgAssocItem[] =
4335 { ASSOCCLASS_PROGID_STR, nullptr, ext },
4336 { ASSOCCLASS_SYSTEM_STR, nullptr, ext },
4337 { ASSOCCLASS_APP_STR, nullptr, ext },
4338 { ASSOCCLASS_STAR, nullptr, nullptr },
4339 { ASSOCCLASS_FOLDER, nullptr, nullptr },
4341 IQueryAssociations* pIQueryAssociations = nullptr;
4342 if (FAILED(AssocCreateForClasses(rgAssocItem, ARRAYSIZE(rgAssocItem), IID_IQueryAssociations, (void**)&pIQueryAssociations)))
4343 pIQueryAssociations = nullptr; // not a problem, it works without this
4345 g_pFolderhook = new CIShellFolderHook(g_psfDesktopFolder, targetList);
4346 LPCONTEXTMENU icm1 = nullptr;
4348 DEFCONTEXTMENU dcm = { 0 };
4349 dcm.hwnd = m_hWnd;
4350 dcm.psf = g_pFolderhook;
4351 dcm.cidl = g_pidlArrayItems;
4352 dcm.apidl = (PCUITEMID_CHILD_ARRAY)g_pidlArray;
4353 dcm.punkAssociationInfo = pIQueryAssociations;
4354 if (SUCCEEDED(SHCreateDefaultContextMenu(&dcm, IID_IContextMenu, (void**)&icm1)))
4356 int iMenuType = 0; // to know which version of IContextMenu is supported
4357 if (icm1)
4358 { // since we got an IContextMenu interface we can now obtain the higher version interfaces via that
4359 if (icm1->QueryInterface(IID_IContextMenu3, (void**)&m_pContextMenu) == S_OK)
4360 iMenuType = 3;
4361 else if (icm1->QueryInterface(IID_IContextMenu2, (void**)&m_pContextMenu) == S_OK)
4362 iMenuType = 2;
4364 if (m_pContextMenu)
4365 icm1->Release(); // we can now release version 1 interface, cause we got a higher one
4366 else
4368 // since no higher versions were found
4369 // redirect ppContextMenu to version 1 interface
4370 iMenuType = 1;
4371 m_pContextMenu = icm1;
4374 if (m_pContextMenu)
4376 // lets fill the our popup menu
4377 UINT flags = CMF_NORMAL;
4378 flags |= (GetKeyState(VK_SHIFT) & 0x8000) != 0 ? CMF_EXTENDEDVERBS : 0;
4379 m_pContextMenu->QueryContextMenu(hMenu, 0, SHELL_MIN_CMD, SHELL_MAX_CMD, flags);
4382 // subclass window to handle menu related messages in CShellContextMenu
4383 if (iMenuType > 1) // only subclass if its version 2 or 3
4385 if (iMenuType == 2)
4386 g_IContext2 = (LPCONTEXTMENU2)m_pContextMenu;
4387 else // version 3
4388 g_IContext3 = (LPCONTEXTMENU3)m_pContextMenu;
4392 if (pIQueryAssociations)
4393 pIQueryAssociations->Release();
4396 if (g_IContext3)
4397 g_IContext3->HandleMenuMsg2(message, wParam, lParam, pResult);
4398 else if (g_IContext2)
4399 g_IContext2->HandleMenuMsg(message, wParam, lParam);
4400 return TRUE;
4404 break;
4405 default:
4406 break;
4409 return __super::OnWndMsg(message, wParam, lParam, pResult);
4412 CTGitPath* CGitStatusListCtrl::GetListEntry(int index)
4414 ATLASSERT(m_guard.GetCurrentThreadStatus());
4415 auto entry = reinterpret_cast<CTGitPath*>(GetItemData(index));
4416 ASSERT(entry);
4417 return entry;
4420 void CGitStatusListCtrl::OnSysColorChange()
4422 __super::OnSysColorChange();
4423 if (m_nBackgroundImageID)
4424 CAppUtils::SetListCtrlBackgroundImage(GetSafeHwnd(), m_nBackgroundImageID);
4427 ULONG CGitStatusListCtrl::GetGestureStatus(CPoint /*ptTouch*/)
4429 return 0;