Keep the font size of 8 for the explorer property page
[TortoiseGit.git] / src / Git / GitStatusListCtrl.cpp
blob272710437eab9e80361a731450f45742830ca665
1 // TortoiseGit - a Windows shell extension for easy version control
3 // Copyright (C) 2008-2018 - 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_EX(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)
275 , m_uiFont(nullptr)
277 m_critSec.Init();
278 m_bNoAutoselectMissing = CRegDWORD(L"Software\\TortoiseGit\\AutoselectMissingFiles", FALSE) == TRUE;
280 NONCLIENTMETRICS metrics = { 0 };
281 metrics.cbSize = sizeof(NONCLIENTMETRICS);
282 SystemParametersInfo(SPI_GETNONCLIENTMETRICS, 0, &metrics, FALSE);
283 m_uiFont = CreateFontIndirect(&metrics.lfMessageFont);
286 CGitStatusListCtrl::~CGitStatusListCtrl()
288 if (m_uiFont)
289 DeleteObject(m_uiFont);
290 ClearStatusArray();
293 HWND CGitStatusListCtrl::GetParentHWND()
295 if (m_hwndLogicalParent)
296 return m_hwndLogicalParent->GetSafeHwnd();
297 auto owner = GetSafeOwner();
298 if (!owner)
299 return GetSafeHwnd();
300 return owner->GetSafeHwnd();
303 void CGitStatusListCtrl::ClearStatusArray()
305 #if 0
306 CAutoWriteLock locker(m_guard);
307 for (size_t i = 0; i < m_arStatusArray.size(); ++i)
309 delete m_arStatusArray[i];
311 m_arStatusArray.clear();
312 #endif
315 void CGitStatusListCtrl::Init(DWORD dwColumns, const CString& sColumnInfoContainer, unsigned __int64 dwContextMenus /* = GitSLC_POPALL */, bool bHasCheckboxes /* = true */, bool bHasWC /* = true */, DWORD allowedColumns /* = 0xffffffff */)
317 CAutoWriteLock locker(m_guard);
319 m_dwDefaultColumns = dwColumns | 1;
320 m_dwContextMenus = dwContextMenus;
321 m_bHasCheckboxes = bHasCheckboxes;
322 m_bHasWC = bHasWC;
323 m_bWaitCursor = true;
325 // set the extended style of the listcontrol
326 DWORD exStyle = LVS_EX_DOUBLEBUFFER | LVS_EX_INFOTIP | LVS_EX_SUBITEMIMAGES;
327 exStyle |= (bHasCheckboxes ? LVS_EX_CHECKBOXES : 0);
328 SetRedraw(false);
329 SetExtendedStyle(exStyle);
330 CResizableColumnsListCtrl::Init();
332 SetWindowTheme(m_hWnd, L"Explorer", nullptr);
334 m_nIconFolder = SYS_IMAGE_LIST().GetDirIconIndex();
335 m_nRestoreOvl = SYS_IMAGE_LIST().AddIcon(CCommonAppUtils::LoadIconEx(IDI_RESTOREOVL, 0, 0));
336 SYS_IMAGE_LIST().SetOverlayImage(m_nRestoreOvl, OVL_RESTORE);
337 SetImageList(&SYS_IMAGE_LIST(), LVSIL_SMALL);
339 // keep CSorter::operator() in sync!!
340 static UINT standardColumnNames[GITSLC_NUMCOLUMNS]
341 = { IDS_STATUSLIST_COLFILE
342 , IDS_STATUSLIST_COLFILENAME
343 , IDS_STATUSLIST_COLEXT
344 , IDS_STATUSLIST_COLSTATUS
345 , IDS_STATUSLIST_COLADD
346 , IDS_STATUSLIST_COLDEL
347 , IDS_STATUSLIST_COLLASTMODIFIED
348 , IDS_STATUSLIST_COLSIZE
351 m_ColumnManager.SetNames(standardColumnNames,GITSLC_NUMCOLUMNS);
352 m_ColumnManager.ReadSettings(m_dwDefaultColumns, 0xffffffff & ~(allowedColumns | m_dwDefaultColumns), sColumnInfoContainer, GITSLC_NUMCOLUMNS);
353 m_ColumnManager.SetRightAlign(4);
354 m_ColumnManager.SetRightAlign(5);
355 m_ColumnManager.SetRightAlign(7);
357 // enable file drops
358 if (!m_pDropTarget)
360 m_pDropTarget = std::make_unique<CGitStatusListCtrlDropTarget>(this);
361 RegisterDragDrop(m_hWnd, m_pDropTarget.get());
362 // create the supported formats:
363 FORMATETC ftetc = { 0 };
364 ftetc.dwAspect = DVASPECT_CONTENT;
365 ftetc.lindex = -1;
366 ftetc.tymed = TYMED_HGLOBAL;
367 ftetc.cfFormat = CF_HDROP;
368 m_pDropTarget->AddSuportedFormat(ftetc);
371 SetRedraw(true);
372 m_bWaitCursor = false;
375 bool CGitStatusListCtrl::SetBackgroundImage(UINT nID)
377 m_nBackgroundImageID = nID;
378 return CAppUtils::SetListCtrlBackgroundImage(GetSafeHwnd(), nID);
381 BOOL CGitStatusListCtrl::GetStatus ( const CTGitPathList* pathList
382 , bool bUpdate /* = FALSE */
383 , bool bShowIgnores /* = false */
384 , bool bShowUnRev /* = false */
385 , bool bShowLocalChangesIgnored /* = false */)
387 CAutoWriteLock locker(m_guard);
389 m_bEmpty = false;
390 m_bBusy = true;
391 m_bWaitCursor = true;
392 Invalidate();
394 m_bIsRevertTheirMy = g_Git.IsRebaseRunning() > 0;
396 int mask= CGitStatusListCtrl::FILELIST_MODIFY;
397 if(bShowIgnores)
398 mask|= CGitStatusListCtrl::FILELIST_IGNORE;
399 if(bShowUnRev)
400 mask|= CGitStatusListCtrl::FILELIST_UNVER;
401 if (bShowLocalChangesIgnored)
402 mask |= CGitStatusListCtrl::FILELIST_LOCALCHANGESIGNORED;
403 this->UpdateFileList(mask, bUpdate, pathList);
405 if (pathList && m_setDirectFiles.empty())
407 // remember files which are selected by users so that those can be preselected
408 for (int i = 0; i < pathList->GetCount(); ++i)
409 if (!(*pathList)[i].IsDirectory())
410 m_setDirectFiles.insert((*pathList)[i].GetGitPathString());
413 #if 0
414 int refetchcounter = 0;
415 BOOL bRet = TRUE;
416 Invalidate();
417 // force the cursor to change
418 POINT pt;
419 GetCursorPos(&pt);
420 SetCursorPos(pt.x, pt.y);
422 m_mapFilenameToChecked.clear();
423 //m_StatusUrlList.Clear();
424 bool bHasChangelists = (m_changelists.size() > 1 || (!m_changelists.empty() && !m_bHasIgnoreGroup));
425 m_changelists.clear();
426 for (size_t i=0; i < m_arStatusArray.size(); i++)
428 FileEntry * entry = m_arStatusArray[i];
429 if ( bHasChangelists && entry->checked)
431 // If change lists are present, remember all checked entries
432 CString path = entry->GetPath().GetGitPathString();
433 m_mapFilenameToChecked[path] = true;
435 if ( (entry->status==git_wc_status_unversioned || entry->status==git_wc_status_missing ) && entry->checked )
437 // The user manually selected an unversioned or missing file. We remember
438 // this so that the selection can be restored when refreshing.
439 CString path = entry->GetPath().GetGitPathString();
440 m_mapFilenameToChecked[path] = true;
442 else if ( entry->status > git_wc_status_normal && !entry->checked )
444 // The user manually deselected a versioned file. We remember
445 // this so that the deselection can be restored when refreshing.
446 CString path = entry->GetPath().GetGitPathString();
447 m_mapFilenameToChecked[path] = false;
451 // use a sorted path list to make sure we fetch the status of
452 // parent items first, *then* the child items (if any)
453 CTGitPathList sortedPathList = pathList;
454 sortedPathList.SortByPathname();
457 bRet = TRUE;
458 m_nTargetCount = 0;
459 m_bHasExternalsFromDifferentRepos = FALSE;
460 m_bHasExternals = FALSE;
461 m_bHasUnversionedItems = FALSE;
462 m_bHasChangeLists = false;
463 m_bShowIgnores = bShowIgnores;
464 m_nSortedColumn = 0;
465 m_bBlock = TRUE;
466 m_bBusy = true;
467 m_bEmpty = false;
468 Invalidate();
470 // first clear possible status data left from
471 // previous GetStatus() calls
472 ClearStatusArray();
474 m_StatusFileList = sortedPathList;
476 // Since Git_client_status() returns all files, even those in
477 // folders included with Git:externals we need to check if all
478 // files we get here belongs to the same repository.
479 // It is possible to commit changes in an external folder, as long
480 // as the folder belongs to the same repository (but another path),
481 // but it is not possible to commit all files if the externals are
482 // from a different repository.
484 // To check if all files belong to the same repository, we compare the
485 // UUID's - if they're identical then the files belong to the same
486 // repository and can be committed. But if they're different, then
487 // tell the user to commit all changes in the external folders
488 // first and exit.
489 CStringA sUUID; // holds the repo UUID
490 CTGitPathList arExtPaths; // list of Git:external paths
492 GitConfig config;
494 m_sURL.Empty();
496 m_nTargetCount = sortedPathList.GetCount();
498 GitStatus status(m_pbCanceled);
499 if(m_nTargetCount > 1 && sortedPathList.AreAllPathsFilesInOneDirectory())
501 // This is a special case, where we've been given a list of files
502 // all from one folder
503 // We handle them by setting a status filter, then requesting the Git status of
504 // all the files in the directory, filtering for the ones we're interested in
505 status.SetFilter(sortedPathList);
507 // if all selected entries are files, we don't do a recursive status
508 // fetching. But if only one is a directory, we have to recurse.
509 git_depth_t depth = git_depth_files;
510 // We have set a filter. If all selected items were files or we fetch
511 // the status not recursively, then we have to include
512 // ignored items too - the user has selected them
513 bool bShowIgnoresRight = true;
514 for (int fcindex=0; fcindex<sortedPathList.GetCount(); ++fcindex)
516 if (sortedPathList[fcindex].IsDirectory())
518 depth = git_depth_infinity;
519 bShowIgnoresRight = false;
520 break;
523 if(!FetchStatusForSingleTarget(config, status, sortedPathList.GetCommonDirectory(), bUpdate, sUUID, arExtPaths, true, depth, bShowIgnoresRight))
524 bRet = FALSE;
526 else
528 for(int nTarget = 0; nTarget < m_nTargetCount; nTarget++)
530 // check whether the path we want the status for is already fetched due to status-fetching
531 // of a parent path.
532 // this check is only done for file paths, because folder paths could be included already
533 // but not recursively
534 if (sortedPathList[nTarget].IsDirectory() || !GetListEntry(sortedPathList[nTarget]))
536 if(!FetchStatusForSingleTarget(config, status, sortedPathList[nTarget], bUpdate, sUUID,
537 arExtPaths, false, git_depth_infinity, bShowIgnores))
539 bRet = FALSE;
545 // remove the 'helper' files of conflicted items from the list.
546 // otherwise they would appear as unversioned files.
547 for (INT_PTR cind = 0; cind < m_ConflictFileList.GetCount(); ++cind)
549 for (size_t i=0; i < m_arStatusArray.size(); i++)
551 if (m_arStatusArray[i]->GetPath().IsEquivalentTo(m_ConflictFileList[cind]))
553 delete m_arStatusArray[i];
554 m_arStatusArray.erase(m_arStatusArray.cbegin() + i);
555 break;
559 refetchcounter++;
560 } while(!BuildStatistics() && (refetchcounter < 2) && (*m_pbCanceled==false));
562 m_bBlock = FALSE;
563 m_bBusy = false;
564 GetCursorPos(&pt);
565 SetCursorPos(pt.x, pt.y);
566 return bRet;
567 #endif
568 m_bBusy = false;
569 m_bWaitCursor = false;
570 BuildStatistics();
571 return TRUE;
574 // Get the show-flags bitmap value which corresponds to a particular Git status
575 DWORD CGitStatusListCtrl::GetShowFlagsFromGitStatus(git_wc_status_kind status)
577 switch (status)
579 case git_wc_status_none:
580 case git_wc_status_unversioned:
581 return GITSLC_SHOWUNVERSIONED;
582 case git_wc_status_ignored:
583 if (!m_bShowIgnores)
584 return GITSLC_SHOWDIRECTS;
585 return GITSLC_SHOWDIRECTS|GITSLC_SHOWIGNORED;
586 case git_wc_status_normal:
587 return GITSLC_SHOWNORMAL;
588 case git_wc_status_added:
589 return GITSLC_SHOWADDED;
590 case git_wc_status_deleted:
591 return GITSLC_SHOWREMOVED;
592 case git_wc_status_modified:
593 return GITSLC_SHOWMODIFIED;
594 case git_wc_status_conflicted:
595 return GITSLC_SHOWCONFLICTED;
596 default:
597 // we should NEVER get here!
598 ASSERT(FALSE);
599 break;
601 return 0;
604 void CGitStatusListCtrl::Show(unsigned int dwShow, unsigned int dwCheck /*=0*/, bool /*bShowFolders*/ /* = true */,BOOL UpdateStatusList,bool UseStoredCheckStatus)
606 m_bWaitCursor = true;
607 m_bBusy = true;
608 m_bEmpty = false;
609 Invalidate();
611 WORD langID = (WORD)CRegStdDWORD(L"Software\\TortoiseGit\\LanguageID", GetUserDefaultLangID());
612 SetRedraw(FALSE);
614 CAutoWriteLock locker(m_guard);
615 DeleteAllItems();
616 m_nSelected = 0;
618 m_nShownUnversioned = 0;
619 m_nShownModified = 0;
620 m_nShownAdded = 0;
621 m_nShownDeleted = 0;
622 m_nShownConflicted = 0;
623 m_nShownFiles = 0;
624 m_nShownSubmodules = 0;
626 m_dwShow = dwShow;
628 if (UpdateStatusList)
630 m_arStatusArray.clear();
631 for (int i = 0; i < m_StatusFileList.GetCount(); ++i)
632 m_arStatusArray.push_back(&m_StatusFileList[i]);
634 for (int i = 0; i < m_UnRevFileList.GetCount(); ++i)
635 m_arStatusArray.push_back(&m_UnRevFileList[i]);
637 for (int i = 0; i < m_IgnoreFileList.GetCount(); ++i)
638 m_arStatusArray.push_back(&m_IgnoreFileList[i]);
640 PrepareGroups();
641 m_arListArray.clear();
642 m_arListArray.reserve(m_arStatusArray.size());
643 if (m_nSortedColumn >= 0)
645 CSorter predicate(&m_ColumnManager, m_nSortedColumn, m_bAscending);
646 std::stable_sort(m_arStatusArray.begin(), m_arStatusArray.end(), predicate);
649 int index = 0;
650 for (size_t i = 0; i < m_arStatusArray.size(); ++i)
652 //set default checkbox status
653 auto entry = const_cast<CTGitPath*>(m_arStatusArray[i]);
654 CString path = entry->GetGitPathString();
655 if (!m_mapFilenameToChecked.empty() && m_mapFilenameToChecked.find(path) != m_mapFilenameToChecked.end())
656 entry->m_Checked = m_mapFilenameToChecked[path];
657 else if (!UseStoredCheckStatus)
659 bool autoSelectSubmodules = !(entry->IsDirectory() && m_bDoNotAutoselectSubmodules);
660 if (((entry->m_Action & dwCheck) && !(m_bNoAutoselectMissing && entry->m_Action & CTGitPath::LOGACTIONS_MISSING) || dwShow & GITSLC_SHOWDIRECTFILES && m_setDirectFiles.find(path) != m_setDirectFiles.end()) && autoSelectSubmodules)
661 entry->m_Checked = true;
662 else
663 entry->m_Checked = false;
664 m_mapFilenameToChecked[path] = entry->m_Checked;
667 if (entry->m_Action & dwShow)
669 AddEntry(i, entry, langID, index);
670 index++;
675 AdjustColumnWidths();
677 SetRedraw(TRUE);
678 GetStatisticsString();
680 CHeaderCtrl * pHeader = GetHeaderCtrl();
681 HDITEM HeaderItem = {0};
682 HeaderItem.mask = HDI_FORMAT;
683 for (int i=0; i<pHeader->GetItemCount(); ++i)
685 pHeader->GetItem(i, &HeaderItem);
686 HeaderItem.fmt &= ~(HDF_SORTDOWN | HDF_SORTUP);
687 pHeader->SetItem(i, &HeaderItem);
689 if (m_nSortedColumn >= 0)
691 pHeader->GetItem(m_nSortedColumn, &HeaderItem);
692 HeaderItem.fmt |= (m_bAscending ? HDF_SORTUP : HDF_SORTDOWN);
693 pHeader->SetItem(m_nSortedColumn, &HeaderItem);
696 RestoreScrollPos();
698 m_bWaitCursor = false;
699 m_bBusy = false;
700 m_bEmpty = (GetItemCount() == 0);
701 Invalidate();
703 this->BuildStatistics();
705 #if 0
707 m_bShowFolders = bShowFolders;
709 int nTopIndex = GetTopIndex();
710 POSITION posSelectedEntry = GetFirstSelectedItemPosition();
711 int nSelectedEntry = 0;
712 if (posSelectedEntry)
713 nSelectedEntry = GetNextSelectedItem(posSelectedEntry);
714 SetRedraw(FALSE);
715 DeleteAllItems();
717 PrepareGroups();
719 m_arListArray.clear();
721 m_arListArray.reserve(m_arStatusArray.size());
722 SetItemCount (static_cast<int>(m_arStatusArray.size()));
724 int listIndex = 0;
725 for (size_t i=0; i < m_arStatusArray.size(); ++i)
727 FileEntry * entry = m_arStatusArray[i];
728 if ((entry->inexternal) && (!(dwShow & SVNSLC_SHOWINEXTERNALS)))
729 continue;
730 if ((entry->differentrepo || entry->isNested) && (! (dwShow & SVNSLC_SHOWEXTERNALFROMDIFFERENTREPO)))
731 continue;
732 if (entry->IsFolder() && (!bShowFolders))
733 continue; // don't show folders if they're not wanted.
735 #if 0
736 git_wc_status_kind status = GitStatus::GetMoreImportant(entry->status, entry->remotestatus);
737 DWORD showFlags = GetShowFlagsFromGitStatus(status);
738 if (entry->switched)
739 showFlags |= SVNSLC_SHOWSWITCHED;
740 if (!entry->changelist.IsEmpty())
741 showFlags |= SVNSLC_SHOWINCHANGELIST;
742 #endif
743 bool bAllowCheck = ((entry->changelist.Compare(GITSLC_IGNORECHANGELIST) != 0)
744 && (m_bCheckIfGroupsExist || (m_changelists.empty() || (m_changelists.size() == 1 && m_bHasIgnoreGroup))));
746 // status_ignored is a special case - we must have the 'direct' flag set to add a status_ignored item
747 #if 0
748 if (status != Git_wc_status_ignored || (entry->direct) || (dwShow & GitSLC_SHOWIGNORED))
750 if ((!entry->IsFolder()) && (status == Git_wc_status_deleted) && (dwShow & SVNSLC_SHOWREMOVEDANDPRESENT))
752 if (PathFileExists(entry->GetPath().GetWinPath()))
754 m_arListArray.push_back(i);
755 if ((dwCheck & SVNSLC_SHOWREMOVEDANDPRESENT)||((dwCheck & SVNSLC_SHOWDIRECTS)&&(entry->direct)))
757 if (bAllowCheck)
758 entry->checked = true;
760 AddEntry(entry, langID, listIndex++);
763 else if ((dwShow & showFlags)||((dwShow & SVNSLC_SHOWDIRECTFILES)&&(entry->direct)&&(!entry->IsFolder())))
765 m_arListArray.push_back(i);
766 if ((dwCheck & showFlags)||((dwCheck & SVNSLC_SHOWDIRECTS)&&(entry->direct)))
768 if (bAllowCheck)
769 entry->checked = true;
771 AddEntry(entry, langID, listIndex++);
773 else if ((dwShow & showFlags)||((dwShow & SVNSLC_SHOWDIRECTFOLDER)&&(entry->direct)&&entry->IsFolder()))
775 m_arListArray.push_back(i);
776 if ((dwCheck & showFlags)||((dwCheck & SVNSLC_SHOWDIRECTS)&&(entry->direct)))
778 if (bAllowCheck)
779 entry->checked = true;
781 AddEntry(entry, langID, listIndex++);
784 #endif
787 SetItemCount(listIndex);
789 m_ColumnManager.UpdateRelevance (m_arStatusArray, m_arListArray);
791 AdjustColumnWidths();
793 SetRedraw(TRUE);
794 GetStatisticsString();
796 CHeaderCtrl * pHeader = GetHeaderCtrl();
797 HDITEM HeaderItem = {0};
798 HeaderItem.mask = HDI_FORMAT;
799 for (int i=0; i<pHeader->GetItemCount(); ++i)
801 pHeader->GetItem(i, &HeaderItem);
802 HeaderItem.fmt &= ~(HDF_SORTDOWN | HDF_SORTUP);
803 pHeader->SetItem(i, &HeaderItem);
805 if (m_nSortedColumn)
807 pHeader->GetItem(m_nSortedColumn, &HeaderItem);
808 HeaderItem.fmt |= (m_bAscending ? HDF_SORTUP : HDF_SORTDOWN);
809 pHeader->SetItem(m_nSortedColumn, &HeaderItem);
812 if (nSelectedEntry)
814 SetItemState(nSelectedEntry, LVIS_SELECTED, LVIS_SELECTED);
815 EnsureVisible(nSelectedEntry, false);
817 else
819 // Restore the item at the top of the list.
820 for (int i=0;GetTopIndex() != nTopIndex;i++)
822 if ( !EnsureVisible(nTopIndex+i,false) )
823 break;
827 m_bEmpty = (GetItemCount() == 0);
828 Invalidate();
829 #endif
832 void CGitStatusListCtrl::StoreScrollPos()
834 m_sScrollPos.enabled = true;
835 m_sScrollPos.nTopIndex = GetTopIndex();
836 m_sScrollPos.selMark = GetSelectionMark();
837 POSITION posSelectedEntry = GetFirstSelectedItemPosition();
838 m_sScrollPos.nSelectedEntry = 0;
839 if (posSelectedEntry)
840 m_sScrollPos.nSelectedEntry = GetNextSelectedItem(posSelectedEntry);
843 void CGitStatusListCtrl::RestoreScrollPos()
845 if (!m_sScrollPos.enabled || CRegDWORD(L"Software\\TortoiseGit\\RememberFileListPosition", TRUE) != TRUE)
846 return;
848 if (m_sScrollPos.nSelectedEntry)
850 SetItemState(m_sScrollPos.nSelectedEntry, LVIS_SELECTED, LVIS_SELECTED);
851 EnsureVisible(m_sScrollPos.nSelectedEntry, false);
853 else
855 // Restore the item at the top of the list.
856 for (int i = 0; GetTopIndex() != m_sScrollPos.nTopIndex; ++i)
858 if (!EnsureVisible(m_sScrollPos.nTopIndex + i, false))
859 break;
862 if (m_sScrollPos.selMark >= 0)
864 SetSelectionMark(m_sScrollPos.selMark);
865 SetItemState(m_sScrollPos.selMark, LVIS_FOCUSED, LVIS_FOCUSED);
867 m_sScrollPos.enabled = false;
870 int CGitStatusListCtrl::GetColumnIndex(int mask)
872 for (int i = 0; i < 32; ++i)
873 if(mask&0x1)
874 return i;
875 else
876 mask=mask>>1;
877 return -1;
880 CString CGitStatusListCtrl::GetCellText(int listIndex, int column)
882 static CString from(MAKEINTRESOURCE(IDS_STATUSLIST_FROM));
883 static bool abbreviateRenamings(((DWORD)CRegDWORD(L"Software\\TortoiseGit\\AbbreviateRenamings", FALSE)) == TRUE);
884 static bool relativeTimes = (CRegDWORD(L"Software\\TortoiseGit\\RelativeTimes", FALSE) != FALSE);
885 static const CString empty;
887 CAutoReadLock locker(m_guard);
888 const auto* entry = GetListEntry(listIndex);
889 if (!entry)
890 return empty;
892 switch (column)
894 case 0: // relative path
895 if (!(entry->m_Action & (CTGitPath::LOGACTIONS_REPLACED | CTGitPath::LOGACTIONS_COPY) && !entry->GetGitOldPathString().IsEmpty()))
896 return entry->GetGitPathString();
898 if (!abbreviateRenamings)
900 CString entryname = entry->GetGitPathString();
901 entryname += L' ';
902 // relative path
903 entryname.AppendFormat(from, (LPCTSTR)entry->GetGitOldPathString());
904 return entryname;
907 return entry->GetAbbreviatedRename();
909 case 1: // GITSLC_COLFILENAME
910 return entry->GetFileOrDirectoryName();
912 case 2: // GITSLC_COLEXT
913 return entry->GetFileExtension();
915 case 3: // GITSLC_COLSTATUS
916 return entry->GetActionName();
918 case 4: // GITSLC_COLADD
919 return entry->m_StatAdd;
921 case 5: // GITSLC_COLDEL
922 return entry->m_StatDel;
924 case 6: // GITSLC_COLMODIFICATIONDATE
925 if (!(entry->m_Action & CTGitPath::LOGACTIONS_DELETED) && m_ColumnManager.IsRelevant(GetColumnIndex(GITSLC_COLMODIFICATIONDATE)))
927 CString modificationDate;
928 __int64 filetime = entry->GetLastWriteTime();
929 if (filetime)
931 FILETIME* f = (FILETIME*)(__int64*)&filetime;
932 modificationDate = CLoglistUtils::FormatDateAndTime(CTime(CGit::filetime_to_time_t(f)), DATE_SHORTDATE, true, relativeTimes);
934 return modificationDate;
936 return empty;
938 case 7: // GITSLC_COLSIZE
939 if (!(entry->IsDirectory() || !m_ColumnManager.IsRelevant(GetColumnIndex(GITSLC_COLSIZE))))
941 TCHAR buf[100] = { 0 };
942 StrFormatByteSize64(entry->GetFileSize(), buf, 100);
943 return buf;
945 return empty;
947 #if 0
948 default: // user-defined properties
949 if (column < m_ColumnManager.GetColumnCount())
951 assert(m_ColumnManager.IsUserProp(column));
953 const CString& name = m_ColumnManager.GetName(column);
954 auto propEntry = m_PropertyMap.find(entry->GetPath());
955 if (propEntry != m_PropertyMap.end())
957 if (propEntry->second.HasProperty(name))
959 const CString& propVal = propEntry->second[name];
960 return propVal.IsEmpty()
961 ? m_sNoPropValueText
962 : propVal;
966 #endif
968 return empty;
971 void CGitStatusListCtrl::AddEntry(size_t arStatusArrayIndex, CTGitPath * GitPath, WORD /*langID*/, int listIndex)
973 CAutoWriteLock locker(m_guard);
974 ScopedInDecrement blocker(m_nBlockItemChangeHandler);
975 CString path = GitPath->GetGitPathString();
977 // Load the icons *now* so the icons are cached when showing them later in the
978 // WM_PAINT handler.
979 // Problem is that (at least on Win10), loading the icons in the WM_PAINT
980 // handler triggers an OLE operation, which should not happen in WM_PAINT at all
981 // (see ..\VC\atlmfc\src\mfc\olemsgf.cpp, COleMessageFilter::OnMessagePending() for details about this)
982 // By loading the icons here, they get cached and the OLE operation won't happen
983 // later in the WM_PAINT handler.
984 // This solves the 'hang' which happens in the commit dialog if images are
985 // shown in the file list.
986 int icon_idx = 0;
987 if (GitPath->IsDirectory())
989 icon_idx = m_nIconFolder;
990 m_nShownSubmodules++;
992 else
994 icon_idx = SYS_IMAGE_LIST().GetPathIconIndex(*GitPath);
995 m_nShownFiles++;
997 switch (GitPath->m_Action)
999 case CTGitPath::LOGACTIONS_ADDED:
1000 case CTGitPath::LOGACTIONS_COPY:
1001 m_nShownAdded++;
1002 break;
1003 case CTGitPath::LOGACTIONS_DELETED:
1004 case CTGitPath::LOGACTIONS_MISSING:
1005 case CTGitPath::LOGACTIONS_DELETED | CTGitPath::LOGACTIONS_MISSING:
1006 m_nShownDeleted++;
1007 break;
1008 case CTGitPath::LOGACTIONS_REPLACED:
1009 case CTGitPath::LOGACTIONS_MODIFIED:
1010 case CTGitPath::LOGACTIONS_MERGED:
1011 m_nShownModified++;
1012 break;
1013 case CTGitPath::LOGACTIONS_UNMERGED:
1014 case CTGitPath::LOGACTIONS_UNMERGED | CTGitPath::LOGACTIONS_ADDED:
1015 case CTGitPath::LOGACTIONS_UNMERGED | CTGitPath::LOGACTIONS_MODIFIED:
1016 m_nShownConflicted++;
1017 break;
1018 case CTGitPath::LOGACTIONS_UNVER:
1019 m_nShownUnversioned++;
1020 break;
1021 default:
1022 m_nShownUnversioned++;
1023 break;
1026 LVITEM lvItem = { 0 };
1027 lvItem.iItem = listIndex;
1028 lvItem.lParam = (LPARAM)GitPath;
1029 lvItem.mask = LVIF_TEXT | LVIF_IMAGE | LVIF_STATE | LVIF_PARAM;
1030 lvItem.pszText = LPSTR_TEXTCALLBACK;
1031 lvItem.stateMask = LVIS_OVERLAYMASK;
1032 if (m_restorepaths.find(GitPath->GetWinPathString()) != m_restorepaths.end())
1033 lvItem.state = INDEXTOOVERLAYMASK(OVL_RESTORE);
1034 lvItem.iImage = icon_idx;
1035 InsertItem(&lvItem);
1037 m_arListArray.push_back(arStatusArrayIndex);
1039 SetCheck(listIndex, GitPath->m_Checked);
1040 if (GitPath->m_Checked)
1041 m_nSelected++;
1043 if ((GitPath->m_Action & CTGitPath::LOGACTIONS_SKIPWORKTREE) || (GitPath->m_Action & CTGitPath::LOGACTIONS_ASSUMEVALID))
1044 SetItemGroup(listIndex, 3);
1045 else if (GitPath->m_Action & CTGitPath::LOGACTIONS_IGNORE)
1046 SetItemGroup(listIndex, 2);
1047 else if( GitPath->m_Action & CTGitPath::LOGACTIONS_UNVER)
1048 SetItemGroup(listIndex,1);
1049 else
1050 SetItemGroup(listIndex, GitPath->m_ParentNo&(PARENT_MASK|MERGE_MASK));
1053 bool CGitStatusListCtrl::SetItemGroup(int item, int groupindex)
1055 CAutoWriteLock locker(m_guard);
1056 // if (!(m_dwContextMenus & SVNSLC_POPCHANGELISTS))
1057 // return false;
1058 if (groupindex < 0)
1059 return false;
1060 LVITEM i = {0};
1061 i.mask = LVIF_GROUPID;
1062 i.iItem = item;
1063 i.iSubItem = 0;
1064 i.iGroupId = groupindex;
1066 return !!SetItem(&i);
1069 void CGitStatusListCtrl::OnHdnItemclick(NMHDR *pNMHDR, LRESULT *pResult)
1071 LPNMHEADER phdr = reinterpret_cast<LPNMHEADER>(pNMHDR);
1072 *pResult = 0;
1074 CAutoReadWeakLock lock(m_guard);
1075 if (!lock.IsAcquired())
1076 return;
1078 if (m_arStatusArray.empty())
1079 return;
1081 if (m_nSortedColumn == phdr->iItem)
1082 m_bAscending = !m_bAscending;
1083 else
1084 m_bAscending = TRUE;
1085 m_nSortedColumn = phdr->iItem;
1086 Show(m_dwShow, 0, m_bShowFolders,false,true);
1089 BOOL CGitStatusListCtrl::OnLvnItemchanged(NMHDR *pNMHDR, LRESULT *pResult)
1091 LPNMLISTVIEW pNMLV = reinterpret_cast<LPNMLISTVIEW>(pNMHDR);
1092 *pResult = 0;
1093 CWnd* pParent = GetLogicalParent();
1094 if (pParent && pParent->GetSafeHwnd())
1095 pParent->SendMessage(GITSLNM_ITEMCHANGED, pNMLV->iItem);
1097 if (m_nBlockItemChangeHandler)
1098 return FALSE;
1100 if ((pNMLV->uNewState==0)||(pNMLV->uNewState & LVIS_SELECTED)||(pNMLV->uNewState & LVIS_FOCUSED))
1101 return FALSE;
1103 CAutoWriteWeakLock writeLock(m_guard);
1104 if (!writeLock.IsAcquired())
1106 NotifyCheck();
1107 return FALSE;
1110 bool bSelected = !!(ListView_GetItemState(m_hWnd, pNMLV->iItem, LVIS_SELECTED) & LVIS_SELECTED);
1111 int nListItems = GetItemCount();
1113 // was the item checked?
1115 if (GetCheck(pNMLV->iItem))
1118 ScopedInDecrement blocker(m_nBlockItemChangeHandler);
1119 CheckEntry(pNMLV->iItem, nListItems);
1121 if (bSelected)
1123 ScopedInDecrement blocker(m_nBlockItemChangeHandler);
1124 POSITION pos = GetFirstSelectedItemPosition();
1125 int index;
1126 while ((index = GetNextSelectedItem(pos)) >= 0)
1128 if (index != pNMLV->iItem)
1129 CheckEntry(index, nListItems);
1133 else
1136 ScopedInDecrement blocker(m_nBlockItemChangeHandler);
1137 UncheckEntry(pNMLV->iItem, nListItems);
1139 if (bSelected)
1141 ScopedInDecrement blocker(m_nBlockItemChangeHandler);
1142 POSITION pos = GetFirstSelectedItemPosition();
1143 int index;
1144 while ((index = GetNextSelectedItem(pos)) >= 0)
1146 if (index != pNMLV->iItem)
1147 UncheckEntry(index, nListItems);
1152 GetStatisticsString();
1153 NotifyCheck();
1155 return FALSE;
1158 void CGitStatusListCtrl::CheckEntry(int index, int /*nListItems*/)
1160 CAutoWriteLock locker(m_guard);
1161 auto path = GetListEntry(index);
1162 if (!path)
1163 return;
1164 m_mapFilenameToChecked[path->GetGitPathString()] = true;
1165 SetCheck(index, TRUE);
1166 // if an unversioned item was checked, then we need to check if
1167 // the parent folders are unversioned too. If the parent folders actually
1168 // are unversioned, then check those too.
1169 #if 0
1170 if (entry->status == git_wc_status_unversioned)
1172 // we need to check the parent folder too
1173 const CTGitPath& folderpath = entry->path;
1174 for (int i=0; i< nListItems; ++i)
1176 FileEntry * testEntry = GetListEntry(i);
1177 ASSERT(testEntry);
1178 if (!testEntry)
1179 continue;
1180 if (!testEntry->checked)
1182 if (testEntry->path.IsAncestorOf(folderpath) && (!testEntry->path.IsEquivalentTo(folderpath)))
1184 SetEntryCheck(testEntry,i,true);
1185 m_nSelected++;
1190 bool bShift = !!(GetAsyncKeyState(VK_SHIFT) & 0x8000);
1191 if ( (entry->status == git_wc_status_deleted) || (m_bCheckChildrenWithParent) || (bShift) )
1193 // if a deleted folder gets checked, we have to check all
1194 // children of that folder too.
1195 if (entry->path.IsDirectory())
1196 SetCheckOnAllDescendentsOf(entry, true);
1198 // if a deleted file or folder gets checked, we have to
1199 // check all parents of this item too.
1200 for (int i=0; i<nListItems; ++i)
1202 FileEntry * testEntry = GetListEntry(i);
1203 ASSERT(testEntry);
1204 if (!testEntry)
1205 continue;
1206 if (!testEntry->checked)
1208 if (testEntry->path.IsAncestorOf(entry->path) && (!testEntry->path.IsEquivalentTo(entry->path)))
1210 if ((testEntry->status == git_wc_status_deleted)||(m_bCheckChildrenWithParent))
1212 SetEntryCheck(testEntry,i,true);
1213 m_nSelected++;
1214 // now we need to check all children of this parent folder
1215 SetCheckOnAllDescendentsOf(testEntry, true);
1221 #endif
1222 if ( !path->m_Checked )
1224 path->m_Checked = TRUE;
1225 m_nSelected++;
1229 void CGitStatusListCtrl::UncheckEntry(int index, int /*nListItems*/)
1231 CAutoWriteLock locker(m_guard);
1232 auto path = GetListEntry(index);
1233 if (!path)
1234 return;
1235 SetCheck(index, FALSE);
1236 m_mapFilenameToChecked[path->GetGitPathString()] = false;
1237 // item was unchecked
1238 #if 0
1239 if (entry->path.IsDirectory())
1241 // disable all files within an unselected folder, except when unchecking a folder with property changes
1242 bool bShift = !!(GetAsyncKeyState(VK_SHIFT) & 0x8000);
1243 if ( (entry->status != git_wc_status_modified) || (bShift) )
1245 SetCheckOnAllDescendentsOf(entry, false);
1248 else if (entry->status == git_wc_status_deleted)
1250 // a "deleted" file was unchecked, so uncheck all parent folders
1251 // and all children of those parents
1252 for (int i=0; i<nListItems; i++)
1254 FileEntry * testEntry = GetListEntry(i);
1255 ASSERT(testEntry);
1256 if (!testEntry)
1257 continue;
1258 if (testEntry->checked)
1260 if (testEntry->path.IsAncestorOf(entry->path))
1262 if (testEntry->status == git_wc_status_deleted)
1264 SetEntryCheck(testEntry,i,false);
1265 m_nSelected--;
1267 SetCheckOnAllDescendentsOf(testEntry, false);
1273 #endif
1274 if ( path->m_Checked )
1276 path->m_Checked = FALSE;
1277 m_nSelected--;
1280 void CGitStatusListCtrl::BuildStatistics()
1282 CAutoReadLock locker(m_guard);
1284 // now gather some statistics
1285 m_nUnversioned = 0;
1286 m_nNormal = 0;
1287 m_nModified = 0;
1288 m_nAdded = 0;
1289 m_nDeleted = 0;
1290 m_nConflicted = 0;
1291 m_nTotal = 0;
1292 m_nSelected = 0;
1293 m_nLineAdded = 0;
1294 m_nLineDeleted = 0;
1295 m_nRenamed = 0;
1297 for (size_t i = 0; i < m_arStatusArray.size(); ++i)
1299 int status = m_arStatusArray[i]->m_Action;
1301 m_nLineAdded += _wtol(m_arStatusArray[i]->m_StatAdd);
1302 m_nLineDeleted += _wtol(m_arStatusArray[i]->m_StatDel);
1304 if(status&(CTGitPath::LOGACTIONS_ADDED|CTGitPath::LOGACTIONS_COPY))
1305 m_nAdded++;
1307 if(status&CTGitPath::LOGACTIONS_DELETED)
1308 m_nDeleted++;
1310 if(status&(CTGitPath::LOGACTIONS_REPLACED|CTGitPath::LOGACTIONS_MODIFIED))
1311 m_nModified++;
1313 if(status&CTGitPath::LOGACTIONS_UNMERGED)
1314 m_nConflicted++;
1316 if(status&(CTGitPath::LOGACTIONS_IGNORE|CTGitPath::LOGACTIONS_UNVER))
1317 m_nUnversioned++;
1319 if(status&(CTGitPath::LOGACTIONS_REPLACED))
1320 m_nRenamed++;
1322 if (m_arStatusArray[i]->m_Checked)
1323 m_nSelected++;
1327 int CGitStatusListCtrl::GetGroupFromPoint(POINT * ppt)
1329 // the point must be relative to the upper left corner of the control
1331 if (!ppt)
1332 return -1;
1333 if (!IsGroupViewEnabled())
1334 return -1;
1336 POINT pt = *ppt;
1337 pt.x = 10;
1338 UINT flags = 0;
1339 int nItem = -1;
1340 RECT rc;
1341 GetWindowRect(&rc);
1342 while (((flags & LVHT_BELOW) == 0)&&(pt.y < rc.bottom))
1344 nItem = HitTest(pt, &flags);
1345 if ((flags & LVHT_ONITEM)||(flags & LVHT_EX_GROUP_HEADER))
1347 // the first item below the point
1349 // check if the point is too much right (i.e. if the point
1350 // is farther to the right than the width of the item)
1351 RECT r;
1352 GetItemRect(nItem, &r, LVIR_LABEL);
1353 if (ppt->x > r.right)
1354 return -1;
1356 LVITEM lv = {0};
1357 lv.mask = LVIF_GROUPID;
1358 lv.iItem = nItem;
1359 GetItem(&lv);
1360 int groupID = lv.iGroupId;
1361 // now we search upwards and check if the item above this one
1362 // belongs to another group. If it belongs to the same group,
1363 // we're not over a group header
1364 while (pt.y >= 0)
1366 pt.y -= 2;
1367 nItem = HitTest(pt, &flags);
1368 if ((flags & LVHT_ONITEM)&&(nItem >= 0))
1370 // the first item below the point
1371 LVITEM lv2 = {0};
1372 lv2.mask = LVIF_GROUPID;
1373 lv2.iItem = nItem;
1374 GetItem(&lv2);
1375 if (lv2.iGroupId != groupID)
1376 return groupID;
1377 else
1378 return -1;
1381 if (pt.y < 0)
1382 return groupID;
1383 return -1;
1385 pt.y += 2;
1387 return -1;
1390 void CGitStatusListCtrl::OnContextMenuGroup(CWnd * /*pWnd*/, CPoint point)
1392 POINT clientpoint = point;
1393 ScreenToClient(&clientpoint);
1394 if ((IsGroupViewEnabled())&&(GetGroupFromPoint(&clientpoint) >= 0))
1396 CAutoReadWeakLock readLock(m_guard);
1397 if (!readLock.IsAcquired())
1398 return;
1400 CMenu popup;
1401 if (popup.CreatePopupMenu())
1403 CString temp;
1404 temp.LoadString(IDS_STATUSLIST_CHECKGROUP);
1405 popup.AppendMenu(MF_STRING | MF_ENABLED, IDGITLC_CHECKGROUP, temp);
1406 temp.LoadString(IDS_STATUSLIST_UNCHECKGROUP);
1407 popup.AppendMenu(MF_STRING | MF_ENABLED, IDGITLC_UNCHECKGROUP, temp);
1408 int cmd = popup.TrackPopupMenu(TPM_RETURNCMD | TPM_LEFTALIGN | TPM_NONOTIFY, point.x, point.y, this);
1409 bool bCheck = false;
1410 switch (cmd)
1412 case IDGITLC_CHECKGROUP:
1413 bCheck = true;
1414 // fall through here
1415 case IDGITLC_UNCHECKGROUP:
1417 int group = GetGroupFromPoint(&clientpoint);
1418 // go through all items and check/uncheck those assigned to the group
1419 // but block the OnLvnItemChanged handler
1420 for (int i=0; i<GetItemCount(); ++i)
1422 LVITEM lv = { 0 };
1423 lv.mask = LVIF_GROUPID;
1424 lv.iItem = i;
1425 GetItem(&lv);
1427 if (lv.iGroupId == group)
1429 auto entry = GetListEntry(i);
1430 if (entry)
1432 bool bOldCheck = entry->m_Checked;
1433 SetEntryCheck(entry, i, bCheck);
1434 if (bCheck != bOldCheck)
1436 if (bCheck)
1437 m_nSelected++;
1438 else
1439 m_nSelected--;
1445 GetStatisticsString();
1446 NotifyCheck();
1448 break;
1454 void CGitStatusListCtrl::OnContextMenuList(CWnd * pWnd, CPoint point)
1456 bool bShift = !!(GetAsyncKeyState(VK_SHIFT) & 0x8000);
1458 CAutoWriteWeakLock writeLock(m_guard);
1459 if (!writeLock.IsAcquired())
1460 return;
1462 auto selectedCount = GetSelectedCount();
1463 int selIndex = GetSelectionMark();
1464 int selSubitem = -1;
1465 if (selectedCount > 0)
1467 CPoint pt = point;
1468 ScreenToClient(&pt);
1469 LVHITTESTINFO hittest = { 0 };
1470 hittest.flags = LVHT_ONITEM;
1471 hittest.pt = pt;
1472 if (this->SubItemHitTest(&hittest) >= 0)
1473 selSubitem = hittest.iSubItem;
1475 if ((point.x == -1) && (point.y == -1))
1477 CRect rect;
1478 GetItemRect(selIndex, &rect, LVIR_LABEL);
1479 ClientToScreen(&rect);
1480 point = rect.CenterPoint();
1482 if (selectedCount == 0 && m_bHasCheckboxes)
1484 // nothing selected could mean the context menu is requested for
1485 // a group header
1486 OnContextMenuGroup(pWnd, point);
1488 else if (selIndex >= 0)
1490 auto filepath = GetListEntry(selIndex);
1491 if (!filepath)
1492 return;
1494 //const CTGitPath& filepath = entry->path;
1495 int wcStatus = filepath->m_Action;
1496 // entry is selected, now show the popup menu
1497 CIconMenu popup;
1498 CMenu changelistSubMenu;
1499 CMenu ignoreSubMenu;
1500 CIconMenu clipSubMenu;
1501 CMenu shellMenu;
1502 if (popup.CreatePopupMenu())
1504 //Add Menu for version controlled file
1506 if (selectedCount > 0 && (wcStatus & CTGitPath::LOGACTIONS_UNMERGED))
1508 if (selectedCount == 1 && (m_dwContextMenus & GITSLC_POPCONFLICT))
1510 popup.AppendMenuIcon(IDGITLC_EDITCONFLICT, IDS_MENUCONFLICT, IDI_CONFLICT);
1511 popup.SetDefaultItem(IDGITLC_EDITCONFLICT, FALSE);
1513 if (m_dwContextMenus & GITSLC_POPRESOLVE)
1515 popup.AppendMenuIcon(IDGITLC_RESOLVECONFLICT, IDS_STATUSLIST_CONTEXT_RESOLVED, IDI_RESOLVE);
1516 CString tmp, mineTitle, theirsTitle;
1517 CAppUtils::GetConflictTitles(nullptr, mineTitle, theirsTitle, m_bIsRevertTheirMy);
1518 tmp.Format(IDS_SVNPROGRESS_MENURESOLVEUSING, (LPCTSTR)theirsTitle);
1519 if (m_bIsRevertTheirMy)
1521 popup.AppendMenuIcon(IDGITLC_RESOLVEMINE, tmp, IDI_RESOLVE);
1522 tmp.Format(IDS_SVNPROGRESS_MENURESOLVEUSING, (LPCTSTR)mineTitle);
1523 popup.AppendMenuIcon(IDGITLC_RESOLVETHEIRS, tmp, IDI_RESOLVE);
1525 else
1527 popup.AppendMenuIcon(IDGITLC_RESOLVETHEIRS, tmp, IDI_RESOLVE);
1528 tmp.Format(IDS_SVNPROGRESS_MENURESOLVEUSING, (LPCTSTR)mineTitle);
1529 popup.AppendMenuIcon(IDGITLC_RESOLVEMINE, tmp, IDI_RESOLVE);
1532 if ((m_dwContextMenus & GITSLC_POPCONFLICT)||(m_dwContextMenus & GITSLC_POPRESOLVE))
1533 popup.AppendMenu(MF_SEPARATOR);
1536 if (selectedCount > 0)
1538 if (wcStatus & (CTGitPath::LOGACTIONS_UNVER | CTGitPath::LOGACTIONS_IGNORE))
1540 if (m_dwContextMenus & GITSLC_POPADD)
1541 popup.AppendMenuIcon(IDGITLC_ADD, IDS_STATUSLIST_CONTEXT_ADD, IDI_ADD);
1542 if (m_dwContextMenus & GITSLC_POPCOMMIT)
1543 popup.AppendMenuIcon(IDGITLC_COMMIT, IDS_STATUSLIST_CONTEXT_COMMIT, IDI_COMMIT);
1547 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)
1549 bool bEntryAdded = false;
1550 if (m_dwContextMenus & GITSLC_POPCOMPAREWITHBASE)
1552 if(filepath->m_ParentNo & MERGE_MASK)
1553 popup.AppendMenuIcon(IDGITLC_COMPARE, IDS_TREE_DIFF, IDI_DIFF);
1554 else
1555 popup.AppendMenuIcon(IDGITLC_COMPARE, IDS_LOG_COMPAREWITHBASE, IDI_DIFF);
1557 if (!(wcStatus & (CTGitPath::LOGACTIONS_UNMERGED)) || selectedCount != 1)
1558 popup.SetDefaultItem(IDGITLC_COMPARE, FALSE);
1559 bEntryAdded = true;
1562 if (!g_Git.IsInitRepos() && (m_dwContextMenus & GITSLC_POPGNUDIFF))
1564 popup.AppendMenuIcon(IDGITLC_GNUDIFF1, IDS_LOG_POPUP_GNUDIFF, IDI_DIFF);
1565 bEntryAdded = true;
1568 if ((m_dwContextMenus & this->GetContextMenuBit(IDGITLC_COMPAREWC)) && m_bHasWC)
1570 if (!m_CurrentVersion.IsEmpty())
1572 popup.AppendMenuIcon(IDGITLC_COMPAREWC, IDS_LOG_POPUP_COMPARE, IDI_DIFF);
1573 bEntryAdded = true;
1577 if (bEntryAdded)
1578 popup.AppendMenu(MF_SEPARATOR);
1581 if (!m_Rev1.IsEmpty() && !m_Rev2.IsEmpty())
1583 if (m_dwContextMenus & this->GetContextMenuBit(IDGITLC_COMPARETWOREVISIONS))
1585 popup.AppendMenuIcon(IDGITLC_COMPARETWOREVISIONS, IDS_LOG_POPUP_COMPARETWO, IDI_DIFF);
1586 popup.SetDefaultItem(IDGITLC_COMPARETWOREVISIONS, FALSE);
1588 if (m_dwContextMenus & this->GetContextMenuBit(IDGITLC_GNUDIFF2REVISIONS))
1589 popup.AppendMenuIcon(IDGITLC_GNUDIFF2REVISIONS, IDS_LOG_POPUP_GNUDIFF, IDI_DIFF);
1592 //Select Multi item
1593 if (selectedCount > 0)
1595 if (selectedCount == 2 && (m_dwContextMenus & GITSLC_POPCOMPARETWOFILES))
1597 POSITION pos = GetFirstSelectedItemPosition();
1598 int index = GetNextSelectedItem(pos);
1599 if (index >= 0)
1601 auto entry2 = GetListEntry(index);
1602 bool firstEntryExistsAndIsFile = entry2 && !entry2->IsDirectory();
1603 index = GetNextSelectedItem(pos);
1604 if (index >= 0)
1606 entry2 = GetListEntry(index);
1607 if (firstEntryExistsAndIsFile && entry2 && !entry2->IsDirectory())
1608 popup.AppendMenuIcon(IDGITLC_COMPARETWOFILES, IDS_STATUSLIST_CONTEXT_COMPARETWOFILES, IDI_DIFF);
1614 if (selectedCount > 0 && (!(wcStatus & (CTGitPath::LOGACTIONS_UNVER | CTGitPath::LOGACTIONS_IGNORE))) && m_bHasWC)
1616 if ((m_dwContextMenus & GITSLC_POPCOMMIT) && m_CurrentVersion.IsEmpty() && !(wcStatus & (CTGitPath::LOGACTIONS_SKIPWORKTREE | CTGitPath::LOGACTIONS_ASSUMEVALID)))
1617 popup.AppendMenuIcon(IDGITLC_COMMIT, IDS_STATUSLIST_CONTEXT_COMMIT, IDI_COMMIT);
1619 if ((m_dwContextMenus & GITSLC_POPREVERT) && m_CurrentVersion.IsEmpty())
1620 popup.AppendMenuIcon(IDGITLC_REVERT, IDS_MENUREVERT, IDI_REVERT);
1622 if ((m_dwContextMenus & GITSLC_POPSKIPWORKTREE) && m_CurrentVersion.IsEmpty() && !(wcStatus & (CTGitPath::LOGACTIONS_ADDED | CTGitPath::LOGACTIONS_UNMERGED | CTGitPath::LOGACTIONS_SKIPWORKTREE)))
1623 popup.AppendMenuIcon(IDGITLC_SKIPWORKTREE, IDS_STATUSLIST_SKIPWORKTREE);
1625 if ((m_dwContextMenus & GITSLC_POPASSUMEVALID) && m_CurrentVersion.IsEmpty() && !(wcStatus & (CTGitPath::LOGACTIONS_ADDED | CTGitPath::LOGACTIONS_DELETED | CTGitPath::LOGACTIONS_UNMERGED | CTGitPath::LOGACTIONS_ASSUMEVALID)))
1626 popup.AppendMenuIcon(IDGITLC_ASSUMEVALID, IDS_MENUASSUMEVALID);
1628 if ((m_dwContextMenus & GITSLC_POPUNSETIGNORELOCALCHANGES) && m_CurrentVersion.IsEmpty() && (wcStatus & (CTGitPath::LOGACTIONS_SKIPWORKTREE | CTGitPath::LOGACTIONS_ASSUMEVALID)))
1629 popup.AppendMenuIcon(IDGITLC_UNSETIGNORELOCALCHANGES, IDS_STATUSLIST_UNSETIGNORELOCALCHANGES);
1631 if (m_dwContextMenus & GITSLC_POPRESTORE && !filepath->IsDirectory())
1633 if (m_restorepaths.find(filepath->GetWinPathString()) == m_restorepaths.end())
1634 popup.AppendMenuIcon(IDGITLC_CREATERESTORE, IDS_MENUCREATERESTORE, IDI_RESTORE);
1635 else
1636 popup.AppendMenuIcon(IDGITLC_RESTOREPATH, IDS_MENURESTORE, IDI_RESTORE);
1639 if ((m_dwContextMenus & GetContextMenuBit(IDGITLC_REVERTTOREV)) && !m_CurrentVersion.IsEmpty() && !(wcStatus & CTGitPath::LOGACTIONS_DELETED))
1641 popup.AppendMenuIcon(IDGITLC_REVERTTOREV, IDS_LOG_POPUP_REVERTTOREV, IDI_REVERT);
1644 if ((m_dwContextMenus & GetContextMenuBit(IDGITLC_REVERTTOPARENT)) && !m_CurrentVersion.IsEmpty() && !(wcStatus & CTGitPath::LOGACTIONS_ADDED))
1646 popup.AppendMenuIcon(IDGITLC_REVERTTOPARENT, IDS_LOG_POPUP_REVERTTOPARENT, IDI_REVERT);
1650 if (selectedCount == 1 && !(wcStatus & (CTGitPath::LOGACTIONS_UNVER | CTGitPath::LOGACTIONS_IGNORE)))
1652 if (m_dwContextMenus & GITSLC_POPSHOWLOG)
1653 popup.AppendMenuIcon(IDGITLC_LOG, IDS_REPOBROWSE_SHOWLOG, IDI_LOG);
1654 if (m_dwContextMenus & GITSLC_POPSHOWLOGSUBMODULE && filepath->IsDirectory())
1655 popup.AppendMenuIcon(IDGITLC_LOGSUBMODULE, IDS_LOG_SUBMODULE, IDI_LOG);
1656 if (m_dwContextMenus & GITSLC_POPSHOWLOGOLDNAME && (wcStatus & (CTGitPath::LOGACTIONS_REPLACED|CTGitPath::LOGACTIONS_COPY) && !filepath->GetGitOldPathString().IsEmpty()))
1657 popup.AppendMenuIcon(IDGITLC_LOGOLDNAME, IDS_STATUSLIST_SHOWLOGOLDNAME, IDI_LOG);
1658 if ((m_dwContextMenus & GITSLC_POPBLAME) && !filepath->IsDirectory() && !(wcStatus & CTGitPath::LOGACTIONS_DELETED) && !((wcStatus & CTGitPath::LOGACTIONS_ADDED) && m_CurrentVersion.IsEmpty()) && m_bHasWC)
1659 popup.AppendMenuIcon(IDGITLC_BLAME, IDS_MENUBLAME, IDI_BLAME);
1662 if (selectedCount > 0)
1664 if ((m_dwContextMenus & GetContextMenuBit(IDGITLC_EXPORT)) && !(wcStatus & (CTGitPath::LOGACTIONS_DELETED | CTGitPath::LOGACTIONS_MISSING)))
1665 popup.AppendMenuIcon(IDGITLC_EXPORT, IDS_LOG_POPUP_EXPORT, IDI_EXPORT);
1668 if (selectedCount == 1)
1670 if (m_dwContextMenus & this->GetContextMenuBit(IDGITLC_SAVEAS) && ! filepath->IsDirectory() && !(wcStatus & CTGitPath::LOGACTIONS_DELETED))
1671 popup.AppendMenuIcon(IDGITLC_SAVEAS, IDS_LOG_POPUP_SAVE, IDI_SAVEAS);
1673 if (m_dwContextMenus & GITSLC_POPOPEN && !filepath->IsDirectory() && !(wcStatus & (CTGitPath::LOGACTIONS_DELETED | CTGitPath::LOGACTIONS_MISSING)))
1675 popup.AppendMenuIcon(IDGITLC_VIEWREV, IDS_LOG_POPUP_VIEWREV, IDI_NOTEPAD);
1676 popup.AppendMenuIcon(IDGITLC_OPEN, IDS_REPOBROWSE_OPEN, IDI_OPEN);
1677 popup.AppendMenuIcon(IDGITLC_OPENWITH, IDS_LOG_POPUP_OPENWITH, IDI_OPEN);
1680 if (m_dwContextMenus & GITSLC_POPEXPLORE && !(wcStatus & CTGitPath::LOGACTIONS_DELETED) && m_bHasWC)
1681 popup.AppendMenuIcon(IDGITLC_EXPLORE, IDS_STATUSLIST_CONTEXT_EXPLORE, IDI_EXPLORER);
1683 if (m_dwContextMenus & GITSLC_POPPREPAREDIFF && !(wcStatus & CTGitPath::LOGACTIONS_DELETED))
1685 popup.AppendMenu(MF_SEPARATOR);
1686 popup.AppendMenuIcon(IDGITLC_PREPAREDIFF, IDS_PREPAREDIFF, IDI_DIFF);
1687 if (!m_sMarkForDiffFilename.IsEmpty())
1689 CString diffWith;
1690 if (filepath->GetGitPathString() == m_sMarkForDiffFilename)
1691 diffWith = m_sMarkForDiffVersion;
1692 else
1694 PathCompactPathEx(CStrBuf(diffWith, 2 * GIT_HASH_SIZE), m_sMarkForDiffFilename, 2 * GIT_HASH_SIZE, 0);
1695 diffWith += L':' + m_sMarkForDiffVersion.Left(g_Git.GetShortHASHLength());
1697 CString menuEntry;
1698 menuEntry.Format(IDS_MENUDIFFNOW, (LPCTSTR)diffWith);
1699 popup.AppendMenuIcon(IDGITLC_PREPAREDIFF_COMPARE, menuEntry, IDI_DIFF);
1703 if (selectedCount > 0)
1705 // if (((wcStatus == git_wc_status_unversioned)||(wcStatus == git_wc_status_ignored))&&(m_dwContextMenus & SVNSLC_POPDELETE))
1706 // popup.AppendMenuIcon(IDSVNLC_DELETE, IDS_MENUREMOVE, IDI_DELETE);
1707 // 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))
1708 // {
1709 // if (bShift)
1710 // popup.AppendMenuIcon(IDGitLC_REMOVE, IDS_MENUREMOVEKEEP, IDI_DELETE);
1711 // else
1712 // popup.AppendMenuIcon(IDGitLC_REMOVE, IDS_MENUREMOVE, IDI_DELETE);
1713 // }
1714 if ((wcStatus & (CTGitPath::LOGACTIONS_UNVER | CTGitPath::LOGACTIONS_IGNORE | CTGitPath::LOGACTIONS_MISSING))/*||(wcStatus == git_wc_status_deleted)*/)
1716 if (m_dwContextMenus & GITSLC_POPDELETE)
1717 popup.AppendMenuIcon(IDGITLC_DELETE, IDS_MENUREMOVE, IDI_DELETE);
1719 if ( (wcStatus & CTGitPath::LOGACTIONS_UNVER || wcStatus & CTGitPath::LOGACTIONS_DELETED) )
1721 if (m_dwContextMenus & GITSLC_POPIGNORE)
1723 CTGitPathList ignorelist;
1724 FillListOfSelectedItemPaths(ignorelist);
1725 //check if all selected entries have the same extension
1726 bool bSameExt = true;
1727 CString sExt;
1728 for (int i=0; i<ignorelist.GetCount(); ++i)
1730 if (sExt.IsEmpty() && (i==0))
1731 sExt = ignorelist[i].GetFileExtension();
1732 else if (sExt.CompareNoCase(ignorelist[i].GetFileExtension())!=0)
1733 bSameExt = false;
1735 if (bSameExt)
1737 if (ignoreSubMenu.CreateMenu())
1739 CString ignorepath;
1740 if (ignorelist.GetCount()==1)
1741 ignorepath = ignorelist[0].GetFileOrDirectoryName();
1742 else
1743 ignorepath.Format(IDS_MENUIGNOREMULTIPLE, ignorelist.GetCount());
1744 ignoreSubMenu.AppendMenu(MF_STRING | MF_ENABLED, IDGITLC_IGNORE, ignorepath);
1745 ignorepath = L'*' + sExt;
1746 ignoreSubMenu.AppendMenu(MF_STRING | MF_ENABLED, IDGITLC_IGNOREMASK, ignorepath);
1747 if (ignorelist.GetCount() == 1 && !ignorelist[0].GetContainingDirectory().GetGitPathString().IsEmpty())
1748 ignoreSubMenu.AppendMenu(MF_STRING | MF_ENABLED, IDGITLC_IGNOREFOLDER, ignorelist[0].GetContainingDirectory().GetGitPathString());
1749 CString temp;
1750 temp.LoadString(IDS_MENUIGNORE);
1751 popup.InsertMenu((UINT)-1, MF_BYPOSITION | MF_POPUP, (UINT_PTR)ignoreSubMenu.m_hMenu, temp);
1754 else
1756 CString temp;
1757 if (ignorelist.GetCount()==1)
1758 temp.LoadString(IDS_MENUIGNORE);
1759 else
1760 temp.Format(IDS_MENUIGNOREMULTIPLE, ignorelist.GetCount());
1761 popup.AppendMenuIcon(IDGITLC_IGNORE, temp, IDI_IGNORE);
1762 temp.Format(IDS_MENUIGNOREMULTIPLEMASK, ignorelist.GetCount());
1763 popup.AppendMenuIcon(IDGITLC_IGNOREMASK, temp, IDI_IGNORE);
1771 if (selectedCount > 0)
1773 popup.AppendMenu(MF_SEPARATOR);
1775 if (clipSubMenu.CreatePopupMenu())
1777 CString temp;
1778 clipSubMenu.AppendMenuIcon(IDGITLC_COPYFULL, IDS_STATUSLIST_CONTEXT_COPYFULLPATHS, IDI_COPYCLIP);
1779 clipSubMenu.AppendMenuIcon(IDGITLC_COPYRELPATHS, IDS_STATUSLIST_CONTEXT_COPYRELPATHS, IDI_COPYCLIP);
1780 clipSubMenu.AppendMenuIcon(IDGITLC_COPYFILENAMES, IDS_STATUSLIST_CONTEXT_COPYFILENAMES, IDI_COPYCLIP);
1781 clipSubMenu.AppendMenuIcon(IDGITLC_COPYEXT, IDS_STATUSLIST_CONTEXT_COPYEXT, IDI_COPYCLIP);
1782 if (selSubitem >= 0)
1784 temp.Format(IDS_STATUSLIST_CONTEXT_COPYCOL, (LPCWSTR)m_ColumnManager.GetName(selSubitem));
1785 clipSubMenu.AppendMenuIcon(IDGITLC_COPYCOL, temp, IDI_COPYCLIP);
1787 temp.LoadString(IDS_LOG_POPUP_COPYTOCLIPBOARD);
1788 popup.InsertMenu((UINT)-1, MF_BYPOSITION | MF_POPUP, (UINT_PTR)clipSubMenu.m_hMenu, temp);
1791 #if 0
1792 if ((m_dwContextMenus & SVNSLC_POPCHANGELISTS))
1793 &&(wcStatus != git_wc_status_unversioned)&&(wcStatus != git_wc_status_none))
1795 popup.AppendMenu(MF_SEPARATOR);
1796 // changelist commands
1797 size_t numChangelists = GetNumberOfChangelistsInSelection();
1798 if (numChangelists > 0)
1799 popup.AppendMenuIcon(IDSVNLC_REMOVEFROMCS, IDS_STATUSLIST_CONTEXT_REMOVEFROMCS);
1800 if ((!entry->IsFolder())&&(changelistSubMenu.CreateMenu()))
1802 CString temp;
1803 temp.LoadString(IDS_STATUSLIST_CONTEXT_CREATECS);
1804 changelistSubMenu.AppendMenu(MF_STRING | MF_ENABLED, IDSVNLC_CREATECS, temp);
1806 if (entry->changelist.Compare(SVNSLC_IGNORECHANGELIST))
1808 changelistSubMenu.AppendMenu(MF_SEPARATOR);
1809 changelistSubMenu.AppendMenu(MF_STRING | MF_ENABLED, IDSVNLC_CREATEIGNORECS, SVNSLC_IGNORECHANGELIST);
1812 if (!m_changelists.empty())
1814 // find the changelist names
1815 bool bNeedSeparator = true;
1816 int cmdID = IDSVNLC_MOVETOCS;
1817 for (auto it = m_changelists.cbegin(); it != m_changelists.cend(); ++it)
1819 if ((entry->changelist.Compare(it->first))&&(it->first.Compare(SVNSLC_IGNORECHANGELIST)))
1821 if (bNeedSeparator)
1823 changelistSubMenu.AppendMenu(MF_SEPARATOR);
1824 bNeedSeparator = false;
1826 changelistSubMenu.AppendMenu(MF_STRING | MF_ENABLED, cmdID, it->first);
1827 cmdID++;
1831 temp.LoadString(IDS_STATUSLIST_CONTEXT_MOVETOCS);
1832 popup.AppendMenu(MF_POPUP|MF_STRING, (UINT_PTR)changelistSubMenu.GetSafeHmenu(), temp);
1835 #endif
1838 m_hShellMenu = nullptr;
1839 if (selectedCount > 0 && !(wcStatus & (CTGitPath::LOGACTIONS_DELETED | CTGitPath::LOGACTIONS_MISSING)) && m_bHasWC && m_CurrentVersion.IsEmpty() && shellMenu.CreatePopupMenu())
1841 // insert the shell context menu
1842 popup.AppendMenu(MF_SEPARATOR);
1843 popup.InsertMenu((UINT)-1, MF_BYPOSITION | MF_POPUP, (UINT_PTR)shellMenu.m_hMenu, CString(MAKEINTRESOURCE(IDS_STATUSLIST_CONTEXT_SHELL)));
1844 m_hShellMenu = shellMenu.m_hMenu;
1847 int cmd = popup.TrackPopupMenu(TPM_RETURNCMD | TPM_LEFTALIGN | TPM_RIGHTBUTTON, point.x, point.y, this);
1848 g_IContext2 = nullptr;
1849 g_IContext3 = nullptr;
1850 if (m_pContextMenu)
1852 if (cmd >= SHELL_MIN_CMD && cmd <= SHELL_MAX_CMD) // see if returned idCommand belongs to shell menu entries)
1854 CMINVOKECOMMANDINFOEX cmi = { 0 };
1855 cmi.cbSize = sizeof(CMINVOKECOMMANDINFOEX);
1856 cmi.fMask = CMIC_MASK_UNICODE | CMIC_MASK_PTINVOKE;
1857 if (GetKeyState(VK_CONTROL) < 0)
1858 cmi.fMask |= CMIC_MASK_CONTROL_DOWN;
1859 if (bShift)
1860 cmi.fMask |= CMIC_MASK_SHIFT_DOWN;
1861 cmi.hwnd = m_hWnd;
1862 cmi.lpVerb = MAKEINTRESOURCEA(cmd - SHELL_MIN_CMD);
1863 cmi.lpVerbW = MAKEINTRESOURCEW(cmd - SHELL_MIN_CMD);
1864 cmi.nShow = SW_SHOWNORMAL;
1865 cmi.ptInvoke = point;
1867 m_pContextMenu->InvokeCommand((LPCMINVOKECOMMANDINFO)&cmi);
1869 cmd = 0;
1871 m_pContextMenu->Release();
1872 m_pContextMenu = nullptr;
1874 if (g_pFolderhook)
1876 delete g_pFolderhook;
1877 g_pFolderhook = nullptr;
1879 if (g_psfDesktopFolder)
1881 g_psfDesktopFolder->Release();
1882 g_psfDesktopFolder = nullptr;
1884 for (int i = 0; i < g_pidlArrayItems; i++)
1886 if (g_pidlArray[i])
1887 CoTaskMemFree(g_pidlArray[i]);
1889 if (g_pidlArray)
1890 CoTaskMemFree(g_pidlArray);
1891 g_pidlArray = nullptr;
1892 g_pidlArrayItems = 0;
1894 m_bWaitCursor = true;
1895 bShift = !!(GetAsyncKeyState(VK_SHIFT) & 0x8000);
1896 //int iItemCountBeforeMenuCmd = GetItemCount();
1897 //bool bForce = false;
1898 switch (cmd)
1900 case IDGITLC_VIEWREV:
1901 OpenFile(filepath, ALTERNATIVEEDITOR);
1902 break;
1904 case IDGITLC_OPEN:
1905 OpenFile(filepath,OPEN);
1906 break;
1908 case IDGITLC_OPENWITH:
1909 OpenFile(filepath,OPEN_WITH);
1910 break;
1912 case IDGITLC_EXPLORE:
1913 CAppUtils::ExploreTo(GetSafeHwnd(), g_Git.CombinePath(filepath));
1914 break;
1916 case IDGITLC_PREPAREDIFF:
1917 m_sMarkForDiffFilename = filepath->GetGitPathString();
1918 m_sMarkForDiffVersion = m_CurrentVersion;
1919 break;
1921 case IDGITLC_PREPAREDIFF_COMPARE:
1923 CTGitPath savedFile(m_sMarkForDiffFilename);
1924 CGitDiff::Diff(GetParentHWND(), filepath, &savedFile, m_CurrentVersion.ToString(), m_sMarkForDiffVersion, false, false, 0, bShift);
1926 break;
1928 case IDGITLC_CREATERESTORE:
1930 POSITION pos = GetFirstSelectedItemPosition();
1931 while (pos)
1933 int index = GetNextSelectedItem(pos);
1934 auto entry2 = GetListEntry(index);
1935 if (!entry2 || entry2->IsDirectory())
1936 continue;
1937 if (m_restorepaths.find(entry2->GetWinPathString()) != m_restorepaths.end())
1938 continue;
1939 CTGitPath tempFile = CTempFiles::Instance().GetTempFilePath(false);
1940 // delete the temp file: the temp file has the FILE_ATTRIBUTE_TEMPORARY flag set
1941 // and copying the real file over it would leave that temp flag.
1942 DeleteFile(tempFile.GetWinPath());
1943 if (CopyFile(g_Git.CombinePath(entry2), tempFile.GetWinPath(), FALSE))
1945 m_restorepaths[entry2->GetWinPathString()] = tempFile.GetWinPathString();
1946 SetItemState(index, INDEXTOOVERLAYMASK(OVL_RESTORE), LVIS_OVERLAYMASK);
1949 Invalidate();
1951 break;
1953 case IDGITLC_RESTOREPATH:
1955 if (CMessageBox::Show(GetParentHWND(), IDS_STATUSLIST_RESTOREPATH, IDS_APPNAME, 2, IDI_QUESTION, IDS_RESTOREBUTTON, IDS_ABORTBUTTON) == 2)
1956 break;
1957 POSITION pos = GetFirstSelectedItemPosition();
1958 while (pos)
1960 int index = GetNextSelectedItem(pos);
1961 auto entry2 = GetListEntry(index);
1962 if (!entry2)
1963 continue;
1964 if (m_restorepaths.find(entry2->GetWinPathString()) == m_restorepaths.end())
1965 continue;
1966 if (CopyFile(m_restorepaths[entry2->GetWinPathString()], g_Git.CombinePath(entry2), FALSE))
1968 CPathUtils::Touch(entry2->GetWinPathString());
1969 m_restorepaths.erase(entry2->GetWinPathString());
1970 SetItemState(index, 0, LVIS_OVERLAYMASK);
1973 Invalidate();
1975 break;
1977 // Compare current version and work copy.
1978 case IDGITLC_COMPAREWC:
1980 if (!CheckMultipleDiffs())
1981 break;
1982 POSITION pos = GetFirstSelectedItemPosition();
1983 while ( pos )
1985 int index = GetNextSelectedItem(pos);
1986 StartDiffWC(index);
1989 break;
1991 // Compare with base version. when current version is zero, compare workcopy and HEAD.
1992 case IDGITLC_COMPARE:
1994 if (!CheckMultipleDiffs())
1995 break;
1996 POSITION pos = GetFirstSelectedItemPosition();
1997 while ( pos )
1999 int index = GetNextSelectedItem(pos);
2000 StartDiff(index);
2003 break;
2005 case IDGITLC_COMPARETWOREVISIONS:
2007 if (!CheckMultipleDiffs())
2008 break;
2009 POSITION pos = GetFirstSelectedItemPosition();
2010 while ( pos )
2012 int index = GetNextSelectedItem(pos);
2013 StartDiffTwo(index);
2016 break;
2018 case IDGITLC_COMPARETWOFILES:
2020 POSITION pos = GetFirstSelectedItemPosition();
2021 if (pos)
2023 auto firstfilepath = GetListEntry(GetNextSelectedItem(pos));
2024 if (!firstfilepath)
2025 break;
2027 auto secondfilepath = GetListEntry(GetNextSelectedItem(pos));
2028 if (!secondfilepath)
2029 break;
2031 CString sCmd;
2032 if (m_CurrentVersion.IsEmpty())
2033 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);
2034 else
2035 sCmd.Format(L"/command:diff /path:\"%s\" /startrev:%s /path2:\"%s\" /endrev:%s /hwnd:%p", firstfilepath->GetWinPath(), firstfilepath->m_Action & CTGitPath::LOGACTIONS_DELETED ? (LPCTSTR)(m_CurrentVersion.ToString() + L"~1") : (LPCTSTR)m_CurrentVersion.ToString(), secondfilepath->GetWinPath(), secondfilepath->m_Action & CTGitPath::LOGACTIONS_DELETED ? (LPCTSTR)(m_CurrentVersion.ToString() + L"~1") : (LPCTSTR)m_CurrentVersion.ToString(), (void*)m_hWnd);
2036 if (bShift)
2037 sCmd += L" /alternative";
2038 CAppUtils::RunTortoiseGitProc(sCmd);
2041 break;
2043 case IDGITLC_GNUDIFF1:
2045 if (!CheckMultipleDiffs())
2046 break;
2047 POSITION pos = GetFirstSelectedItemPosition();
2048 while (pos)
2050 auto selectedFilepath = GetListEntry(GetNextSelectedItem(pos));
2051 if (m_CurrentVersion.IsEmpty())
2053 CString fromwhere;
2054 if (m_amend)
2055 fromwhere = L"~1";
2056 CAppUtils::StartShowUnifiedDiff(m_hWnd, *selectedFilepath, GitRev::GetHead() + fromwhere, *selectedFilepath, GitRev::GetWorkingCopy(), bShift);
2058 else
2060 if ((selectedFilepath->m_ParentNo & (PARENT_MASK | MERGE_MASK)) == 0)
2061 CAppUtils::StartShowUnifiedDiff(m_hWnd, *selectedFilepath, m_CurrentVersion.ToString() + L"~1", *selectedFilepath, m_CurrentVersion.ToString(), bShift);
2062 else
2064 CString str;
2065 if (!(selectedFilepath->m_ParentNo & MERGE_MASK))
2066 str.Format(L"%s^%d", (LPCTSTR)m_CurrentVersion.ToString(), (selectedFilepath->m_ParentNo & PARENT_MASK) + 1);
2068 CAppUtils::StartShowUnifiedDiff(m_hWnd, *selectedFilepath, str, *selectedFilepath, m_CurrentVersion.ToString(), bShift, false, false, false, !!(selectedFilepath->m_ParentNo & MERGE_MASK));
2073 break;
2075 case IDGITLC_GNUDIFF2REVISIONS:
2077 if (!CheckMultipleDiffs())
2078 break;
2079 POSITION pos = GetFirstSelectedItemPosition();
2080 while (pos)
2082 auto entry = GetListEntry(GetNextSelectedItem(pos));
2083 CAppUtils::StartShowUnifiedDiff(m_hWnd, *entry, m_Rev2, *entry, m_Rev1, bShift);
2086 break;
2088 case IDGITLC_ADD:
2090 CTGitPathList paths;
2091 FillListOfSelectedItemPaths(paths, true);
2093 CGitProgressDlg progDlg;
2094 AddProgressCommand addCommand;
2095 progDlg.SetCommand(&addCommand);
2096 addCommand.SetShowCommitButtonAfterAdd((m_dwContextMenus & GITSLC_POPCOMMIT) != 0);
2097 addCommand.SetPathList(paths);
2098 progDlg.SetItemCount(paths.GetCount());
2099 progDlg.DoModal();
2101 // reset unchecked status
2102 POSITION pos = GetFirstSelectedItemPosition();
2103 int index;
2104 while ((index = GetNextSelectedItem(pos)) >= 0)
2105 m_mapFilenameToChecked.erase(GetListEntry(index)->GetGitPathString());
2107 if (GetLogicalParent() && GetLogicalParent()->GetSafeHwnd())
2108 GetLogicalParent()->SendMessage(GITSLNM_NEEDSREFRESH);
2110 SetRedraw(TRUE);
2112 break;
2114 case IDGITLC_DELETE:
2115 DeleteSelectedFiles();
2116 break;
2118 case IDGITLC_BLAME:
2120 CAppUtils::LaunchTortoiseBlame(g_Git.CombinePath(filepath), m_CurrentVersion.ToString());
2122 break;
2124 case IDGITLC_LOG:
2125 case IDGITLC_LOGSUBMODULE:
2127 CString sCmd;
2128 sCmd.Format(L"/command:log /path:\"%s\"", (LPCTSTR)g_Git.CombinePath(filepath));
2129 if (cmd == IDGITLC_LOG && filepath->IsDirectory())
2130 sCmd += L" /submodule";
2131 if (!m_sDisplayedBranch.IsEmpty())
2132 sCmd += L" /range:\"" + m_sDisplayedBranch + L'"';
2133 CAppUtils::RunTortoiseGitProc(sCmd, false, !(cmd == IDGITLC_LOGSUBMODULE));
2135 break;
2137 case IDGITLC_LOGOLDNAME:
2139 CTGitPath oldName(filepath->GetGitOldPathString());
2140 CString sCmd;
2141 sCmd.Format(L"/command:log /path:\"%s\"", (LPCTSTR)g_Git.CombinePath(oldName));
2142 if (!m_sDisplayedBranch.IsEmpty())
2143 sCmd += L" /range:\"" + m_sDisplayedBranch + L'"';
2144 CAppUtils::RunTortoiseGitProc(sCmd);
2146 break;
2148 case IDGITLC_EDITCONFLICT:
2150 if (CAppUtils::ConflictEdit(GetParentHWND(), *filepath, bShift, m_bIsRevertTheirMy, GetLogicalParent() ? GetLogicalParent()->GetSafeHwnd() : nullptr))
2152 CString conflictedFile = g_Git.CombinePath(filepath);
2153 if (!PathFileExists(conflictedFile) && GetLogicalParent() && GetLogicalParent()->GetSafeHwnd())
2155 GetLogicalParent()->SendMessage(GITSLNM_NEEDSREFRESH);
2156 break;
2158 StoreScrollPos();
2159 Show(m_dwShow, 0, m_bShowFolders, 0, true);
2162 break;
2164 case IDGITLC_RESOLVETHEIRS: //follow up
2165 case IDGITLC_RESOLVEMINE: //follow up
2166 case IDGITLC_RESOLVECONFLICT:
2168 if (CMessageBox::Show(GetParentHWND(), IDS_PROC_RESOLVE, IDS_APPNAME, MB_ICONQUESTION | MB_YESNO) == IDYES)
2170 bool needsFullRefresh = false;
2171 POSITION pos = GetFirstSelectedItemPosition();
2172 while (pos != 0)
2174 int index;
2175 index = GetNextSelectedItem(pos);
2176 auto fentry = GetListEntry(index);
2177 if (!fentry)
2178 continue;
2180 CAppUtils::resolve_with resolveWith = CAppUtils::RESOLVE_WITH_CURRENT;
2181 if (((!this->m_bIsRevertTheirMy) && cmd == IDGITLC_RESOLVETHEIRS) || ((this->m_bIsRevertTheirMy) && cmd == IDGITLC_RESOLVEMINE))
2182 resolveWith = CAppUtils::RESOLVE_WITH_THEIRS;
2183 else if (((!this->m_bIsRevertTheirMy) && cmd == IDGITLC_RESOLVEMINE) || ((this->m_bIsRevertTheirMy) && cmd == IDGITLC_RESOLVETHEIRS))
2184 resolveWith = CAppUtils::RESOLVE_WITH_MINE;
2185 if (CAppUtils::ResolveConflict(GetParentHWND(), *fentry, resolveWith) == 0 && fentry->m_Action & CTGitPath::LOGACTIONS_UNMERGED)
2186 needsFullRefresh = true;
2188 if (needsFullRefresh && CRegDWORD(L"Software\\TortoiseGit\\RefreshFileListAfterResolvingConflict", TRUE) == TRUE)
2190 CWnd* pParent = GetLogicalParent();
2191 if (pParent && pParent->GetSafeHwnd())
2192 pParent->SendMessage(GITSLNM_NEEDSREFRESH);
2193 SetRedraw(TRUE);
2194 break;
2196 StoreScrollPos();
2197 Show(m_dwShow, 0, m_bShowFolders,0,true);
2200 break;
2202 case IDGITLC_IGNORE:
2204 CTGitPathList ignorelist;
2205 //std::vector<CString> toremove;
2206 FillListOfSelectedItemPaths(ignorelist, true);
2208 if (!CAppUtils::IgnoreFile(GetParentHWND(), ignorelist, false))
2209 break;
2211 SetRedraw(FALSE);
2212 CWnd* pParent = GetLogicalParent();
2213 if (pParent && pParent->GetSafeHwnd())
2215 pParent->SendMessage(GITSLNM_NEEDSREFRESH);
2217 SetRedraw(TRUE);
2219 break;
2221 case IDGITLC_IGNOREMASK:
2223 CString common;
2224 CString ext=filepath->GetFileExtension();
2225 CTGitPathList ignorelist;
2226 FillListOfSelectedItemPaths(ignorelist, true);
2228 if (!CAppUtils::IgnoreFile(GetParentHWND(), ignorelist, true))
2229 break;
2231 SetRedraw(FALSE);
2232 CWnd* pParent = GetLogicalParent();
2233 if (pParent && pParent->GetSafeHwnd())
2235 pParent->SendMessage(GITSLNM_NEEDSREFRESH);
2238 SetRedraw(TRUE);
2240 break;
2242 case IDGITLC_IGNOREFOLDER:
2244 CTGitPathList ignorelist;
2245 ignorelist.AddPath(filepath->GetContainingDirectory());
2247 if (!CAppUtils::IgnoreFile(GetParentHWND(), ignorelist, false))
2248 break;
2250 SetRedraw(FALSE);
2251 CWnd *pParent = GetLogicalParent();
2252 if (pParent && pParent->GetSafeHwnd())
2253 pParent->SendMessage(GITSLNM_NEEDSREFRESH);
2255 SetRedraw(TRUE);
2257 break;
2258 case IDGITLC_COMMIT:
2260 CTGitPathList targetList;
2261 FillListOfSelectedItemPaths(targetList);
2262 CTGitPath tempFile = CTempFiles::Instance().GetTempFilePath(false);
2263 VERIFY(targetList.WriteToFile(tempFile.GetWinPathString()));
2264 CString commandline = L"/command:commit /pathfile:\"";
2265 commandline += tempFile.GetWinPathString();
2266 commandline += L'"';
2267 commandline += L" /deletepathfile";
2268 CAppUtils::RunTortoiseGitProc(commandline);
2270 break;
2271 case IDGITLC_REVERT:
2273 // If at least one item is not in the status "added"
2274 // we ask for a confirmation
2275 BOOL bConfirm = FALSE;
2276 POSITION pos = GetFirstSelectedItemPosition();
2277 int index;
2278 while ((index = GetNextSelectedItem(pos)) >= 0)
2280 auto fentry = GetListEntry(index);
2281 if(fentry && fentry->m_Action &CTGitPath::LOGACTIONS_MODIFIED && !fentry->IsDirectory())
2283 bConfirm = TRUE;
2284 break;
2288 CString str;
2289 str.Format(IDS_PROC_WARNREVERT, selectedCount);
2291 if (!bConfirm || MessageBox(str, L"TortoiseGit", MB_YESNO | MB_ICONQUESTION) == IDYES)
2293 CTGitPathList targetList;
2294 FillListOfSelectedItemPaths(targetList);
2296 // make sure that the list is reverse sorted, so that
2297 // children are removed before any parents
2298 targetList.SortByPathname(true);
2300 // put all reverted files in the trashbin, except the ones with 'added'
2301 // status because they are not restored by the revert.
2302 CTGitPathList delList;
2303 POSITION pos2 = GetFirstSelectedItemPosition();
2304 int index2;
2305 while ((index2 = GetNextSelectedItem(pos2)) >= 0)
2307 auto entry = GetListEntry(index2);
2308 if (entry&&(!(entry->m_Action& CTGitPath::LOGACTIONS_ADDED))
2309 && (!(entry->m_Action& CTGitPath::LOGACTIONS_REPLACED)) && !entry->IsDirectory())
2311 CTGitPath fullpath;
2312 fullpath.SetFromWin(g_Git.CombinePath(entry));
2313 delList.AddPath(fullpath);
2316 if (DWORD(CRegDWORD(L"Software\\TortoiseGit\\RevertWithRecycleBin", TRUE)))
2317 delList.DeleteAllFiles(true);
2319 CString revertToCommit = L"HEAD";
2320 if (m_amend)
2321 revertToCommit = L"HEAD~1";
2322 CString err;
2323 if (g_Git.Revert(revertToCommit, targetList, err))
2324 MessageBox(L"Revert failed:\n" + err, L"TortoiseGit", MB_ICONERROR);
2325 else
2327 bool updateStatusList = false;
2328 for (int i = 0 ; i < targetList.GetCount(); ++i)
2330 int nListboxEntries = GetItemCount();
2331 for (int nItem=0; nItem<nListboxEntries; ++nItem)
2333 auto path = GetListEntry(nItem);
2334 if (path->GetGitPathString()==targetList[i].GetGitPathString() && !path->IsDirectory())
2336 if(path->m_Action & CTGitPath::LOGACTIONS_ADDED)
2338 path->m_Action = CTGitPath::LOGACTIONS_UNVER;
2339 SetEntryCheck(path,nItem,false);
2340 updateStatusList = true;
2341 #if 0 // revert an added file and some entry will be cloned (part 1/2)
2342 SetItemGroup(nItem,1);
2343 this->m_StatusFileList.RemoveItem(*path);
2344 this->m_UnRevFileList.AddPath(*path);
2345 //this->m_IgnoreFileList.RemoveItem(*path);
2346 #endif
2348 else
2350 if (GetCheck(nItem))
2351 m_nSelected--;
2352 RemoveListEntry(nItem);
2354 break;
2356 else if (path->GetGitPathString()==targetList[i].GetGitPathString() && path->IsDirectory() && path->IsWCRoot())
2358 CString sCmd;
2359 sCmd.Format(L"/command:revert /path:\"%s\"", (LPCTSTR)path->GetGitPathString());
2360 CCommonAppUtils::RunTortoiseGitProc(sCmd);
2364 SetRedraw(TRUE);
2365 #if 0 // revert an added file and some entry will be cloned (part 2/2)
2366 Show(m_dwShow, 0, m_bShowFolders,updateStatusList,true);
2367 NotifyCheck();
2368 #else
2369 if (updateStatusList && GetLogicalParent() && GetLogicalParent()->GetSafeHwnd())
2370 GetLogicalParent()->SendMessage(GITSLNM_NEEDSREFRESH);
2371 #endif
2375 break;
2377 case IDGITLC_ASSUMEVALID:
2378 SetGitIndexFlagsForSelectedFiles(IDS_PROC_MARK_ASSUMEVALID, BST_CHECKED, BST_INDETERMINATE);
2379 break;
2380 case IDGITLC_SKIPWORKTREE:
2381 SetGitIndexFlagsForSelectedFiles(IDS_PROC_MARK_SKIPWORKTREE, BST_INDETERMINATE, BST_CHECKED);
2382 break;
2383 case IDGITLC_UNSETIGNORELOCALCHANGES:
2384 SetGitIndexFlagsForSelectedFiles(IDS_PROC_UNSET_IGNORELOCALCHANGES, BST_UNCHECKED, BST_UNCHECKED);
2385 break;
2386 case IDGITLC_COPYFULL:
2387 case IDGITLC_COPYRELPATHS:
2388 case IDGITLC_COPYFILENAMES:
2389 CopySelectedEntriesToClipboard(GITSLC_COLFILENAME, cmd);
2390 break;
2391 case IDGITLC_COPYEXT:
2392 CopySelectedEntriesToClipboard((DWORD)-1, 0);
2393 break;
2394 case IDGITLC_COPYCOL:
2395 CopySelectedEntriesToClipboard((DWORD)1 << selSubitem, 0);
2396 break;
2397 case IDGITLC_EXPORT:
2398 FilesExport();
2399 break;
2400 case IDGITLC_SAVEAS:
2401 FileSaveAs(filepath);
2402 break;
2404 case IDGITLC_REVERTTOREV:
2405 RevertSelectedItemToVersion();
2406 break;
2407 case IDGITLC_REVERTTOPARENT:
2408 RevertSelectedItemToVersion(true);
2409 break;
2410 #if 0
2411 case IDSVNLC_COMMIT:
2413 CTSVNPathList targetList;
2414 FillListOfSelectedItemPaths(targetList);
2415 CTSVNPath tempFile = CTempFiles::Instance().GetTempFilePath(false);
2416 VERIFY(targetList.WriteToFile(tempFile.GetWinPathString()));
2417 CString commandline = CPathUtils::GetAppDirectory();
2418 commandline += L"TortoiseGitProc.exe /command:commit /pathfile:\"";
2419 commandline += tempFile.GetWinPathString();
2420 commandline += L'"';
2421 commandline += L" /deletepathfile";
2422 CAppUtils::LaunchApplication(commandline, nullptr, false);
2424 break;
2425 case IDSVNLC_CREATEIGNORECS:
2426 CreateChangeList(SVNSLC_IGNORECHANGELIST);
2427 break;
2428 case IDSVNLC_CREATECS:
2430 CCreateChangelistDlg dlg;
2431 if (dlg.DoModal() == IDOK)
2432 CreateChangeList(dlg.m_sName);
2434 break;
2435 default:
2437 if (cmd < IDSVNLC_MOVETOCS)
2438 break;
2439 CTSVNPathList changelistItems;
2440 FillListOfSelectedItemPaths(changelistItems);
2442 // find the changelist name
2443 CString sChangelist;
2444 int cmdID = IDSVNLC_MOVETOCS;
2445 SetRedraw(FALSE);
2446 for (auto it = m_changelists.cbegin(); it != m_changelists.cend(); ++it)
2448 if ((it->first.Compare(SVNSLC_IGNORECHANGELIST))&&(entry->changelist.Compare(it->first)))
2450 if (cmd == cmdID)
2451 sChangelist = it->first;
2452 cmdID++;
2455 if (!sChangelist.IsEmpty())
2457 SVN git;
2458 if (git.AddToChangeList(changelistItems, sChangelist, git_depth_empty))
2460 // The changelists were moved, but we now need to run through the selected items again
2461 // and update their changelist
2462 POSITION pos = GetFirstSelectedItemPosition();
2463 int index;
2464 while ((index = GetNextSelectedItem(pos)) >= 0)
2466 FileEntry * e = GetListEntry(index);
2467 e->changelist = sChangelist;
2468 if (!e->IsFolder())
2470 if (m_changelists.find(e->changelist)!=m_changelists.end())
2471 SetItemGroup(index, m_changelists[e->changelist]);
2472 else
2473 SetItemGroup(index, 0);
2477 else
2478 MessageBox(git.GetLastErrorMessage(), L"TortoiseGit", MB_ICONERROR);
2480 SetRedraw(TRUE);
2482 break;
2483 #endif
2485 } // switch (cmd)
2486 m_bWaitCursor = false;
2487 GetStatisticsString();
2488 //int iItemCountAfterMenuCmd = GetItemCount();
2489 //if (iItemCountAfterMenuCmd != iItemCountBeforeMenuCmd)
2491 // CWnd* pParent = GetParent();
2492 // if (pParent && pParent->GetSafeHwnd())
2493 // {
2494 // pParent->SendMessage(SVNSLNM_ITEMCOUNTCHANGED);
2495 // }
2497 } // if (popup.CreatePopupMenu())
2498 } // if (selIndex >= 0)
2501 void CGitStatusListCtrl::SetGitIndexFlagsForSelectedFiles(UINT message, BOOL assumevalid, BOOL skipworktree)
2503 if (CMessageBox::Show(GetParentHWND(), message, IDS_APPNAME, MB_YESNO | MB_DEFBUTTON2 | MB_ICONQUESTION) != IDYES)
2504 return;
2506 CAutoReadLock locker(m_guard);
2508 CAutoRepository repository(g_Git.GetGitRepository());
2509 if (!repository)
2511 MessageBox(g_Git.GetLibGit2LastErr(), L"TortoiseGit", MB_ICONERROR);
2512 return;
2515 CAutoIndex gitindex;
2516 if (git_repository_index(gitindex.GetPointer(), repository))
2518 MessageBox(g_Git.GetLibGit2LastErr(), L"TortoiseGit", MB_ICONERROR);
2519 return;
2522 POSITION pos = GetFirstSelectedItemPosition();
2523 int index = -1;
2524 while ((index = GetNextSelectedItem(pos)) >= 0)
2526 auto path = GetListEntry(index);
2527 if (path == nullptr)
2528 continue;
2530 size_t idx;
2531 if (!git_index_find(&idx, gitindex, CUnicodeUtils::GetMulti(path->GetGitPathString(), CP_UTF8)))
2533 git_index_entry *e = const_cast<git_index_entry *>(git_index_get_byindex(gitindex, idx)); // HACK
2534 if (assumevalid == BST_UNCHECKED)
2535 e->flags &= ~GIT_IDXENTRY_VALID;
2536 else if (assumevalid == BST_CHECKED)
2537 e->flags |= GIT_IDXENTRY_VALID;
2538 if (skipworktree == BST_UNCHECKED)
2539 e->flags_extended &= ~GIT_IDXENTRY_SKIP_WORKTREE;
2540 else if (skipworktree == BST_CHECKED)
2541 e->flags_extended |= GIT_IDXENTRY_SKIP_WORKTREE;
2542 git_index_add(gitindex, e);
2544 else
2545 MessageBox(g_Git.GetLibGit2LastErr(), L"TortoiseGit", MB_ICONERROR);
2548 if (git_index_write(gitindex))
2550 MessageBox(g_Git.GetLibGit2LastErr(), L"TortoiseGit", MB_ICONERROR);
2551 return;
2554 if (nullptr != GetLogicalParent() && nullptr != GetLogicalParent()->GetSafeHwnd())
2555 GetLogicalParent()->SendMessage(GITSLNM_NEEDSREFRESH);
2557 SetRedraw(TRUE);
2560 void CGitStatusListCtrl::OnContextMenu(CWnd* pWnd, CPoint point)
2562 __super::OnContextMenu(pWnd, point);
2563 if (pWnd == this)
2564 OnContextMenuList(pWnd, point);
2567 void CGitStatusListCtrl::OnNMDblclk(NMHDR *pNMHDR, LRESULT *pResult)
2569 LPNMLISTVIEW pNMLV = reinterpret_cast<LPNMLISTVIEW>(pNMHDR);
2570 *pResult = 0;
2572 CAutoReadWeakLock readLock(m_guard);
2573 if (!readLock.IsAcquired())
2574 return;
2576 if (pNMLV->iItem < 0)
2577 return;
2579 auto file = GetListEntry(pNMLV->iItem);
2581 if (file->m_Action & (CTGitPath::LOGACTIONS_UNVER | CTGitPath::LOGACTIONS_IGNORE)) {
2582 StartDiffWC(pNMLV->iItem);
2583 return;
2585 if( file->m_Action&CTGitPath::LOGACTIONS_UNMERGED )
2587 if (CAppUtils::ConflictEdit(GetParentHWND(), *file, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000), m_bIsRevertTheirMy, GetLogicalParent() ? GetLogicalParent()->GetSafeHwnd() : nullptr))
2589 CString conflictedFile = g_Git.CombinePath(file);
2590 if (!PathFileExists(conflictedFile) && GetLogicalParent() && GetLogicalParent()->GetSafeHwnd())
2592 GetLogicalParent()->SendMessage(GITSLNM_NEEDSREFRESH);
2593 return;
2595 StoreScrollPos();
2596 Show(m_dwShow, 0, m_bShowFolders, 0, true);
2599 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))
2600 return;
2601 else
2603 if (!m_Rev1.IsEmpty() && !m_Rev2.IsEmpty())
2604 StartDiffTwo(pNMLV->iItem);
2605 else
2606 StartDiff(pNMLV->iItem);
2609 void CGitStatusListCtrl::StartDiffTwo(int fileindex)
2611 if(fileindex<0)
2612 return;
2614 CAutoReadLock locker(m_guard);
2615 auto ptr = GetListEntry(fileindex);
2616 if (!ptr)
2617 return;
2618 CTGitPath file1 = *ptr;
2620 if (file1.m_Action & CTGitPath::LOGACTIONS_ADDED)
2621 CGitDiff::DiffNull(GetParentHWND(), &file1, m_Rev1, true, 0, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000));
2622 else if (file1.m_Action & CTGitPath::LOGACTIONS_DELETED)
2623 CGitDiff::DiffNull(GetParentHWND(), &file1, m_Rev2, false, 0, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000));
2624 else
2625 CGitDiff::Diff(GetParentHWND(), &file1, &file1, m_Rev1, m_Rev2, false, false, 0, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000));
2628 void CGitStatusListCtrl::StartDiffWC(int fileindex)
2630 if(fileindex<0)
2631 return;
2633 CAutoReadLock locker(m_guard);
2634 auto ptr = GetListEntry(fileindex);
2635 if (!ptr)
2636 return;
2637 CTGitPath file1 = *ptr;
2638 file1.m_Action = 0; // reset action, so that diff is not started as added/deleted file; see issue #1757
2640 CGitDiff::Diff(GetParentHWND(), &file1, &file1, GIT_REV_ZERO, m_CurrentVersion.ToString(), false, false, 0, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000));
2643 void CGitStatusListCtrl::StartDiff(int fileindex)
2645 if(fileindex<0)
2646 return;
2648 CAutoReadLock locker(m_guard);
2649 auto ptr = GetListEntry(fileindex);
2650 if (!ptr)
2651 return;
2652 CTGitPath file1 = *ptr;
2653 CTGitPath file2;
2654 if(file1.m_Action & (CTGitPath::LOGACTIONS_REPLACED|CTGitPath::LOGACTIONS_COPY))
2655 file2.SetFromGit(file1.GetGitOldPathString());
2656 else
2657 file2=file1;
2659 if (m_CurrentVersion.IsEmpty())
2661 CString fromwhere;
2662 if(m_amend && (file1.m_Action & CTGitPath::LOGACTIONS_ADDED) == 0)
2663 fromwhere = L"~1";
2664 if( g_Git.IsInitRepos())
2665 CGitDiff::DiffNull(GetParentHWND(), GetListEntry(fileindex),
2666 GIT_REV_ZERO, true, 0, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000));
2667 else if( file1.m_Action&CTGitPath::LOGACTIONS_ADDED )
2668 CGitDiff::DiffNull(GetParentHWND(), GetListEntry(fileindex),
2669 m_CurrentVersion.ToString() + fromwhere, true, 0, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000));
2670 else if( file1.m_Action&CTGitPath::LOGACTIONS_DELETED )
2671 CGitDiff::DiffNull(GetParentHWND(), GetListEntry(fileindex),
2672 GitRev::GetHead() + fromwhere, false, 0, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000));
2673 else
2674 CGitDiff::Diff(GetParentHWND(), &file1,&file2,
2675 CString(GIT_REV_ZERO),
2676 GitRev::GetHead() + fromwhere, false, false, 0, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000));
2678 else
2680 CGitHash hash;
2681 CString fromwhere = m_CurrentVersion.ToString() + L"~1";
2682 if(m_amend)
2683 fromwhere = m_CurrentVersion.ToString() + L"~2";
2684 bool revfail = !!g_Git.GetHash(hash, fromwhere);
2685 if (revfail || (file1.m_Action & file1.LOGACTIONS_ADDED))
2686 CGitDiff::DiffNull(GetParentHWND(), &file1, m_CurrentVersion.ToString(), true, 0, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000));
2687 else if (file1.m_Action & file1.LOGACTIONS_DELETED)
2689 if (file1.m_ParentNo > 0)
2690 fromwhere.Format(L"%s^%d", (LPCTSTR)m_CurrentVersion.ToString(), file1.m_ParentNo + 1);
2692 CGitDiff::DiffNull(GetParentHWND(), &file1, fromwhere, false, 0, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000));
2694 else
2696 if( file1.m_ParentNo & MERGE_MASK)
2698 CTGitPath base, theirs, mine, merge;
2700 CString temppath;
2701 GetTempPath(temppath);
2702 temppath.TrimRight(L'\\');
2704 mine.SetFromGit(temppath + L'\\' + file1.GetFileOrDirectoryName() + L".LOCAL" + file1.GetFileExtension());
2705 theirs.SetFromGit(temppath + L'\\' + file1.GetFileOrDirectoryName() + L".REMOTE" + file1.GetFileExtension());
2706 base.SetFromGit(temppath + L'\\' + file1.GetFileOrDirectoryName() + L".BASE" + file1.GetFileExtension());
2708 CFile tempfile;
2709 //create a empty file, incase stage is not three
2710 tempfile.Open(mine.GetWinPathString(),CFile::modeCreate|CFile::modeReadWrite);
2711 tempfile.Close();
2712 tempfile.Open(theirs.GetWinPathString(),CFile::modeCreate|CFile::modeReadWrite);
2713 tempfile.Close();
2714 tempfile.Open(base.GetWinPathString(),CFile::modeCreate|CFile::modeReadWrite);
2715 tempfile.Close();
2717 merge.SetFromGit(temppath + L'\\' + file1.GetFileOrDirectoryName() + L".Merged" + file1.GetFileExtension());
2719 int parent1=-1, parent2 =-1;
2720 for (size_t i = 0; i < m_arStatusArray.size(); ++i)
2722 if(m_arStatusArray[i]->GetGitPathString() == file1.GetGitPathString())
2724 if(m_arStatusArray[i]->m_ParentNo & MERGE_MASK)
2727 else
2729 if(parent1<0)
2730 parent1 = m_arStatusArray[i]->m_ParentNo & PARENT_MASK;
2731 else if (parent2 <0)
2732 parent2 = m_arStatusArray[i]->m_ParentNo & PARENT_MASK;
2737 if (g_Git.GetOneFile(m_CurrentVersion.ToString(), file1, merge.GetWinPathString()))
2738 CMessageBox::Show(GetParentHWND(), IDS_STATUSLIST_FAILEDGETMERGEFILE, IDS_APPNAME, MB_OK | MB_ICONERROR);
2740 if(parent1>=0)
2742 CString str;
2743 str.Format(L"%s^%d", (LPCTSTR)m_CurrentVersion.ToString(), parent1 + 1);
2745 if (g_Git.GetOneFile(str, file1, mine.GetWinPathString()))
2746 CMessageBox::Show(GetParentHWND(), IDS_STATUSLIST_FAILEDGETMERGEFILE, IDS_APPNAME, MB_OK | MB_ICONERROR);
2749 if(parent2>=0)
2751 CString str;
2752 str.Format(L"%s^%d", (LPCTSTR)m_CurrentVersion.ToString(), parent2 + 1);
2754 if (g_Git.GetOneFile(str, file1, theirs.GetWinPathString()))
2755 CMessageBox::Show(GetParentHWND(), IDS_STATUSLIST_FAILEDGETMERGEFILE, IDS_APPNAME, MB_OK | MB_ICONERROR);
2758 if(parent1>=0 && parent2>=0)
2760 CString cmd, output;
2761 cmd.Format(L"git.exe merge-base %s^%d %s^%d", (LPCTSTR)m_CurrentVersion.ToString(), parent1 + 1,
2762 (LPCTSTR)m_CurrentVersion.ToString(), parent2 + 1);
2764 if (!g_Git.Run(cmd, &output, nullptr, CP_UTF8))
2766 if (g_Git.GetOneFile(output.Left(2 * GIT_HASH_SIZE), file1, base.GetWinPathString()))
2767 CMessageBox::Show(GetParentHWND(), IDS_STATUSLIST_FAILEDGETBASEFILE, IDS_APPNAME, MB_OK | MB_ICONERROR);
2770 CAppUtils::StartExtMerge(!!(GetAsyncKeyState(VK_SHIFT) & 0x8000), base, theirs, mine, merge, L"BASE", L"REMOTE", L"LOCAL");
2772 else
2774 CString str;
2775 if( (file1.m_ParentNo&PARENT_MASK) == 0)
2776 str = L"~1";
2777 else
2778 str.Format(L"^%d", (file1.m_ParentNo & PARENT_MASK) + 1);
2779 CGitDiff::Diff(GetParentHWND(), &file1,&file2,
2780 m_CurrentVersion.ToString(),
2781 m_CurrentVersion.ToString() + str, false, false, 0, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000));
2787 CString CGitStatusListCtrl::GetStatisticsString(bool simple)
2789 CString sNormal = CString(MAKEINTRESOURCE(IDS_STATUSNORMAL));
2790 CString sAdded = CString(MAKEINTRESOURCE(IDS_STATUSADDED));
2791 CString sDeleted = CString(MAKEINTRESOURCE(IDS_STATUSDELETED));
2792 CString sModified = CString(MAKEINTRESOURCE(IDS_STATUSMODIFIED));
2793 CString sConflicted = CString(MAKEINTRESOURCE(IDS_STATUSCONFLICTED));
2794 CString sUnversioned = CString(MAKEINTRESOURCE(IDS_STATUSUNVERSIONED));
2795 CString sRenamed = CString(MAKEINTRESOURCE(IDS_STATUSREPLACED));
2796 CString sToolTip;
2797 if(simple)
2799 sToolTip.Format(IDS_STATUSLIST_STATUSLINE1,
2800 this->m_nLineAdded,this->m_nLineDeleted,
2801 (LPCTSTR)sModified, m_nModified,
2802 (LPCTSTR)sAdded, m_nAdded,
2803 (LPCTSTR)sDeleted, m_nDeleted,
2804 (LPCTSTR)sRenamed, m_nRenamed
2807 else
2809 sToolTip.Format(IDS_STATUSLIST_STATUSLINE2,
2810 this->m_nLineAdded,this->m_nLineDeleted,
2811 (LPCTSTR)sNormal, m_nNormal,
2812 (LPCTSTR)sUnversioned, m_nUnversioned,
2813 (LPCTSTR)sModified, m_nModified,
2814 (LPCTSTR)sAdded, m_nAdded,
2815 (LPCTSTR)sDeleted, m_nDeleted,
2816 (LPCTSTR)sConflicted, m_nConflicted
2819 CString sStats;
2820 sStats.FormatMessage(IDS_COMMITDLG_STATISTICSFORMAT, m_nSelected, GetItemCount());
2821 if (m_pStatLabel)
2822 m_pStatLabel->SetWindowText(sStats);
2824 if (m_pSelectButton)
2826 if (m_nSelected == 0)
2827 m_pSelectButton->SetCheck(BST_UNCHECKED);
2828 else if (m_nSelected != GetItemCount())
2829 m_pSelectButton->SetCheck(BST_INDETERMINATE);
2830 else
2831 m_pSelectButton->SetCheck(BST_CHECKED);
2834 if (m_pConfirmButton)
2835 m_pConfirmButton->EnableWindow(m_nSelected>0);
2837 return sToolTip;
2840 CString CGitStatusListCtrl::GetCommonDirectory(bool bStrict)
2842 CAutoReadLock locker(m_guard);
2843 if (!bStrict)
2845 // not strict means that the selected folder has priority
2846 if (!m_StatusFileList.GetCommonDirectory().IsEmpty())
2847 return m_StatusFileList.GetCommonDirectory().GetWinPath();
2850 CTGitPathList list;
2851 int nListItems = GetItemCount();
2852 for (int i=0; i<nListItems; ++i)
2854 auto* entry = GetListEntry(i);
2855 if (entry->IsEmpty())
2856 continue;
2857 list.AddPath(*entry);
2859 return list.GetCommonRoot().GetWinPath();
2862 void CGitStatusListCtrl::SelectAll(bool bSelect, bool /*bIncludeNoCommits*/)
2864 CWaitCursor waitCursor;
2865 // block here so the LVN_ITEMCHANGED messages
2866 // get ignored
2867 ScopedInDecrement blocker(m_nBlockItemChangeHandler);
2870 CAutoWriteLock locker(m_guard);
2871 SetRedraw(FALSE);
2873 int nListItems = GetItemCount();
2874 if (bSelect)
2875 m_nSelected = nListItems;
2876 else
2877 m_nSelected = 0;
2879 for (int i=0; i<nListItems; ++i)
2881 auto path = GetListEntry(i);
2882 if (!path)
2883 continue;
2884 //if ((bIncludeNoCommits)||(entry->GetChangeList().Compare(SVNSLC_IGNORECHANGELIST)))
2885 SetEntryCheck(path,i,bSelect);
2888 SetRedraw(TRUE);
2889 GetStatisticsString();
2890 NotifyCheck();
2893 void CGitStatusListCtrl::Check(DWORD dwCheck, bool check)
2895 CWaitCursor waitCursor;
2896 // block here so the LVN_ITEMCHANGED messages
2897 // get ignored
2899 CAutoWriteLock locker(m_guard);
2900 SetRedraw(FALSE);
2901 ScopedInDecrement blocker(m_nBlockItemChangeHandler);
2903 int nListItems = GetItemCount();
2905 for (int i = 0; i < nListItems; ++i)
2907 auto entry = GetListEntry(i);
2908 if (!entry)
2909 continue;
2911 DWORD showFlags = entry->m_Action;
2912 if (entry->IsDirectory())
2913 showFlags |= GITSLC_SHOWSUBMODULES;
2914 else
2915 showFlags |= GITSLC_SHOWFILES;
2917 if (check && (showFlags & dwCheck) && !GetCheck(i) && !(entry->IsDirectory() && m_bDoNotAutoselectSubmodules && !(dwCheck & GITSLC_SHOWSUBMODULES)))
2919 SetEntryCheck(entry, i, true);
2920 m_nSelected++;
2922 else if (!check && (showFlags & dwCheck) && GetCheck(i))
2924 SetEntryCheck(entry, i, false);
2925 m_nSelected--;
2929 SetRedraw(TRUE);
2930 GetStatisticsString();
2931 NotifyCheck();
2934 void CGitStatusListCtrl::OnLvnGetInfoTip(NMHDR *pNMHDR, LRESULT *pResult)
2936 LPNMLVGETINFOTIP pGetInfoTip = reinterpret_cast<LPNMLVGETINFOTIP>(pNMHDR);
2937 *pResult = 0;
2938 if (CRegDWORD(L"Software\\TortoiseGit\\ShowListFullPathTooltip", TRUE) != TRUE)
2939 return;
2941 CAutoReadWeakLock readLock(m_guard);
2942 if (!readLock.IsAcquired())
2943 return;
2945 auto entry = GetListEntry(pGetInfoTip->iItem);
2947 if (entry)
2948 if (pGetInfoTip->cchTextMax > entry->GetGitPathString().GetLength() + g_Git.m_CurrentDir.GetLength())
2950 CString str = g_Git.CombinePath(entry->GetWinPathString());
2951 wcsncpy_s(pGetInfoTip->pszText, pGetInfoTip->cchTextMax, str.GetBuffer(), pGetInfoTip->cchTextMax - 1);
2955 BOOL CGitStatusListCtrl::OnNMCustomdraw(NMHDR* pNMHDR, LRESULT* pResult)
2957 NMLVCUSTOMDRAW* pLVCD = reinterpret_cast<NMLVCUSTOMDRAW*>( pNMHDR );
2959 // Take the default processing unless we set this to something else below.
2960 *pResult = CDRF_DODEFAULT;
2962 // First thing - check the draw stage. If it's the control's prepaint
2963 // stage, then tell Windows we want messages for every item.
2965 switch (pLVCD->nmcd.dwDrawStage)
2967 case CDDS_PREPAINT:
2968 *pResult = CDRF_NOTIFYITEMDRAW;
2969 break;
2970 case CDDS_ITEMPREPAINT:
2972 // This is the prepaint stage for an item. Here's where we set the
2973 // item's text color.
2975 // Tell Windows we want a callback (be)for(e) painting the subitems.
2976 *pResult = CDRF_NOTIFYSUBITEMDRAW;
2978 CAutoReadWeakLock readLock(m_guard, 0);
2979 if (!readLock.IsAcquired())
2980 return TRUE;
2982 COLORREF crText = GetSysColor(COLOR_WINDOWTEXT);
2984 if (m_arStatusArray.size() > (DWORD_PTR)pLVCD->nmcd.dwItemSpec)
2986 auto entry = GetListEntry((int)pLVCD->nmcd.dwItemSpec);
2987 if (!entry)
2988 return TRUE;
2990 // coloring
2991 // ========
2992 // black : unversioned, normal
2993 // purple : added
2994 // blue : modified
2995 // brown : missing, deleted, replaced
2996 // green : merged (or potential merges)
2997 // red : conflicts or sure conflicts
2998 if(entry->m_Action & CTGitPath::LOGACTIONS_GRAY)
2999 crText = RGB(128,128,128);
3000 else if(entry->m_Action & CTGitPath::LOGACTIONS_UNMERGED)
3001 crText = m_Colors.GetColor(CColors::Conflict);
3002 else if(entry->m_Action & (CTGitPath::LOGACTIONS_MODIFIED))
3003 crText = m_Colors.GetColor(CColors::Modified);
3004 else if(entry->m_Action & (CTGitPath::LOGACTIONS_ADDED|CTGitPath::LOGACTIONS_COPY))
3005 crText = m_Colors.GetColor(CColors::Added);
3006 else if(entry->m_Action & CTGitPath::LOGACTIONS_DELETED)
3007 crText = m_Colors.GetColor(CColors::Deleted);
3008 else if(entry->m_Action & CTGitPath::LOGACTIONS_REPLACED)
3009 crText = m_Colors.GetColor(CColors::Renamed);
3010 else if(entry->m_Action & CTGitPath::LOGACTIONS_MERGED)
3011 crText = m_Colors.GetColor(CColors::Merged);
3012 else
3013 crText = GetSysColor(COLOR_WINDOWTEXT);
3014 // Store the color back in the NMLVCUSTOMDRAW struct.
3015 pLVCD->clrText = crText;
3018 break;
3020 case CDDS_ITEMPREPAINT | CDDS_ITEM | CDDS_SUBITEM:
3021 return FALSE; // allow parent to handle this
3022 break;
3024 return TRUE;
3027 void CGitStatusListCtrl::OnLvnGetdispinfo(NMHDR* pNMHDR, LRESULT* pResult)
3029 auto pDispInfo = reinterpret_cast<NMLVDISPINFO*>(pNMHDR);
3030 *pResult = 0;
3032 // Create a pointer to the item
3033 LV_ITEM* pItem = &(pDispInfo)->item;
3035 CAutoReadWeakLock readLock(m_guard, 0);
3036 if (readLock.IsAcquired())
3038 if (pItem->mask & LVIF_TEXT)
3040 CString text = GetCellText(pItem->iItem, pItem->iSubItem);
3041 lstrcpyn(pItem->pszText, text, pItem->cchTextMax - 1);
3044 else
3045 pItem->mask = 0;
3048 BOOL CGitStatusListCtrl::OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message)
3050 if (pWnd != this)
3051 return CListCtrl::OnSetCursor(pWnd, nHitTest, message);
3052 if (!m_bWaitCursor && !m_bBusy)
3054 HCURSOR hCur = LoadCursor(nullptr, IDC_ARROW);
3055 SetCursor(hCur);
3056 return CListCtrl::OnSetCursor(pWnd, nHitTest, message);
3058 HCURSOR hCur = LoadCursor(nullptr, IDC_WAIT);
3059 SetCursor(hCur);
3060 return TRUE;
3063 void CGitStatusListCtrl::RemoveListEntry(int index)
3065 CAutoWriteLock locker(m_guard);
3066 DeleteItem(index);
3068 m_arStatusArray.erase(m_arStatusArray.cbegin() + index);
3070 #if 0
3071 delete m_arStatusArray[m_arListArray[index]];
3072 m_arStatusArray.erase(m_arStatusArray.begin()+m_arListArray[index]);
3073 m_arListArray.erase(m_arListArray.begin()+index);
3074 for (int i=index; i< (int)m_arListArray.size(); ++i)
3076 m_arListArray[i]--;
3078 #endif
3081 ///< Set a checkbox on an entry in the listbox
3082 // NEVER, EVER call SetCheck directly, because you'll end-up with the checkboxes and the 'checked' flag getting out of sync
3083 void CGitStatusListCtrl::SetEntryCheck(CTGitPath* pEntry, int listboxIndex, bool bCheck)
3085 CAutoWriteLock locker(m_guard);
3086 pEntry->m_Checked = bCheck;
3087 m_mapFilenameToChecked[pEntry->GetGitPathString()] = bCheck;
3088 SetCheck(listboxIndex, bCheck);
3091 void CGitStatusListCtrl::ResetChecked(const CTGitPath& entry)
3093 CAutoWriteLock locker(m_guard);
3094 CTGitPath adjustedEntry;
3095 if (g_Git.m_CurrentDir[g_Git.m_CurrentDir.GetLength() - 1] == L'\\')
3096 adjustedEntry.SetFromWin(entry.GetWinPathString().Right(entry.GetWinPathString().GetLength() - g_Git.m_CurrentDir.GetLength()));
3097 else
3098 adjustedEntry.SetFromWin(entry.GetWinPathString().Right(entry.GetWinPathString().GetLength() - g_Git.m_CurrentDir.GetLength() - 1));
3099 if (entry.IsDirectory())
3101 STRING_VECTOR toDelete;
3102 for (auto it = m_mapFilenameToChecked.begin(); it != m_mapFilenameToChecked.end(); ++it)
3104 if (adjustedEntry.IsAncestorOf(it->first))
3105 toDelete.emplace_back(it->first);
3107 for (const auto& file : toDelete)
3108 m_mapFilenameToChecked.erase(file);
3109 return;
3111 m_mapFilenameToChecked.erase(adjustedEntry.GetGitPathString());
3114 #if 0
3115 void CGitStatusListCtrl::SetCheckOnAllDescendentsOf(const FileEntry* parentEntry, bool bCheck)
3117 CAutoWriteLock locker(m_guard);
3118 int nListItems = GetItemCount();
3119 for (int j=0; j< nListItems ; ++j)
3121 FileEntry * childEntry = GetListEntry(j);
3122 ASSERT(childEntry);
3123 if (!childEntry || childEntry == parentEntry)
3124 continue;
3125 if (childEntry->checked != bCheck)
3127 if (parentEntry->path.IsAncestorOf(childEntry->path))
3129 SetEntryCheck(childEntry,j,bCheck);
3130 if(bCheck)
3131 m_nSelected++;
3132 else
3133 m_nSelected--;
3138 #endif
3140 void CGitStatusListCtrl::WriteCheckedNamesToPathList(CTGitPathList& pathList)
3142 pathList.Clear();
3143 CAutoReadLock locker(m_guard);
3144 int nListItems = GetItemCount();
3145 for (int i = 0; i< nListItems; ++i)
3147 auto entry = GetListEntry(i);
3148 if (entry->m_Checked)
3149 pathList.AddPath(*entry);
3151 pathList.SortByPathname();
3155 /// Build a path list of all the selected items in the list (NOTE - SELECTED, not CHECKED)
3156 void CGitStatusListCtrl::FillListOfSelectedItemPaths(CTGitPathList& pathList, bool /*bNoIgnored*/)
3158 pathList.Clear();
3160 CAutoReadLock locker(m_guard);
3161 POSITION pos = GetFirstSelectedItemPosition();
3162 int index;
3163 while ((index = GetNextSelectedItem(pos)) >= 0)
3165 auto entry = GetListEntry(index);
3166 //if ((bNoIgnored)&&(entry->status == git_wc_status_ignored))
3167 // continue;
3168 pathList.AddPath(*entry);
3172 UINT CGitStatusListCtrl::OnGetDlgCode()
3174 // we want to process the return key and not have that one
3175 // routed to the default pushbutton
3176 return CListCtrl::OnGetDlgCode() | DLGC_WANTALLKEYS;
3179 void CGitStatusListCtrl::OnNMReturn(NMHDR * /*pNMHDR*/, LRESULT *pResult)
3181 *pResult = 0;
3182 CAutoReadWeakLock readLock(m_guard);
3183 if (!readLock.IsAcquired())
3184 return;
3185 if (!CheckMultipleDiffs())
3186 return;
3187 bool needsRefresh = false;
3188 bool resolvedTreeConfict = false;
3189 POSITION pos = GetFirstSelectedItemPosition();
3190 while ( pos )
3192 int index = GetNextSelectedItem(pos);
3193 if (index < 0)
3194 return;
3195 auto file = GetListEntry(index);
3196 if (file == nullptr)
3197 return;
3198 if (file->m_Action & (CTGitPath::LOGACTIONS_UNVER | CTGitPath::LOGACTIONS_IGNORE))
3199 StartDiffWC(index);
3200 else if ((file->m_Action & CTGitPath::LOGACTIONS_UNMERGED))
3202 if (CAppUtils::ConflictEdit(GetParentHWND(), *file, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000), m_bIsRevertTheirMy, GetLogicalParent() ? GetLogicalParent()->GetSafeHwnd() : nullptr))
3204 CString conflictedFile = g_Git.CombinePath(file);
3205 needsRefresh = needsRefresh || !PathFileExists(conflictedFile);
3206 resolvedTreeConfict = resolvedTreeConfict || (file->m_Action & CTGitPath::LOGACTIONS_UNMERGED) == 0;
3209 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))
3210 continue;
3211 else
3213 if (!m_Rev1.IsEmpty() && !m_Rev2.IsEmpty())
3214 StartDiffTwo(index);
3215 else
3216 StartDiff(index);
3219 if (needsRefresh && GetLogicalParent() && GetLogicalParent()->GetSafeHwnd())
3220 GetLogicalParent()->SendMessage(GITSLNM_NEEDSREFRESH);
3221 else if (resolvedTreeConfict)
3223 StoreScrollPos();
3224 Show(m_dwShow, 0, m_bShowFolders, 0, true);
3228 void CGitStatusListCtrl::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags)
3230 // Since we catch all keystrokes (to have the enter key processed here instead
3231 // of routed to the default pushbutton) we have to make sure that other
3232 // keys like Tab and Esc still do what they're supposed to do
3233 // Tab = change focus to next/previous control
3234 // Esc = quit the dialog
3235 switch (nChar)
3237 case (VK_TAB):
3239 ::PostMessage(GetLogicalParent()->GetSafeHwnd(), WM_NEXTDLGCTL, GetKeyState(VK_SHIFT)&0x8000, 0);
3240 return;
3242 break;
3243 case (VK_ESCAPE):
3245 ::SendMessage(GetLogicalParent()->GetSafeHwnd(), WM_CLOSE, 0, 0);
3247 break;
3250 CListCtrl::OnKeyDown(nChar, nRepCnt, nFlags);
3253 void CGitStatusListCtrl::PreSubclassWindow()
3255 __super::PreSubclassWindow();
3256 EnableToolTips(TRUE);
3257 SetWindowTheme(GetSafeHwnd(), L"Explorer", nullptr);
3260 void CGitStatusListCtrl::OnPaint()
3262 LRESULT defres = Default();
3263 if ((m_bBusy) || (m_bEmpty))
3265 CString str;
3266 if (m_bBusy)
3268 if (m_sBusy.IsEmpty())
3269 str.LoadString(IDS_STATUSLIST_BUSYMSG);
3270 else
3271 str = m_sBusy;
3273 else
3275 if (m_sEmpty.IsEmpty())
3276 str.LoadString(IDS_STATUSLIST_EMPTYMSG);
3277 else
3278 str = m_sEmpty;
3280 COLORREF clrText = ::GetSysColor(COLOR_WINDOWTEXT);
3281 COLORREF clrTextBk;
3282 if (IsWindowEnabled())
3283 clrTextBk = ::GetSysColor(COLOR_WINDOW);
3284 else
3285 clrTextBk = ::GetSysColor(COLOR_3DFACE);
3287 CRect rc;
3288 GetClientRect(&rc);
3289 CHeaderCtrl* pHC = GetHeaderCtrl();
3290 if (pHC)
3292 CRect rcH;
3293 pHC->GetItemRect(0, &rcH);
3294 rc.top += rcH.bottom;
3296 CDC* pDC = GetDC();
3298 CMyMemDC memDC(pDC, &rc);
3300 memDC.SetTextColor(clrText);
3301 memDC.SetBkColor(clrTextBk);
3302 memDC.BitBlt(rc.left, rc.top, rc.Width(), rc.Height(), pDC, rc.left, rc.top, SRCCOPY);
3303 rc.top += 10;
3304 CGdiObject* oldfont = memDC.SelectObject(CGdiObject::FromHandle(m_uiFont));
3305 memDC.DrawText(str, rc, DT_CENTER | DT_VCENTER |
3306 DT_WORDBREAK | DT_NOPREFIX | DT_NOCLIP);
3307 memDC.SelectObject(oldfont);
3309 ReleaseDC(pDC);
3311 if (defres)
3313 // the Default() call did not process the WM_PAINT message!
3314 // Validate the update region ourselves to avoid
3315 // an endless loop repainting
3316 CRect rc;
3317 GetUpdateRect(&rc, FALSE);
3318 if (!rc.IsRectEmpty())
3319 ValidateRect(rc);
3323 void CGitStatusListCtrl::OnBeginDrag(NMHDR* pNMHDR, LRESULT* pResult)
3325 LPNMLISTVIEW pNMLV = reinterpret_cast<LPNMLISTVIEW>(pNMHDR);
3327 CAutoReadLock locker(m_guard);
3329 CTGitPathList pathList;
3330 FillListOfSelectedItemPaths(pathList);
3331 if (pathList.IsEmpty())
3332 return;
3334 auto pdsrc = std::make_unique<CIDropSource>();
3335 if (!pdsrc)
3336 return;
3337 pdsrc->AddRef();
3339 GitDataObject* pdobj = new GitDataObject(pathList, m_Rev2.IsEmpty() ? m_CurrentVersion : m_Rev2);
3340 if (!pdobj)
3341 return;
3342 pdobj->AddRef();
3344 CDragSourceHelper dragsrchelper;
3346 SetRedraw(false);
3347 dragsrchelper.InitializeFromWindow(m_hWnd, pNMLV->ptAction, pdobj);
3348 SetRedraw(true);
3349 //dragsrchelper.InitializeFromBitmap()
3350 pdsrc->m_pIDataObj = pdobj;
3351 pdsrc->m_pIDataObj->AddRef();
3353 // Initiate the Drag & Drop
3354 DWORD dwEffect;
3355 m_bOwnDrag = true;
3356 ::DoDragDrop(pdobj, pdsrc.get(), DROPEFFECT_MOVE | DROPEFFECT_COPY, &dwEffect);
3357 m_bOwnDrag = false;
3358 pdsrc->Release();
3359 pdsrc.release();
3360 pdobj->Release();
3362 *pResult = 0;
3365 bool CGitStatusListCtrl::EnableFileDrop()
3367 m_bFileDropsEnabled = true;
3368 return true;
3371 bool CGitStatusListCtrl::HasPath(const CTGitPath& path)
3373 CAutoReadLock locker(m_guard);
3374 CTGitPath adjustedEntry;
3375 if (g_Git.m_CurrentDir[g_Git.m_CurrentDir.GetLength() - 1] == L'\\')
3376 adjustedEntry.SetFromWin(path.GetWinPathString().Right(path.GetWinPathString().GetLength() - g_Git.m_CurrentDir.GetLength()));
3377 else
3378 adjustedEntry.SetFromWin(path.GetWinPathString().Right(path.GetWinPathString().GetLength() - g_Git.m_CurrentDir.GetLength() - 1));
3379 for (size_t i=0; i < m_arStatusArray.size(); ++i)
3381 if (m_arStatusArray[i]->IsEquivalentTo(adjustedEntry))
3382 return true;
3385 return false;
3388 BOOL CGitStatusListCtrl::PreTranslateMessage(MSG* pMsg)
3390 if (pMsg->message == WM_KEYDOWN)
3392 switch (pMsg->wParam)
3394 case 'A':
3396 if (GetAsyncKeyState(VK_CONTROL)&0x8000)
3398 // select all entries
3399 for (int i=0; i<GetItemCount(); ++i)
3400 SetItemState(i, LVIS_SELECTED, LVIS_SELECTED);
3401 return TRUE;
3404 break;
3405 case 'C':
3406 case VK_INSERT:
3408 if (GetAsyncKeyState(VK_CONTROL)&0x8000)
3410 // copy all selected paths to the clipboard
3411 if (GetAsyncKeyState(VK_SHIFT)&0x8000)
3412 CopySelectedEntriesToClipboard(GITSLC_COLFILENAME | GITSLC_COLSTATUS, IDGITLC_COPYRELPATHS);
3413 else
3414 CopySelectedEntriesToClipboard(GITSLC_COLFILENAME, IDGITLC_COPYRELPATHS);
3415 return TRUE;
3418 break;
3419 case VK_DELETE:
3421 if ((GetSelectedCount() > 0) && (m_dwContextMenus & GITSLC_POPDELETE))
3423 CAutoReadLock locker(m_guard);
3424 auto filepath = GetListEntry(GetSelectionMark());
3425 if (filepath != nullptr && (filepath->m_Action & (CTGitPath::LOGACTIONS_UNVER | CTGitPath::LOGACTIONS_IGNORE)))
3426 DeleteSelectedFiles();
3429 break;
3433 return __super::PreTranslateMessage(pMsg);
3436 bool CGitStatusListCtrl::CopySelectedEntriesToClipboard(DWORD dwCols, int cmd)
3438 if (GetSelectedCount() == 0)
3439 return false;
3441 CString sClipboard;
3443 bool bMultipleColumnSelected = ((dwCols & dwCols - 1) != 0); // multiple columns are selected (clear least signifient bit and check for zero)
3445 #define ADDTOCLIPBOARDSTRING(x) sClipboard += (sClipboard.IsEmpty() || (sClipboard.Right(1)==L"\n")) ? (x) : ('\t' + x)
3446 #define ADDNEWLINETOCLIPBOARDSTRING() sClipboard += (sClipboard.IsEmpty()) ? L"" : L"\r\n"
3448 // first add the column titles as the first line
3449 DWORD selection = 0;
3450 int count = m_ColumnManager.GetColumnCount();
3451 for (int column = 0; column < count; ++column)
3453 if ((dwCols == -1 && m_ColumnManager.IsVisible(column)) || (column < GITSLC_NUMCOLUMNS && (dwCols & (1 << column))))
3455 if (bMultipleColumnSelected)
3456 ADDTOCLIPBOARDSTRING(m_ColumnManager.GetName(column));
3458 selection |= 1 << column;
3462 if (bMultipleColumnSelected)
3463 ADDNEWLINETOCLIPBOARDSTRING();
3465 // maybe clear first line when only one column is selected (btw by select not by dwCols) is simpler(not faster) way
3466 // but why no title on single column output ?
3467 // if (selection & selection-1) == 0 ) sClipboard = "";
3469 CAutoReadLock locker(m_guard);
3471 POSITION pos = GetFirstSelectedItemPosition();
3472 while (pos)
3474 int index = GetNextSelectedItem(pos);
3475 // we selected only cols we want, so not other then select test needed
3476 for (int column = 0; column < count; ++column)
3478 if (cmd && (GITSLC_COLFILENAME & (1 << column)))
3480 auto* entry = GetListEntry(index);
3481 if (entry)
3483 CString sPath;
3484 switch (cmd)
3486 case IDGITLC_COPYFULL:
3487 sPath = g_Git.CombinePath(entry);
3488 break;
3489 case IDGITLC_COPYRELPATHS:
3490 sPath = entry->GetGitPathString();
3491 break;
3492 case IDGITLC_COPYFILENAMES:
3493 sPath = entry->GetFileOrDirectoryName();
3494 break;
3496 ADDTOCLIPBOARDSTRING(sPath);
3499 else if (selection & (1 << column))
3500 ADDTOCLIPBOARDSTRING(GetCellText(index, column));
3503 ADDNEWLINETOCLIPBOARDSTRING();
3506 return CStringUtils::WriteAsciiStringToClipboard(sClipboard);
3509 size_t CGitStatusListCtrl::GetNumberOfChangelistsInSelection()
3511 #if 0
3512 CAutoReadLock locker(m_guard);
3513 std::set<CString> changelists;
3514 POSITION pos = GetFirstSelectedItemPosition();
3515 int index;
3516 while ((index = GetNextSelectedItem(pos)) >= 0)
3518 FileEntry * entry = GetListEntry(index);
3519 if (!entry->changelist.IsEmpty())
3520 changelists.insert(entry->changelist);
3522 return changelists.size();
3523 #endif
3524 return 0;
3527 bool CGitStatusListCtrl::PrepareGroups(bool bForce /* = false */)
3529 CAutoWriteLock locker(m_guard);
3530 bool bHasGroups=false;
3531 int max =0;
3533 for (size_t i = 0; i < m_arStatusArray.size(); ++i)
3535 int ParentNo = m_arStatusArray[i]->m_ParentNo&PARENT_MASK;
3536 if( ParentNo > max)
3537 max=m_arStatusArray[i]->m_ParentNo&PARENT_MASK;
3540 if (((m_dwShow & GITSLC_SHOWUNVERSIONED) && !m_UnRevFileList.IsEmpty()) ||
3541 ((m_dwShow & GITSLC_SHOWIGNORED) && !m_IgnoreFileList.IsEmpty()) ||
3542 (m_dwShow & (GITSLC_SHOWASSUMEVALID | GITSLC_SHOWSKIPWORKTREE) && !m_LocalChangesIgnoredFileList.IsEmpty()) ||
3543 max>0 || bForce)
3545 bHasGroups = true;
3548 RemoveAllGroups();
3549 EnableGroupView(bHasGroups);
3551 TCHAR groupname[1024] = { 0 };
3552 int groupindex = 0;
3554 if(bHasGroups)
3556 LVGROUP grp = {0};
3557 grp.cbSize = sizeof(LVGROUP);
3558 grp.mask = LVGF_ALIGN | LVGF_GROUPID | LVGF_HEADER;
3559 groupindex=0;
3561 //if(m_UnRevFileList.GetCount()>0)
3562 if(max >0)
3564 wcsncpy_s(groupname, 1024, (LPCTSTR)CString(MAKEINTRESOURCE(IDS_STATUSLIST_GROUP_MERGEDFILES)), 1023);
3565 grp.pszHeader = groupname;
3566 grp.iGroupId = MERGE_MASK;
3567 grp.uAlign = LVGA_HEADER_LEFT;
3568 InsertGroup(0, &grp);
3570 CAutoRepository repository(g_Git.GetGitRepository());
3571 if (!repository)
3572 MessageBox(CGit::GetLibGit2LastErr(L"Could not open repository."), L"TortoiseGit", MB_OK | MB_ICONERROR);
3573 for (int i = 0; i <= max; ++i)
3575 CString str;
3576 str.Format(IDS_STATUSLIST_GROUP_DIFFWITHPARENT, i+1);
3577 if (repository)
3579 CString rev;
3580 rev.Format(L"%s^%d", (LPCTSTR)m_CurrentVersion.ToString(), i + 1);
3581 CGitHash hash;
3582 if (!CGit::GetHash(repository, hash, rev))
3583 str += L": " + hash.ToString().Left(g_Git.GetShortHASHLength());
3585 grp.pszHeader = str.GetBuffer();
3586 str.ReleaseBuffer();
3587 grp.iGroupId = i;
3588 grp.uAlign = LVGA_HEADER_LEFT;
3589 InsertGroup(i+1, &grp);
3592 else
3594 wcsncpy_s(groupname, 1024, (LPCTSTR)CString(MAKEINTRESOURCE(IDS_STATUSLIST_GROUP_MODIFIEDFILES)), 1023);
3595 grp.pszHeader = groupname;
3596 grp.iGroupId = groupindex;
3597 grp.uAlign = LVGA_HEADER_LEFT;
3598 InsertGroup(groupindex++, &grp);
3601 wcsncpy_s(groupname, 1024, (LPCTSTR)CString(MAKEINTRESOURCE(IDS_STATUSLIST_GROUP_NOTVERSIONEDFILES)), 1023);
3602 grp.pszHeader = groupname;
3603 grp.iGroupId = groupindex;
3604 grp.uAlign = LVGA_HEADER_LEFT;
3605 InsertGroup(groupindex++, &grp);
3608 //if(m_IgnoreFileList.GetCount()>0)
3610 wcsncpy_s(groupname, 1024, (LPCTSTR)CString(MAKEINTRESOURCE(IDS_STATUSLIST_GROUP_IGNOREDFILES)), 1023);
3611 grp.pszHeader = groupname;
3612 grp.iGroupId = groupindex;
3613 grp.uAlign = LVGA_HEADER_LEFT;
3614 InsertGroup(groupindex++, &grp);
3618 wcsncpy_s(groupname, 1024, (LPCTSTR)CString(MAKEINTRESOURCE(IDS_STATUSLIST_GROUP_IGNORELOCALCHANGES)), 1023);
3619 grp.pszHeader = groupname;
3620 grp.iGroupId = groupindex;
3621 grp.uAlign = LVGA_HEADER_LEFT;
3622 InsertGroup(groupindex++, &grp);
3627 #if 0
3628 m_bHasIgnoreGroup = false;
3630 // now add the items which don't belong to a group
3631 LVGROUP grp = {0};
3632 grp.cbSize = sizeof(LVGROUP);
3633 grp.mask = LVGF_ALIGN | LVGF_GROUPID | LVGF_HEADER;
3634 CString sUnassignedName(MAKEINTRESOURCE(IDS_STATUSLIST_UNASSIGNED_CHANGESET));
3635 wcsncpy_s(groupname, 1024, (LPCTSTR)sUnassignedName, 1023);
3636 grp.pszHeader = groupname;
3637 grp.iGroupId = groupindex;
3638 grp.uAlign = LVGA_HEADER_LEFT;
3639 InsertGroup(groupindex++, &grp);
3641 for (auto it = m_changelists.cbegin(); it != m_changelists.cend(); ++it)
3643 if (it->first.Compare(SVNSLC_IGNORECHANGELIST)!=0)
3645 LVGROUP grp = {0};
3646 grp.cbSize = sizeof(LVGROUP);
3647 grp.mask = LVGF_ALIGN | LVGF_GROUPID | LVGF_HEADER;
3648 wcsncpy_s(groupname, 1024, it->first, 1023);
3649 grp.pszHeader = groupname;
3650 grp.iGroupId = groupindex;
3651 grp.uAlign = LVGA_HEADER_LEFT;
3652 it->second = InsertGroup(groupindex++, &grp);
3654 else
3655 m_bHasIgnoreGroup = true;
3658 if (m_bHasIgnoreGroup)
3660 // and now add the group 'ignore-on-commit'
3661 std::map<CString,int>::iterator it = m_changelists.find(SVNSLC_IGNORECHANGELIST);
3662 if (it != m_changelists.end())
3664 grp.cbSize = sizeof(LVGROUP);
3665 grp.mask = LVGF_ALIGN | LVGF_GROUPID | LVGF_HEADER;
3666 wcsncpy_s(groupname, 1024, SVNSLC_IGNORECHANGELIST, 1023);
3667 grp.pszHeader = groupname;
3668 grp.iGroupId = groupindex;
3669 grp.uAlign = LVGA_HEADER_LEFT;
3670 it->second = InsertGroup(groupindex, &grp);
3673 #endif
3674 return bHasGroups;
3677 void CGitStatusListCtrl::NotifyCheck()
3679 CWnd* pParent = GetLogicalParent();
3680 if (pParent && pParent->GetSafeHwnd())
3682 pParent->SendMessage(GITSLNM_CHECKCHANGED, m_nSelected);
3686 int CGitStatusListCtrl::UpdateFileList(const CTGitPathList* list)
3688 CAutoWriteLock locker(m_guard);
3689 m_CurrentVersion = GIT_REV_ZERO;
3691 g_Git.GetWorkingTreeChanges(m_StatusFileList, m_amend, list);
3693 BOOL bDeleteChecked = FALSE;
3694 int deleteFromIndex = 0;
3695 bool needsRefresh = false;
3696 for (int i = 0; i < m_StatusFileList.GetCount(); ++i)
3698 auto gitpatch = const_cast<CTGitPath*>(&m_StatusFileList[i]);
3699 gitpatch->m_Checked = TRUE;
3701 if ((gitpatch->m_Action & (CTGitPath::LOGACTIONS_ADDED | CTGitPath::LOGACTIONS_REPLACED | CTGitPath::LOGACTIONS_MODIFIED)) && !gitpatch->Exists())
3703 if (!bDeleteChecked)
3705 CString message;
3706 message.Format(IDS_ASK_REMOVE_FROM_INDEX, gitpatch->GetWinPath());
3707 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);
3709 if (deleteFromIndex == 1)
3711 CString err;
3712 if (g_Git.Run(L"git.exe checkout -- \"" + gitpatch->GetWinPathString() + L'"', &err, CP_UTF8))
3713 MessageBox(L"Restoring from index failed:\n" + err, L"TortoiseGit", MB_ICONERROR);
3714 else
3715 needsRefresh = true;
3717 else if (deleteFromIndex == 2)
3719 CString err;
3720 if (g_Git.Run(L"git.exe rm -f --cache -- \"" + gitpatch->GetWinPathString() + L'"', &err, CP_UTF8))
3721 MessageBox(L"Removing from index failed:\n" + err, L"TortoiseGit", MB_ICONERROR);
3722 else
3723 needsRefresh = true;
3727 m_arStatusArray.push_back(&m_StatusFileList[i]);
3730 if (needsRefresh)
3731 MessageBox(L"Due to changes to the index, please refresh the dialog (e.g., by pressing F5).", L"TortoiseGit", MB_ICONINFORMATION);
3733 return 0;
3736 int CGitStatusListCtrl::UpdateWithGitPathList(CTGitPathList &list)
3738 CAutoWriteLock locker(m_guard);
3739 m_arStatusArray.clear();
3740 for (int i = 0; i < list.GetCount(); ++i)
3742 auto gitpath = const_cast<CTGitPath*>(&list[i]);
3744 if(gitpath ->m_Action & CTGitPath::LOGACTIONS_HIDE)
3745 continue;
3747 gitpath->m_Checked = TRUE;
3748 m_arStatusArray.push_back(&list[i]);
3750 return 0;
3753 int CGitStatusListCtrl::UpdateUnRevFileList(CTGitPathList &list)
3755 CAutoWriteLock locker(m_guard);
3756 m_UnRevFileList = list;
3757 for (int i = 0; i < m_UnRevFileList.GetCount(); ++i)
3759 auto gitpatch = const_cast<CTGitPath*>(&m_UnRevFileList[i]);
3760 gitpatch->m_Checked = FALSE;
3761 m_arStatusArray.push_back(&m_UnRevFileList[i]);
3763 return 0;
3766 int CGitStatusListCtrl::UpdateUnRevFileList(const CTGitPathList* List)
3768 CAutoWriteLock locker(m_guard);
3769 CString err;
3770 if (m_UnRevFileList.FillUnRev(CTGitPath::LOGACTIONS_UNVER, List, &err))
3772 MessageBox(L"Failed to get UnRev file list\n" + err, L"TortoiseGit", MB_OK | MB_ICONERROR);
3773 return -1;
3776 if (m_StatusFileList.m_Action & CTGitPath::LOGACTIONS_DELETED)
3778 int unrev = 0;
3779 int status = 0;
3780 while (unrev < m_UnRevFileList.GetCount() && status < m_StatusFileList.GetCount())
3782 auto cmp = CTGitPath::Compare(m_UnRevFileList[unrev], m_StatusFileList[status]);
3783 if (cmp < 1)
3785 ++unrev;
3786 continue;
3788 if (cmp == 1)
3789 m_UnRevFileList.RemovePath(m_StatusFileList[status]);
3790 ++status;
3794 for (int i = 0; i < m_UnRevFileList.GetCount(); ++i)
3796 auto gitpatch = const_cast<CTGitPath*>(&m_UnRevFileList[i]);
3797 gitpatch->m_Checked = FALSE;
3798 m_arStatusArray.push_back(&m_UnRevFileList[i]);
3800 return 0;
3803 int CGitStatusListCtrl::UpdateIgnoreFileList(const CTGitPathList* List)
3805 CAutoWriteLock locker(m_guard);
3806 CString err;
3807 if (m_IgnoreFileList.FillUnRev(CTGitPath::LOGACTIONS_IGNORE, List, &err))
3809 MessageBox(L"Failed to get Ignore file list\n" + err, L"TortoiseGit", MB_OK | MB_ICONERROR);
3810 return -1;
3813 for (int i = 0; i < m_IgnoreFileList.GetCount(); ++i)
3815 auto gitpatch = const_cast<CTGitPath*>(&m_IgnoreFileList[i]);
3816 gitpatch->m_Checked = FALSE;
3817 m_arStatusArray.push_back(&m_IgnoreFileList[i]);
3819 return 0;
3822 int CGitStatusListCtrl::UpdateLocalChangesIgnoredFileList(const CTGitPathList* list)
3824 CAutoWriteLock locker(m_guard);
3825 m_LocalChangesIgnoredFileList.FillBasedOnIndexFlags(GIT_IDXENTRY_VALID, GIT_IDXENTRY_SKIP_WORKTREE, list);
3826 for (int i = 0; i < m_LocalChangesIgnoredFileList.GetCount(); ++i)
3828 auto gitpatch = const_cast<CTGitPath*>(&m_LocalChangesIgnoredFileList[i]);
3829 gitpatch->m_Checked = FALSE;
3830 m_arStatusArray.push_back(&m_LocalChangesIgnoredFileList[i]);
3832 return 0;
3835 int CGitStatusListCtrl::UpdateFileList(int mask, bool once, const CTGitPathList* pList)
3837 CAutoWriteLock locker(m_guard);
3838 auto List = (pList && pList->GetCount() >= 1 && !(*pList)[0].GetWinPathString().IsEmpty()) ? pList : nullptr;
3839 if(mask&CGitStatusListCtrl::FILELIST_MODIFY)
3841 if(once || (!(m_FileLoaded&CGitStatusListCtrl::FILELIST_MODIFY)))
3843 UpdateFileList(List);
3844 m_FileLoaded|=CGitStatusListCtrl::FILELIST_MODIFY;
3847 if (mask & CGitStatusListCtrl::FILELIST_UNVER || mask & CGitStatusListCtrl::FILELIST_IGNORE)
3849 if(once || (!(m_FileLoaded&CGitStatusListCtrl::FILELIST_UNVER)))
3851 UpdateUnRevFileList(List);
3852 m_FileLoaded|=CGitStatusListCtrl::FILELIST_UNVER;
3854 if(mask&CGitStatusListCtrl::FILELIST_IGNORE && (once || (!(m_FileLoaded&CGitStatusListCtrl::FILELIST_IGNORE))))
3856 UpdateIgnoreFileList(List);
3857 m_FileLoaded |= CGitStatusListCtrl::FILELIST_IGNORE;
3860 if (mask & CGitStatusListCtrl::FILELIST_LOCALCHANGESIGNORED && (once || (!(m_FileLoaded & CGitStatusListCtrl::FILELIST_LOCALCHANGESIGNORED))))
3862 UpdateLocalChangesIgnoredFileList(List);
3863 m_FileLoaded |= CGitStatusListCtrl::FILELIST_LOCALCHANGESIGNORED;
3865 return 0;
3868 void CGitStatusListCtrl::Clear()
3870 CAutoWriteLock locker(m_guard);
3871 m_FileLoaded=0;
3872 this->DeleteAllItems();
3873 this->m_arListArray.clear();
3874 this->m_arStatusArray.clear();
3875 this->m_changelists.clear();
3878 bool CGitStatusListCtrl::CheckMultipleDiffs()
3880 UINT selCount = GetSelectedCount();
3881 if (selCount > max(3, (DWORD)CRegDWORD(L"Software\\TortoiseGit\\NumDiffWarning", 10)))
3883 CString message;
3884 message.Format(IDS_STATUSLIST_WARN_MAXDIFF, selCount);
3885 return MessageBox(message, L"TortoiseGit", MB_YESNO | MB_ICONQUESTION) == IDYES;
3887 return true;
3890 //////////////////////////////////////////////////////////////////////////
3891 bool CGitStatusListCtrlDropTarget::OnDrop(FORMATETC* pFmtEtc, STGMEDIUM& medium, DWORD* /*pdwEffect*/, POINTL pt)
3893 if (pFmtEtc->cfFormat == CF_HDROP && medium.tymed == TYMED_HGLOBAL)
3895 HDROP hDrop = (HDROP)GlobalLock(medium.hGlobal);
3896 if (hDrop)
3898 TCHAR szFileName[MAX_PATH] = {0};
3900 UINT cFiles = DragQueryFile(hDrop, 0xFFFFFFFF, nullptr, 0);
3902 POINT clientpoint;
3903 clientpoint.x = pt.x;
3904 clientpoint.y = pt.y;
3905 ScreenToClient(m_hTargetWnd, &clientpoint);
3906 if ((m_pGitStatusListCtrl->IsGroupViewEnabled()) && (m_pGitStatusListCtrl->GetGroupFromPoint(&clientpoint) >= 0))
3908 #if 0
3909 CTGitPathList changelistItems;
3910 for (UINT i = 0; i < cFiles; ++i)
3912 if (DragQueryFile(hDrop, i, szFileName, _countof(szFileName)))
3913 changelistItems.AddPath(CTGitPath(szFileName));
3915 // find the changelist name
3916 CString sChangelist;
3917 LONG_PTR nGroup = m_pGitStatusListCtrl->GetGroupFromPoint(&clientpoint);
3918 for (std::map<CString, int>::iterator it = m_pGitStatusListCtrl->m_changelists.begin(); it != m_pGitStatusListCtrl->m_changelists.end(); ++it)
3919 if (it->second == nGroup)
3920 sChangelist = it->first;
3922 if (!sChangelist.IsEmpty())
3924 CGit git;
3925 if (git.AddToChangeList(changelistItems, sChangelist, git_depth_empty))
3927 for (int l=0; l<changelistItems.GetCount(); ++l)
3929 int index = m_pGitStatusListCtrl->GetIndex(changelistItems[l]);
3930 if (index >= 0)
3932 auto e = m_pGitStatusListCtrl->GetListEntry(index);
3933 if (e)
3935 e->changelist = sChangelist;
3936 if (!e->IsFolder())
3938 if (m_pGitStatusListCtrl->m_changelists.find(e->changelist) != m_pGitStatusListCtrl->m_changelists.end())
3939 m_pGitStatusListCtrl->SetItemGroup(index, m_pGitStatusListCtrl->m_changelists[e->changelist]);
3940 else
3941 m_pGitStatusListCtrl->SetItemGroup(index, 0);
3945 else
3947 HWND hParentWnd = GetParent(m_hTargetWnd);
3948 if (hParentWnd)
3949 ::SendMessage(hParentWnd, CGitStatusListCtrl::GITSLNM_ADDFILE, 0, (LPARAM)changelistItems[l].GetWinPath());
3953 else
3954 CMessageBox::Show(m_pGitStatusListCtrl->m_hWnd, git.GetLastErrorMessage(), L"TortoiseGit", MB_ICONERROR);
3956 else
3958 SVN git;
3959 if (git.RemoveFromChangeList(changelistItems, CStringArray(), git_depth_empty))
3961 for (int l=0; l<changelistItems.GetCount(); ++l)
3963 int index = m_pGitStatusListCtrl->GetIndex(changelistItems[l]);
3964 if (index >= 0)
3966 auto e = m_pGitStatusListCtrl->GetListEntry(index);
3967 if (e)
3969 e->changelist = sChangelist;
3970 m_pGitStatusListCtrl->SetItemGroup(index, 0);
3973 else
3975 HWND hParentWnd = GetParent(m_hTargetWnd);
3976 if (hParentWnd)
3977 ::SendMessage(hParentWnd, CGitStatusListCtrl::GITSLNM_ADDFILE, 0, (LPARAM)changelistItems[l].GetWinPath());
3981 else
3982 CMessageBox::Show(m_pGitStatusListCtrl->m_hWnd, git.GetLastErrorMessage(), L"TortoiseGit", MB_ICONERROR);
3984 #endif
3986 else
3988 for (UINT i = 0; i < cFiles; ++i)
3990 if (!DragQueryFile(hDrop, i, szFileName, _countof(szFileName)))
3991 continue;
3993 HWND hParentWnd = GetParent(m_hTargetWnd);
3994 if (hParentWnd)
3995 ::SendMessage(hParentWnd, CGitStatusListCtrl::GITSLNM_ADDFILE, 0, (LPARAM)szFileName);
3999 GlobalUnlock(medium.hGlobal);
4001 return true; //let base free the medium
4004 HRESULT STDMETHODCALLTYPE CGitStatusListCtrlDropTarget::DragOver(DWORD grfKeyState, POINTL pt, DWORD __RPC_FAR* pdwEffect)
4006 CIDropTarget::DragOver(grfKeyState, pt, pdwEffect);
4007 *pdwEffect = DROPEFFECT_COPY;
4008 if (m_pGitStatusListCtrl)
4010 POINT clientpoint;
4011 clientpoint.x = pt.x;
4012 clientpoint.y = pt.y;
4013 ScreenToClient(m_hTargetWnd, &clientpoint);
4014 if ((m_pGitStatusListCtrl->IsGroupViewEnabled()) && (m_pGitStatusListCtrl->GetGroupFromPoint(&clientpoint) >= 0))
4015 *pdwEffect = DROPEFFECT_NONE;
4016 else if ((!m_pGitStatusListCtrl->m_bFileDropsEnabled) || (m_pGitStatusListCtrl->m_bOwnDrag))
4017 *pdwEffect = DROPEFFECT_NONE;
4019 return S_OK;
4022 void CGitStatusListCtrl::FilesExport()
4024 CAutoReadLock locker(m_guard);
4025 CString exportDir;
4026 // export all changed files to a folder
4027 CBrowseFolder browseFolder;
4028 browseFolder.m_style = BIF_EDITBOX | BIF_NEWDIALOGSTYLE | BIF_RETURNFSANCESTORS | BIF_RETURNONLYFSDIRS;
4029 if (browseFolder.Show(GetParentHWND(), exportDir) != CBrowseFolder::OK)
4030 return;
4032 POSITION pos = GetFirstSelectedItemPosition();
4033 int index;
4034 while ((index = GetNextSelectedItem(pos)) >= 0)
4036 auto fd = GetListEntry(index);
4037 // we cannot export directories or folders
4038 if ((fd->m_Action & CTGitPath::LOGACTIONS_DELETED) || fd->IsDirectory())
4039 continue;
4041 CPathUtils::MakeSureDirectoryPathExists(exportDir + L'\\' + fd->GetContainingDirectory().GetWinPathString());
4042 CString filename = exportDir + L'\\' + fd->GetWinPathString();
4043 if (m_CurrentVersion.IsEmpty())
4045 if (!CopyFile(g_Git.CombinePath(fd), filename, false))
4047 MessageBox(CFormatMessageWrapper(), L"TortoiseGit", MB_OK | MB_ICONERROR);
4048 return;
4051 else
4053 if (g_Git.GetOneFile(m_CurrentVersion.ToString(), *fd, filename))
4055 CString out;
4056 out.FormatMessage(IDS_STATUSLIST_CHECKOUTFILEFAILED, (LPCTSTR)fd->GetGitPathString(), (LPCTSTR)m_CurrentVersion.ToString(), (LPCTSTR)filename);
4057 if (CMessageBox::Show(GetParentHWND(), g_Git.GetGitLastErr(out, CGit::GIT_CMD_GETONEFILE), L"TortoiseGit", 2, IDI_WARNING, CString(MAKEINTRESOURCE(IDS_IGNOREBUTTON)), CString(MAKEINTRESOURCE(IDS_ABORTBUTTON))) == 2)
4058 return;
4064 void CGitStatusListCtrl::FileSaveAs(CTGitPath *path)
4066 CAutoReadLock locker(m_guard);
4067 CString filename;
4068 filename.Format(L"%s\\%s-%s%s", (LPCTSTR)g_Git.CombinePath(path->GetContainingDirectory()), (LPCTSTR)path->GetBaseFilename(), (LPCTSTR)m_CurrentVersion.ToString().Left(g_Git.GetShortHASHLength()), (LPCTSTR)path->GetFileExtension());
4069 if (!CAppUtils::FileOpenSave(filename, nullptr, 0, 0, false, GetSafeHwnd()))
4070 return;
4071 if (m_CurrentVersion.IsEmpty())
4073 if (!CopyFile(g_Git.CombinePath(path), filename, false))
4075 MessageBox(CFormatMessageWrapper(), L"TortoiseGit", MB_OK | MB_ICONERROR);
4076 return;
4079 else
4081 if (g_Git.GetOneFile(m_CurrentVersion.ToString(), *path, filename))
4083 CString out;
4084 out.FormatMessage(IDS_STATUSLIST_CHECKOUTFILEFAILED, (LPCTSTR)path->GetGitPathString(), (LPCTSTR)m_CurrentVersion.ToString(), (LPCTSTR)filename);
4085 CMessageBox::Show(GetParentHWND(), g_Git.GetGitLastErr(out, CGit::GIT_CMD_GETONEFILE), L"TortoiseGit", MB_OK);
4086 return;
4091 int CGitStatusListCtrl::RevertSelectedItemToVersion(bool parent)
4093 CAutoReadLock locker(m_guard);
4094 if(this->m_CurrentVersion.IsEmpty())
4095 return 0;
4096 if (m_CurrentVersion.IsEmpty())
4097 return 0;
4099 POSITION pos = GetFirstSelectedItemPosition();
4100 int index;
4101 CString cmd,out;
4102 std::map<CString, int> versionMap;
4103 while ((index = GetNextSelectedItem(pos)) >= 0)
4105 auto fentry = GetListEntry(index);
4106 CString version;
4107 if (parent)
4109 int parentNo = fentry->m_ParentNo & PARENT_MASK;
4110 CString ref;
4111 ref.Format(L"%s^%d", (LPCTSTR)m_CurrentVersion.ToString(), parentNo + 1);
4112 CGitHash hash;
4113 if (g_Git.GetHash(hash, ref))
4115 MessageBox(g_Git.GetGitLastErr(L"Could not get hash of ref \"" + ref + L"\"."), L"TortoiseGit", MB_ICONERROR);
4116 continue;
4119 version = hash.ToString();
4121 else
4122 version = m_CurrentVersion;
4124 CString filename = fentry->GetGitPathString();
4125 if (!fentry->GetGitOldPathString().IsEmpty())
4126 filename = fentry->GetGitOldPathString();
4127 cmd.Format(L"git.exe checkout %s -- \"%s\"", (LPCTSTR)version, (LPCTSTR)filename);
4128 out.Empty();
4129 if (g_Git.Run(cmd, &out, CP_UTF8))
4131 if (MessageBox(out, L"TortoiseGit", MB_ICONEXCLAMATION | MB_OKCANCEL) == IDCANCEL)
4132 continue;
4134 else
4135 versionMap[version]++;
4138 out.Empty();
4139 for (auto it = versionMap.cbegin(); it != versionMap.cend(); ++it)
4141 CString versionEntry;
4142 versionEntry.FormatMessage(IDS_STATUSLIST_FILESREVERTED, it->second, (LPCTSTR)it->first);
4143 out += versionEntry + L"\r\n";
4145 if (!out.IsEmpty())
4147 if (GetLogicalParent() && GetLogicalParent()->GetSafeHwnd())
4148 GetLogicalParent()->SendMessage(GITSLNM_NEEDSREFRESH);
4149 CMessageBox::Show(GetParentHWND(), out, L"TortoiseGit", MB_OK);
4151 return 0;
4154 void CGitStatusListCtrl::OpenFile(CTGitPath*filepath,int mode)
4156 CString file;
4157 if (m_CurrentVersion.IsEmpty())
4158 file = g_Git.CombinePath(filepath);
4159 else
4161 file = CTempFiles::Instance().GetTempFilePath(false, *filepath, m_CurrentVersion).GetWinPathString();
4162 CString cmd,out;
4163 if(g_Git.GetOneFile(m_CurrentVersion.ToString(), *filepath, file))
4165 out.FormatMessage(IDS_STATUSLIST_CHECKOUTFILEFAILED, (LPCTSTR)filepath->GetGitPathString(), (LPCTSTR)m_CurrentVersion.ToString(), (LPCTSTR)file);
4166 CMessageBox::Show(GetParentHWND(), g_Git.GetGitLastErr(out, CGit::GIT_CMD_GETONEFILE), L"TortoiseGit", MB_OK);
4167 return;
4169 SetFileAttributes(file, FILE_ATTRIBUTE_READONLY);
4171 if(mode == ALTERNATIVEEDITOR)
4173 CAppUtils::LaunchAlternativeEditor(file);
4174 return;
4177 if (mode == OPEN)
4178 CAppUtils::ShellOpen(file, GetSafeHwnd());
4179 else
4180 CAppUtils::ShowOpenWithDialog(file, GetSafeHwnd());
4183 void CGitStatusListCtrl::DeleteSelectedFiles()
4185 CAutoWriteLock locker(m_guard);
4186 //Collect paths
4187 std::vector<int> selectIndex;
4189 POSITION pos = GetFirstSelectedItemPosition();
4190 int index;
4191 while ((index = GetNextSelectedItem(pos)) >= 0)
4192 selectIndex.push_back(index);
4194 CAutoRepository repo = g_Git.GetGitRepository();
4195 if (!repo)
4197 MessageBox(g_Git.GetLibGit2LastErr(L"Could not open repository."), L"TortoiseGit", MB_OK);
4198 return;
4200 CAutoIndex gitIndex;
4201 if (git_repository_index(gitIndex.GetPointer(), repo))
4203 g_Git.GetLibGit2LastErr(L"Could not open index.");
4204 return;
4206 int needWriteIndex = 0;
4208 //Create file-list ('\0' separated) for SHFileOperation
4209 CString filelist;
4210 for (size_t i = 0; i < selectIndex.size(); ++i)
4212 index = selectIndex[i];
4214 auto path = GetListEntry(index);
4215 if (path == nullptr)
4216 continue;
4218 // do not report errors as we could remove an unversioned file
4219 needWriteIndex += git_index_remove_bypath(gitIndex, CUnicodeUtils::GetUTF8(path->GetGitPathString())) == 0 ? 1 : 0;
4221 if (!path->Exists())
4222 continue;
4224 filelist += path->GetWinPathString();
4225 filelist += L'|';
4227 filelist += L'|';
4228 int len = filelist.GetLength();
4229 auto buf = std::make_unique<TCHAR[]>(len + 2);
4230 wcscpy_s(buf.get(), len + 2, filelist);
4231 CStringUtils::PipesToNulls(buf.get(), len + 2);
4232 SHFILEOPSTRUCT fileop;
4233 fileop.hwnd = this->m_hWnd;
4234 fileop.wFunc = FO_DELETE;
4235 fileop.pFrom = buf.get();
4236 fileop.pTo = nullptr;
4237 fileop.fFlags = FOF_NO_CONNECTED_ELEMENTS | ((GetAsyncKeyState(VK_SHIFT) & 0x8000) ? 0 : FOF_ALLOWUNDO);
4238 fileop.lpszProgressTitle = L"deleting file";
4239 int result = SHFileOperation(&fileop);
4241 if ((result == 0 || len == 1) && (!fileop.fAnyOperationsAborted))
4243 if (needWriteIndex && git_index_write(gitIndex))
4244 MessageBox(g_Git.GetLibGit2LastErr(L"Could not write index."), L"TortoiseGit", MB_OK);
4246 if (needWriteIndex)
4248 CWnd* pParent = GetLogicalParent();
4249 if (pParent && pParent->GetSafeHwnd())
4250 pParent->SendMessage(GITSLNM_NEEDSREFRESH);
4251 SetRedraw(TRUE);
4252 return;
4255 SetRedraw(FALSE);
4256 POSITION pos2 = nullptr;
4257 while ((pos2 = GetFirstSelectedItemPosition()) != nullptr)
4259 int index2 = GetNextSelectedItem(pos2);
4260 if (GetCheck(index2))
4261 m_nSelected--;
4262 m_nTotal--;
4264 RemoveListEntry(index2);
4266 SetRedraw(TRUE);
4270 BOOL CGitStatusListCtrl::OnWndMsg(UINT message, WPARAM wParam, LPARAM lParam, LRESULT* pResult)
4272 switch (message)
4274 case WM_MENUCHAR: // only supported by IContextMenu3
4275 if (g_IContext3)
4277 g_IContext3->HandleMenuMsg2(message, wParam, lParam, pResult);
4278 return TRUE;
4280 break;
4282 case WM_DRAWITEM:
4283 case WM_MEASUREITEM:
4284 if (wParam)
4285 break; // if wParam != 0 then the message is not menu-related
4287 case WM_INITMENU:
4288 case WM_INITMENUPOPUP:
4290 HMENU hMenu = (HMENU)wParam;
4291 if ((hMenu == m_hShellMenu) && (GetMenuItemCount(hMenu) == 0))
4293 // the shell submenu is populated only on request, i.e. right
4294 // before the submenu is shown
4295 if (g_pFolderhook)
4297 delete g_pFolderhook;
4298 g_pFolderhook = nullptr;
4300 CTGitPathList targetList;
4301 FillListOfSelectedItemPaths(targetList);
4302 if (!targetList.IsEmpty())
4304 // get IShellFolder interface of Desktop (root of shell namespace)
4305 if (g_psfDesktopFolder)
4306 g_psfDesktopFolder->Release();
4307 SHGetDesktopFolder(&g_psfDesktopFolder); // needed to obtain full qualified pidl
4309 // ParseDisplayName creates a PIDL from a file system path relative to the IShellFolder interface
4310 // but since we use the Desktop as our interface and the Desktop is the namespace root
4311 // that means that it's a fully qualified PIDL, which is what we need
4313 if (g_pidlArray)
4315 for (int i = 0; i < g_pidlArrayItems; i++)
4317 if (g_pidlArray[i])
4318 CoTaskMemFree(g_pidlArray[i]);
4320 CoTaskMemFree(g_pidlArray);
4321 g_pidlArray = nullptr;
4322 g_pidlArrayItems = 0;
4324 int nItems = targetList.GetCount();
4325 g_pidlArray = (LPITEMIDLIST *)CoTaskMemAlloc((nItems + 10) * sizeof(LPITEMIDLIST));
4326 SecureZeroMemory(g_pidlArray, (nItems + 10) * sizeof(LPITEMIDLIST));
4327 int succeededItems = 0;
4328 PIDLIST_RELATIVE pidl = nullptr;
4330 int bufsize = 1024;
4331 auto filepath = std::make_unique<WCHAR[]>(bufsize);
4332 for (int i = 0; i < nItems; i++)
4334 CString fullPath = g_Git.CombinePath(targetList[i].GetWinPath());
4335 if (bufsize < fullPath.GetLength())
4337 bufsize = fullPath.GetLength() + 3;
4338 filepath = std::make_unique<WCHAR[]>(bufsize);
4340 wcscpy_s(filepath.get(), bufsize, fullPath);
4341 if (SUCCEEDED(g_psfDesktopFolder->ParseDisplayName(nullptr, 0, filepath.get(), nullptr, &pidl, nullptr)))
4342 g_pidlArray[succeededItems++] = pidl; // copy pidl to pidlArray
4344 if (succeededItems == 0)
4346 CoTaskMemFree(g_pidlArray);
4347 g_pidlArray = nullptr;
4350 g_pidlArrayItems = succeededItems;
4352 if (g_pidlArrayItems)
4354 CString ext = targetList[0].GetFileExtension();
4356 ASSOCIATIONELEMENT const rgAssocItem[] =
4358 { ASSOCCLASS_PROGID_STR, nullptr, ext },
4359 { ASSOCCLASS_SYSTEM_STR, nullptr, ext },
4360 { ASSOCCLASS_APP_STR, nullptr, ext },
4361 { ASSOCCLASS_STAR, nullptr, nullptr },
4362 { ASSOCCLASS_FOLDER, nullptr, nullptr },
4364 IQueryAssociations* pIQueryAssociations = nullptr;
4365 if (FAILED(AssocCreateForClasses(rgAssocItem, ARRAYSIZE(rgAssocItem), IID_IQueryAssociations, (void**)&pIQueryAssociations)))
4366 pIQueryAssociations = nullptr; // not a problem, it works without this
4368 g_pFolderhook = new CIShellFolderHook(g_psfDesktopFolder, targetList);
4369 LPCONTEXTMENU icm1 = nullptr;
4371 DEFCONTEXTMENU dcm = { 0 };
4372 dcm.hwnd = m_hWnd;
4373 dcm.psf = g_pFolderhook;
4374 dcm.cidl = g_pidlArrayItems;
4375 dcm.apidl = (PCUITEMID_CHILD_ARRAY)g_pidlArray;
4376 dcm.punkAssociationInfo = pIQueryAssociations;
4377 if (SUCCEEDED(SHCreateDefaultContextMenu(&dcm, IID_IContextMenu, (void**)&icm1)))
4379 int iMenuType = 0; // to know which version of IContextMenu is supported
4380 if (icm1)
4381 { // since we got an IContextMenu interface we can now obtain the higher version interfaces via that
4382 if (icm1->QueryInterface(IID_IContextMenu3, (void**)&m_pContextMenu) == S_OK)
4383 iMenuType = 3;
4384 else if (icm1->QueryInterface(IID_IContextMenu2, (void**)&m_pContextMenu) == S_OK)
4385 iMenuType = 2;
4387 if (m_pContextMenu)
4388 icm1->Release(); // we can now release version 1 interface, cause we got a higher one
4389 else
4391 // since no higher versions were found
4392 // redirect ppContextMenu to version 1 interface
4393 iMenuType = 1;
4394 m_pContextMenu = icm1;
4397 if (m_pContextMenu)
4399 // lets fill the our popup menu
4400 UINT flags = CMF_NORMAL;
4401 flags |= (GetKeyState(VK_SHIFT) & 0x8000) != 0 ? CMF_EXTENDEDVERBS : 0;
4402 m_pContextMenu->QueryContextMenu(hMenu, 0, SHELL_MIN_CMD, SHELL_MAX_CMD, flags);
4405 // subclass window to handle menu related messages in CShellContextMenu
4406 if (iMenuType > 1) // only subclass if its version 2 or 3
4408 if (iMenuType == 2)
4409 g_IContext2 = (LPCONTEXTMENU2)m_pContextMenu;
4410 else // version 3
4411 g_IContext3 = (LPCONTEXTMENU3)m_pContextMenu;
4415 if (pIQueryAssociations)
4416 pIQueryAssociations->Release();
4419 if (g_IContext3)
4420 g_IContext3->HandleMenuMsg2(message, wParam, lParam, pResult);
4421 else if (g_IContext2)
4422 g_IContext2->HandleMenuMsg(message, wParam, lParam);
4423 return TRUE;
4427 break;
4428 default:
4429 break;
4432 return __super::OnWndMsg(message, wParam, lParam, pResult);
4435 CTGitPath* CGitStatusListCtrl::GetListEntry(int index)
4437 ATLASSERT(m_guard.GetCurrentThreadStatus());
4438 if ((size_t)index >= m_arListArray.size())
4440 ATLASSERT(FALSE);
4441 return nullptr;
4443 if (m_arListArray[index] >= m_arStatusArray.size())
4445 ATLASSERT(FALSE);
4446 return nullptr;
4448 return const_cast<CTGitPath*>(m_arStatusArray[m_arListArray[index]]);
4451 void CGitStatusListCtrl::OnSysColorChange()
4453 __super::OnSysColorChange();
4454 if (m_nBackgroundImageID)
4455 CAppUtils::SetListCtrlBackgroundImage(GetSafeHwnd(), m_nBackgroundImageID);
4458 ULONG CGitStatusListCtrl::GetGestureStatus(CPoint /*ptTouch*/)
4460 return 0;