Fix typos
[TortoiseGit.git] / src / Git / GitStatusListCtrl.cpp
blob51f7dd3732cca672f8f19a7cc4c16f29234b39a9
1 // TortoiseGit - a Windows shell extension for easy version control
3 // Copyright (C) 2008-2024 - 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 "ProgressCommands/LFSSetLockedProgressCommand.h"
45 #include "ProgressCommands/RevertProgressCommand.h"
46 #include "ProgressCommands/ResolveProgressCommand.h"
47 #include "IconMenu.h"
48 #include "FormatMessageWrapper.h"
49 #include "BrowseFolder.h"
50 #include "SysInfo.h"
51 #include "SysProgressDlg.h"
52 #include "CreateChangelistDlg.h"
53 #include "GitAdminDir.h"
54 #include "Theme.h"
56 #include <fstream>
58 const UINT CGitStatusListCtrl::GITSLNM_ITEMCOUNTCHANGED
59 = ::RegisterWindowMessage(L"GITSLNM_ITEMCOUNTCHANGED");
60 const UINT CGitStatusListCtrl::GITSLNM_NEEDSREFRESH
61 = ::RegisterWindowMessage(L"GITSLNM_NEEDSREFRESH");
62 const UINT CGitStatusListCtrl::GITSLNM_ADDFILE
63 = ::RegisterWindowMessage(L"GITSLNM_ADDFILE");
64 const UINT CGitStatusListCtrl::GITSLNM_CHECKCHANGED
65 = ::RegisterWindowMessage(L"GITSLNM_CHECKCHANGED");
66 const UINT CGitStatusListCtrl::GITSLNM_ITEMCHANGED
67 = ::RegisterWindowMessage(L"GITSLNM_ITEMCHANGED");
69 struct icompare
71 bool operator() (const std::wstring& lhs, const std::wstring& rhs) const
73 // no logical comparison here: we need this sorted strictly
74 return _wcsicmp(lhs.c_str(), rhs.c_str()) < 0;
78 class CIShellFolderHook : public IShellFolder
80 public:
81 CIShellFolderHook(LPSHELLFOLDER sf, const CTGitPathList& pathlist)
83 sf->AddRef();
84 m_iSF = sf;
85 // it seems the paths in the HDROP need to be sorted, otherwise
86 // it might not work properly or even crash.
87 // to get the items sorted, we just add them to a set
88 for (int i = 0; i < pathlist.GetCount(); ++i)
89 sortedpaths.insert(static_cast<LPCWSTR>(g_Git.CombinePath(pathlist[i].GetWinPath())));
92 ~CIShellFolderHook() { m_iSF->Release(); }
94 // IUnknown methods --------
95 HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, __RPC__deref_out void** ppvObject) override { return m_iSF->QueryInterface(riid, ppvObject); }
96 ULONG STDMETHODCALLTYPE AddRef() override { return m_iSF->AddRef(); }
97 ULONG STDMETHODCALLTYPE Release() override { return m_iSF->Release(); }
99 // IShellFolder methods ----
100 HRESULT STDMETHODCALLTYPE GetUIObjectOf(HWND hwndOwner, UINT cidl, PCUITEMID_CHILD_ARRAY apidl, REFIID riid, UINT* rgfReserved, void** ppv) override;
102 HRESULT STDMETHODCALLTYPE CompareIDs(LPARAM lParam, __RPC__in PCUIDLIST_RELATIVE pidl1, __RPC__in PCUIDLIST_RELATIVE pidl2) override { return m_iSF->CompareIDs(lParam, pidl1, pidl2); }
103 HRESULT STDMETHODCALLTYPE GetDisplayNameOf(__RPC__in_opt PCUITEMID_CHILD pidl, SHGDNF uFlags, __RPC__out STRRET* pName) override { return m_iSF->GetDisplayNameOf(pidl, uFlags, pName); }
104 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); }
105 HRESULT STDMETHODCALLTYPE EnumObjects(__RPC__in_opt HWND hwndOwner, SHCONTF grfFlags, __RPC__deref_out_opt IEnumIDList** ppenumIDList) override { return m_iSF->EnumObjects(hwndOwner, grfFlags, ppenumIDList); }
106 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); }
107 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); }
108 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); }
109 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); }
110 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); }
112 protected:
113 LPSHELLFOLDER m_iSF;
114 std::set<std::wstring, icompare> sortedpaths;
117 HRESULT STDMETHODCALLTYPE CIShellFolderHook::GetUIObjectOf(HWND hwndOwner, UINT cidl, PCUITEMID_CHILD_ARRAY apidl, REFIID riid, UINT* rgfReserved, void** ppv)
119 if (InlineIsEqualGUID(riid, IID_IDataObject))
121 HRESULT hres = m_iSF->GetUIObjectOf(hwndOwner, cidl, apidl, IID_IDataObject, nullptr, ppv);
122 if (FAILED(hres))
123 return hres;
125 auto idata = static_cast<LPDATAOBJECT>(*ppv);
126 // the IDataObject returned here doesn't have a HDROP, so we create one ourselves and add it to the IDataObject
127 // the HDROP is necessary for most context menu handlers
128 size_t nBufferSize = 0;
129 for (auto it = sortedpaths.cbegin(); it != sortedpaths.cend(); ++it)
131 if (HRESULT ret = SizeTAdd(nBufferSize, it->size(), &nBufferSize); ret != S_OK)
132 return ret;
134 if (HRESULT ret = SizeTAdd(nBufferSize, sortedpaths.size(), &nBufferSize); ret != S_OK) // +1 for '\0' separator for each path entry
135 return ret;
136 if (HRESULT ret; (ret = SizeTAdd(nBufferSize, 5, &nBufferSize)) != S_OK || (ret = SizeTMult(nBufferSize, sizeof(wchar_t), &nBufferSize)) != S_OK)
137 return ret;
138 if (HRESULT ret = SizeTAdd(nBufferSize, sizeof(DROPFILES), &nBufferSize); ret != S_OK)
139 return ret;
140 auto pBuffer = std::make_unique<char[]>(nBufferSize);
141 auto df = reinterpret_cast<DROPFILES*>(pBuffer.get());
142 df->pFiles = sizeof(DROPFILES);
143 df->fWide = 1;
144 auto pFilenames = reinterpret_cast<wchar_t*>(reinterpret_cast<BYTE*>(pBuffer.get()) + sizeof(DROPFILES));
145 wchar_t* pCurrentFilename = pFilenames;
147 for (auto it = sortedpaths.cbegin(); it != sortedpaths.cend(); ++it)
149 wcscpy_s(pCurrentFilename, it->size() + 1, it->c_str());
150 pCurrentFilename += it->size();
151 *pCurrentFilename = '\0'; // separator between file names
152 pCurrentFilename++;
154 *pCurrentFilename = '\0'; // terminate array
155 pCurrentFilename++;
156 *pCurrentFilename = '\0'; // terminate array
157 STGMEDIUM medium = { 0 };
158 medium.tymed = TYMED_HGLOBAL;
159 if (SIZE_T_MAX - 20 <= nBufferSize)
160 return INTSAFE_E_ARITHMETIC_OVERFLOW;
161 medium.hGlobal = GlobalAlloc(GMEM_ZEROINIT | GMEM_MOVEABLE, nBufferSize + 20);
162 if (medium.hGlobal)
164 LPVOID pMem = ::GlobalLock(medium.hGlobal);
165 if (pMem)
167 memcpy(pMem, pBuffer.get(), nBufferSize);
168 GlobalUnlock(medium.hGlobal);
169 FORMATETC formatetc = { 0 };
170 formatetc.cfFormat = CF_HDROP;
171 formatetc.dwAspect = DVASPECT_CONTENT;
172 formatetc.lindex = -1;
173 formatetc.tymed = TYMED_HGLOBAL;
174 medium.pUnkForRelease = nullptr;
175 hres = idata->SetData(&formatetc, &medium, TRUE);
176 return hres;
179 return E_OUTOFMEMORY;
181 else
183 // just pass it on to the base object
184 return m_iSF->GetUIObjectOf(hwndOwner, cidl, apidl, riid, rgfReserved, ppv);
188 IContextMenu2 * g_IContext2 = nullptr;
189 IContextMenu3 * g_IContext3 = nullptr;
190 CIShellFolderHook * g_pFolderhook = nullptr;
191 IShellFolder * g_psfDesktopFolder = nullptr;
192 LPITEMIDLIST * g_pidlArray = nullptr;
193 int g_pidlArrayItems = 0;
195 #define SHELL_MIN_CMD 10000
196 #define SHELL_MAX_CMD 20000
198 HRESULT CALLBACK dfmCallback(IShellFolder * /*psf*/, HWND /*hwnd*/, IDataObject * /*pdtobj*/, UINT uMsg, WPARAM /*wParam*/, LPARAM /*lParam*/)
200 switch (uMsg)
202 case DFM_MERGECONTEXTMENU:
203 return S_OK;
204 case DFM_INVOKECOMMAND:
205 case DFM_INVOKECOMMANDEX:
206 case DFM_GETDEFSTATICID: // Required for Windows 7 to pick a default
207 return S_FALSE;
209 return E_NOTIMPL;
212 BEGIN_MESSAGE_MAP(CGitStatusListCtrl, CResizableColumnsListCtrl<CListCtrl>)
213 ON_NOTIFY(HDN_ITEMCLICKA, 0, OnHdnItemclick)
214 ON_NOTIFY(HDN_ITEMCLICKW, 0, OnHdnItemclick)
215 ON_NOTIFY_REFLECT_EX(LVN_ITEMCHANGED, OnLvnItemchanged)
216 ON_WM_CONTEXTMENU()
217 ON_NOTIFY_REFLECT(NM_DBLCLK, OnNMDblclk)
218 ON_NOTIFY_REFLECT(LVN_GETINFOTIP, OnLvnGetInfoTip)
219 ON_NOTIFY_REFLECT_EX(NM_CUSTOMDRAW, OnNMCustomdraw)
220 ON_NOTIFY_REFLECT(LVN_GETDISPINFO, OnLvnGetdispinfo)
221 ON_WM_SETCURSOR()
222 ON_WM_GETDLGCODE()
223 ON_NOTIFY_REFLECT(NM_RETURN, OnNMReturn)
224 ON_WM_KEYDOWN()
225 ON_WM_PAINT()
226 ON_NOTIFY_REFLECT(LVN_BEGINDRAG, OnBeginDrag)
227 END_MESSAGE_MAP()
229 CGitStatusListCtrl::CGitStatusListCtrl() : CResizableColumnsListCtrl<CListCtrl>()
230 , m_nTooManyItemsThreshold(CRegDWORD(L"Software\\TortoiseGit\\LogTooManyItemsThreshold", 1000))
232 m_bNoAutoselectMissing = CRegDWORD(L"Software\\TortoiseGit\\AutoselectMissingFiles", FALSE) == TRUE;
234 NONCLIENTMETRICS metrics = { 0 };
235 metrics.cbSize = sizeof(NONCLIENTMETRICS);
236 SystemParametersInfo(SPI_GETNONCLIENTMETRICS, 0, &metrics, FALSE);
237 m_uiFont.CreateFontIndirect(&metrics.lfMessageFont);
239 m_ColumnManager.SetOnVisibilityChanged(
240 [this](int column, bool visible)
242 OnColumnVisibilityChanged(column, visible);
245 m_regKeepChangeLists = CRegDWORD(L"Software\\TortoiseGit\\KeepChangeLists", FALSE);
246 m_bKeepChangeLists = m_regKeepChangeLists;
249 CGitStatusListCtrl::~CGitStatusListCtrl()
251 ClearStatusArray();
254 HWND CGitStatusListCtrl::GetParentHWND()
256 if (m_hwndLogicalParent)
257 return m_hwndLogicalParent->GetSafeHwnd();
258 auto owner = GetSafeOwner();
259 if (!owner)
260 return GetSafeHwnd();
261 return owner->GetSafeHwnd();
264 void CGitStatusListCtrl::ClearStatusArray()
266 #if 0
267 CAutoWriteLock locker(m_guard);
268 for (size_t i = 0; i < m_arStatusArray.size(); ++i)
270 delete m_arStatusArray[i];
272 m_arStatusArray.clear();
273 #endif
276 void CGitStatusListCtrl::Init(DWORD dwColumns, const CString& sColumnInfoContainer, unsigned __int64 dwContextMenus /* = GitSLC_POPALL */, bool bHasCheckboxes /* = true */, bool bHasWC /* = true */, DWORD allowedColumns /* = 0xffffffff */)
278 CAutoWriteLock locker(m_guard);
280 m_dwDefaultColumns = dwColumns | 1;
281 m_dwContextMenus = dwContextMenus;
282 m_bHasCheckboxes = bHasCheckboxes;
283 m_bHasWC = bHasWC;
284 m_bWaitCursor = true;
286 // set the extended style of the listcontrol
287 DWORD exStyle = LVS_EX_DOUBLEBUFFER | LVS_EX_INFOTIP | LVS_EX_SUBITEMIMAGES;
288 exStyle |= (bHasCheckboxes ? LVS_EX_CHECKBOXES : 0);
289 SetRedraw(false);
290 SetExtendedStyle(exStyle);
291 CResizableColumnsListCtrl::Init();
293 SetWindowTheme(m_hWnd, L"Explorer", nullptr);
295 if (CRegDWORD(L"Software\\TortoiseGit\\LogFontForFileListCtrl", FALSE))
297 m_uiFont.DeleteObject();
298 CAppUtils::CreateFontForLogs(GetSafeHwnd(), m_uiFont);
299 SetFont(&m_uiFont);
302 m_nIconFolder = SYS_IMAGE_LIST().GetDirIconIndex();
303 m_nRestoreOvl = SYS_IMAGE_LIST().AddIcon(CCommonAppUtils::LoadIconEx(IDI_RESTOREOVL, 0, 0));
304 SYS_IMAGE_LIST().SetOverlayImage(m_nRestoreOvl, OVL_RESTORE);
305 SetImageList(&SYS_IMAGE_LIST(), LVSIL_SMALL);
307 // keep CSorter::operator() in sync!!
308 static UINT standardColumnNames[GITSLC_NUMCOLUMNS]
309 = { IDS_STATUSLIST_COLFILE
310 , IDS_STATUSLIST_COLFILENAME
311 , IDS_STATUSLIST_COLEXT
312 , IDS_STATUSLIST_COLSTATUS
313 , IDS_STATUSLIST_COLADD
314 , IDS_STATUSLIST_COLDEL
315 , IDS_STATUSLIST_COLLASTMODIFIED
316 , IDS_STATUSLIST_COLSIZE
317 , IDS_STATUSLIST_COLLFSLOCK
320 if (!CTGitPath(g_Git.m_CurrentDir).HasLFS())
321 allowedColumns &= ~GITSLC_COLLFSLOCK;
323 static_assert(_countof(standardColumnNames) == GITSLC_NUMCOLUMNS);
324 m_ColumnManager.SetNames(standardColumnNames,GITSLC_NUMCOLUMNS);
325 constexpr int columnVersion = 7; // adjust when changing number/names/etc. of columns
326 m_ColumnManager.ReadSettings(m_dwDefaultColumns, 0xffffffff & ~(allowedColumns | m_dwDefaultColumns), sColumnInfoContainer, columnVersion, GITSLC_NUMCOLUMNS);
327 m_ColumnManager.SetRightAlign(m_ColumnManager.GetColumnByName(IDS_STATUSLIST_COLADD));
328 m_ColumnManager.SetRightAlign(m_ColumnManager.GetColumnByName(IDS_STATUSLIST_COLDEL));
329 m_ColumnManager.SetRightAlign(m_ColumnManager.GetColumnByName(IDS_STATUSLIST_COLSIZE));
331 // enable file drops
332 if (!m_pDropTarget)
334 m_pDropTarget = std::make_unique<CGitStatusListCtrlDropTarget>(this);
335 RegisterDragDrop(m_hWnd, m_pDropTarget.get());
336 // create the supported formats:
337 FORMATETC ftetc = { 0 };
338 ftetc.dwAspect = DVASPECT_CONTENT;
339 ftetc.lindex = -1;
340 ftetc.tymed = TYMED_HGLOBAL;
341 ftetc.cfFormat = CF_HDROP;
342 m_pDropTarget->AddSuportedFormat(ftetc);
345 UpdateDiffWithFileFromReg();
347 SetRedraw(true);
348 m_bWaitCursor = false;
351 void CGitStatusListCtrl::EnableThreeStateCheckboxes(bool enable)
353 auto stateImageList = GetImageList(LVSIL_STATE);
354 int numStateImageList = stateImageList->GetImageCount();
355 if (!enable)
357 if (m_bThreeStateCheckboxes)
359 VERIFY(numStateImageList == 3);
360 stateImageList->Remove(2);
361 m_bThreeStateCheckboxes = false;
364 else
366 if (!m_bThreeStateCheckboxes)
368 VERIFY(numStateImageList == 2);
369 stateImageList->Add(CCommonAppUtils::LoadIconEx(IDI_INDETERMINATE, 0, 0));
370 m_bThreeStateCheckboxes = true;
375 bool CGitStatusListCtrl::SetBackgroundImage(UINT nID)
377 return CAppUtils::SetListCtrlBackgroundImage(GetSafeHwnd(), nID);
380 BOOL CGitStatusListCtrl::GetStatus ( const CTGitPathList* pathList
381 , bool bUpdate /* = FALSE */
382 , bool bShowIgnores /* = false */
383 , bool bShowUnRev /* = false */
384 , bool bShowLocalChangesIgnored /* = false */
385 , bool bShowLFSLocks /* = false */
386 , bool bGetStagingStatus /* = false */)
388 CAutoWriteLock locker(m_guard);
390 m_bEmpty = false;
391 m_bBusy = true;
392 m_bWaitCursor = true;
393 Invalidate();
395 CTGitPath repo{ g_Git.m_CurrentDir };
396 bool hasLFS = repo.HasLFS();
397 m_bIsRevertTheirMy = repo.IsRebaseActive();
399 int mask= CGitStatusListCtrl::FILELIST_MODIFY;
400 if(bShowIgnores)
401 mask|= CGitStatusListCtrl::FILELIST_IGNORE;
402 if(bShowUnRev)
403 mask|= CGitStatusListCtrl::FILELIST_UNVER;
404 if (bShowLocalChangesIgnored)
405 mask |= CGitStatusListCtrl::FILELIST_LOCALCHANGESIGNORED;
406 if (bShowLFSLocks && hasLFS)
407 mask |= CGitStatusListCtrl::FILELIST_LOCKS;
408 this->UpdateFileList(mask, bUpdate, pathList, bGetStagingStatus);
410 if (!bShowLFSLocks && hasLFS)
412 int id = m_ColumnManager.GetColumnByName(IDS_STATUSLIST_COLLFSLOCK);
413 if (id >= 0 && m_ColumnManager.IsVisible(id))
414 UpdateLFSLockedFileList(true);
417 if (pathList && m_setDirectFiles.empty())
419 // remember files which are selected by users so that those can be preselected
420 for (int i = 0; i < pathList->GetCount(); ++i)
421 if (!(*pathList)[i].IsDirectory())
422 m_setDirectFiles.insert((*pathList)[i].GetGitPathString());
424 LoadChangelists();
426 #if 0
427 int refetchcounter = 0;
428 BOOL bRet = TRUE;
429 Invalidate();
430 // force the cursor to change
431 POINT pt;
432 GetCursorPos(&pt);
433 SetCursorPos(pt.x, pt.y);
435 m_mapFilenameToChecked.clear();
436 //m_StatusUrlList.Clear();
437 bool bHasChangelists = (m_changelists.size() > 1 || (!m_changelists.empty() && !m_bHasIgnoreGroup));
438 m_changelists.clear();
439 for (size_t i=0; i < m_arStatusArray.size(); i++)
441 FileEntry * entry = m_arStatusArray[i];
442 if ( bHasChangelists && entry->checked)
444 // If change lists are present, remember all checked entries
445 CString path = entry->GetPath().GetGitPathString();
446 m_mapFilenameToChecked[path] = true;
448 if ( (entry->status==git_wc_status_unversioned || entry->status==git_wc_status_missing ) && entry->checked )
450 // The user manually selected an unversioned or missing file. We remember
451 // this so that the selection can be restored when refreshing.
452 CString path = entry->GetPath().GetGitPathString();
453 m_mapFilenameToChecked[path] = true;
455 else if ( entry->status > git_wc_status_normal && !entry->checked )
457 // The user manually deselected a versioned file. We remember
458 // this so that the deselection can be restored when refreshing.
459 CString path = entry->GetPath().GetGitPathString();
460 m_mapFilenameToChecked[path] = false;
464 // use a sorted path list to make sure we fetch the status of
465 // parent items first, *then* the child items (if any)
466 CTGitPathList sortedPathList = pathList;
467 sortedPathList.SortByPathname();
470 bRet = TRUE;
471 m_nTargetCount = 0;
472 m_bHasUnversionedItems = FALSE;
473 m_bHasChangeLists = false;
474 m_bShowIgnores = bShowIgnores;
475 m_nSortedColumn = 0;
476 m_bBlock = TRUE;
477 m_bBusy = true;
478 m_bEmpty = false;
479 Invalidate();
481 // first clear possible status data left from
482 // previous GetStatus() calls
483 ClearStatusArray();
485 m_StatusFileList = sortedPathList;
487 // Since Git_client_status() returns all files, even those in
488 // folders included with Git:externals we need to check if all
489 // files we get here belongs to the same repository.
490 // It is possible to commit changes in an external folder, as long
491 // as the folder belongs to the same repository (but another path),
492 // but it is not possible to commit all files if the externals are
493 // from a different repository.
495 // To check if all files belong to the same repository, we compare the
496 // UUID's - if they're identical then the files belong to the same
497 // repository and can be committed. But if they're different, then
498 // tell the user to commit all changes in the external folders
499 // first and exit.
500 CStringA sUUID; // holds the repo UUID
501 CTGitPathList arExtPaths; // list of Git:external paths
503 GitConfig config;
505 m_sURL.Empty();
507 m_nTargetCount = sortedPathList.GetCount();
509 GitStatus status(m_pbCanceled);
510 if(m_nTargetCount > 1 && sortedPathList.AreAllPathsFilesInOneDirectory())
512 // This is a special case, where we've been given a list of files
513 // all from one folder
514 // We handle them by setting a status filter, then requesting the Git status of
515 // all the files in the directory, filtering for the ones we're interested in
516 status.SetFilter(sortedPathList);
518 // if all selected entries are files, we don't do a recursive status
519 // fetching. But if only one is a directory, we have to recurse.
520 git_depth_t depth = git_depth_files;
521 // We have set a filter. If all selected items were files or we fetch
522 // the status not recursively, then we have to include
523 // ignored items too - the user has selected them
524 bool bShowIgnoresRight = true;
525 for (int fcindex=0; fcindex<sortedPathList.GetCount(); ++fcindex)
527 if (sortedPathList[fcindex].IsDirectory())
529 depth = git_depth_infinity;
530 bShowIgnoresRight = false;
531 break;
534 if(!FetchStatusForSingleTarget(config, status, sortedPathList.GetCommonDirectory(), bUpdate, sUUID, arExtPaths, true, depth, bShowIgnoresRight))
535 bRet = FALSE;
537 else
539 for(int nTarget = 0; nTarget < m_nTargetCount; nTarget++)
541 // check whether the path we want the status for is already fetched due to status-fetching
542 // of a parent path.
543 // this check is only done for file paths, because folder paths could be included already
544 // but not recursively
545 if (sortedPathList[nTarget].IsDirectory() || !GetListEntry(sortedPathList[nTarget]))
547 if(!FetchStatusForSingleTarget(config, status, sortedPathList[nTarget], bUpdate, sUUID,
548 arExtPaths, false, git_depth_infinity, bShowIgnores))
550 bRet = FALSE;
556 // remove the 'helper' files of conflicted items from the list.
557 // otherwise they would appear as unversioned files.
558 for (INT_PTR cind = 0; cind < m_ConflictFileList.GetCount(); ++cind)
560 for (size_t i=0; i < m_arStatusArray.size(); i++)
562 if (m_arStatusArray[i]->GetPath().IsEquivalentTo(m_ConflictFileList[cind]))
564 delete m_arStatusArray[i];
565 m_arStatusArray.erase(m_arStatusArray.cbegin() + i);
566 break;
570 refetchcounter++;
571 } while(!BuildStatistics() && (refetchcounter < 2) && (*m_pbCanceled==false));
573 m_bBlock = FALSE;
574 m_bBusy = false;
575 GetCursorPos(&pt);
576 SetCursorPos(pt.x, pt.y);
577 return bRet;
578 #endif
579 m_bBusy = false;
580 m_bWaitCursor = false;
581 BuildStatistics();
582 return TRUE;
585 // Get the show-flags bitmap value which corresponds to a particular Git status
586 DWORD CGitStatusListCtrl::GetShowFlagsFromGitStatus(git_wc_status_kind status)
588 switch (status)
590 case git_wc_status_none:
591 case git_wc_status_unversioned:
592 return GITSLC_SHOWUNVERSIONED;
593 case git_wc_status_ignored:
594 if (!m_bShowIgnores)
595 return GITSLC_SHOWDIRECTS;
596 return GITSLC_SHOWDIRECTS|GITSLC_SHOWIGNORED;
597 case git_wc_status_normal:
598 return GITSLC_SHOWNORMAL;
599 case git_wc_status_added:
600 return GITSLC_SHOWADDED;
601 case git_wc_status_deleted:
602 return GITSLC_SHOWREMOVED;
603 case git_wc_status_modified:
604 return GITSLC_SHOWMODIFIED;
605 case git_wc_status_conflicted:
606 return GITSLC_SHOWCONFLICTED;
607 default:
608 // we should NEVER get here!
609 ASSERT(FALSE);
610 break;
612 return 0;
615 void CGitStatusListCtrl::Show(unsigned int dwShow, unsigned int dwCheck /*=0*/, bool /*bShowFolders*/ /* = true */,BOOL UpdateStatusList,bool UseStoredCheckStatus)
617 m_bWaitCursor = true;
618 m_bBusy = true;
619 m_bEmpty = false;
620 Invalidate();
622 WORD langID = static_cast<WORD>(CRegStdDWORD(L"Software\\TortoiseGit\\LanguageID", GetUserDefaultLangID()));
623 SetRedraw(FALSE);
625 CAutoWriteLock locker(m_guard);
626 DeleteAllItems();
627 m_nSelected = 0;
629 m_nShownUnversioned = 0;
630 m_nShownModified = 0;
631 m_nShownAdded = 0;
632 m_nShownDeleted = 0;
633 m_nShownConflicted = 0;
634 m_nShownFiles = 0;
635 m_nShownSubmodules = 0;
637 m_dwShow = dwShow;
639 if (UpdateStatusList)
641 m_arStatusArray.clear();
642 for (int i = 0; i < m_StatusFileList.GetCount(); ++i)
643 m_arStatusArray.push_back(&m_StatusFileList[i]);
645 for (int i = 0; i < m_UnRevFileList.GetCount(); ++i)
646 m_arStatusArray.push_back(&m_UnRevFileList[i]);
648 for (int i = 0; i < m_IgnoreFileList.GetCount(); ++i)
649 m_arStatusArray.push_back(&m_IgnoreFileList[i]);
651 for (int i = 0; i < m_LocalChangesIgnoredFileList.GetCount(); ++i)
652 m_arStatusArray.push_back(&m_LocalChangesIgnoredFileList[i]);
654 AppendLFSLocks(false);
657 m_bTooManyItems = (m_bHideTooManyItems && m_arStatusArray.size() > m_nTooManyItemsThreshold);
658 if (m_bTooManyItems)
660 m_arListArray.clear();
661 RemoveAllGroups();
663 RestoreScrollPos();
665 m_bWaitCursor = false;
666 m_bBusy = false;
667 m_bEmpty = true;
668 Invalidate();
670 BuildStatistics();
671 return;
674 PrepareGroups();
675 m_arListArray.clear();
676 m_arListArray.reserve(m_arStatusArray.size());
677 if (m_nSortedColumn >= 0)
679 CSorter predicate(&m_ColumnManager, m_nSortedColumn, m_bAscending);
680 std::stable_sort(m_arStatusArray.begin(), m_arStatusArray.end(), predicate);
683 int index = 0;
684 for (size_t i = 0; i < m_arStatusArray.size(); ++i)
686 //set default checkbox status
687 auto entry = const_cast<CTGitPath*>(m_arStatusArray[i]);
688 CString path = entry->GetGitPathString();
689 if (m_bThreeStateCheckboxes)
691 auto stagingStatus = entry->m_stagingStatus;
692 if (stagingStatus == CTGitPath::StagingStatus::TotallyStaged || stagingStatus == CTGitPath::StagingStatus::PartiallyStaged)
693 entry->m_Checked = true;
694 else
695 entry->m_Checked = false;
697 else if (!m_mapFilenameToChecked.empty() && m_mapFilenameToChecked.find(path) != m_mapFilenameToChecked.end())
698 entry->m_Checked = m_mapFilenameToChecked[path];
699 else if (!UseStoredCheckStatus)
701 bool autoSelectSubmodules = !(entry->IsDirectory() && m_bDoNotAutoselectSubmodules);
702 if (((entry->m_Action & dwCheck) &&
703 !(m_bNoAutoselectMissing && entry->m_Action & CTGitPath::LOGACTIONS_MISSING) ||
704 dwShow & GITSLC_SHOWDIRECTFILES && m_setDirectFiles.find(path) != m_setDirectFiles.end()
705 ) && autoSelectSubmodules)
707 auto it = m_pathToChangelist.find(path);
708 if ((it != m_pathToChangelist.end()) && (it->second.Compare(GITSLC_IGNORECHANGELIST) == 0))
709 entry->m_Checked = false; // is in ignore-on-commit
710 else
711 entry->m_Checked = true;
713 else
714 entry->m_Checked = false;
715 m_mapFilenameToChecked[path] = entry->m_Checked;
718 if (entry->m_Action & dwShow)
720 AddEntry(i, entry, langID, index);
721 index++;
726 AdjustColumnWidths();
728 SetRedraw(TRUE);
729 GetStatisticsString();
731 CHeaderCtrl * pHeader = GetHeaderCtrl();
732 HDITEM HeaderItem = {0};
733 HeaderItem.mask = HDI_FORMAT;
734 for (int i=0; i<pHeader->GetItemCount(); ++i)
736 pHeader->GetItem(i, &HeaderItem);
737 HeaderItem.fmt &= ~(HDF_SORTDOWN | HDF_SORTUP);
738 pHeader->SetItem(i, &HeaderItem);
740 if (m_nSortedColumn >= 0)
742 pHeader->GetItem(m_nSortedColumn, &HeaderItem);
743 HeaderItem.fmt |= (m_bAscending ? HDF_SORTUP : HDF_SORTDOWN);
744 pHeader->SetItem(m_nSortedColumn, &HeaderItem);
747 RestoreScrollPos();
749 m_bWaitCursor = false;
750 m_bBusy = false;
751 m_bEmpty = (GetItemCount() == 0);
752 Invalidate();
754 this->BuildStatistics();
756 #if 0
758 m_bShowFolders = bShowFolders;
760 int nTopIndex = GetTopIndex();
761 POSITION posSelectedEntry = GetFirstSelectedItemPosition();
762 int nSelectedEntry = 0;
763 if (posSelectedEntry)
764 nSelectedEntry = GetNextSelectedItem(posSelectedEntry);
765 SetRedraw(FALSE);
766 DeleteAllItems();
768 PrepareGroups();
770 m_arListArray.clear();
772 m_arListArray.reserve(m_arStatusArray.size());
773 SetItemCount (static_cast<int>(m_arStatusArray.size()));
775 int listIndex = 0;
776 for (size_t i=0; i < m_arStatusArray.size(); ++i)
778 FileEntry * entry = m_arStatusArray[i];
779 if ((entry->inexternal) && (!(dwShow & SVNSLC_SHOWINEXTERNALS)))
780 continue;
781 if ((entry->differentrepo || entry->isNested) && (! (dwShow & SVNSLC_SHOWEXTERNALFROMDIFFERENTREPO)))
782 continue;
783 if (entry->IsFolder() && (!bShowFolders))
784 continue; // don't show folders if they're not wanted.
786 #if 0
787 git_wc_status_kind status = GitStatus::GetMoreImportant(entry->status, entry->remotestatus);
788 DWORD showFlags = GetShowFlagsFromGitStatus(status);
789 if (entry->switched)
790 showFlags |= SVNSLC_SHOWSWITCHED;
791 if (!entry->changelist.IsEmpty())
792 showFlags |= SVNSLC_SHOWINCHANGELIST;
793 #endif
794 bool bAllowCheck = ((entry->changelist.Compare(GITSLC_IGNORECHANGELIST) != 0)
795 && (m_bCheckIfGroupsExist || (m_changelists.empty() || (m_changelists.size() == 1 && m_bHasIgnoreGroup))));
797 // status_ignored is a special case - we must have the 'direct' flag set to add a status_ignored item
798 #if 0
799 if (status != Git_wc_status_ignored || (entry->direct) || (dwShow & GitSLC_SHOWIGNORED))
801 if ((!entry->IsFolder()) && (status == Git_wc_status_deleted) && (dwShow & SVNSLC_SHOWREMOVEDANDPRESENT))
803 if (PathFileExists(entry->GetPath().GetWinPath()))
805 m_arListArray.push_back(i);
806 if ((dwCheck & SVNSLC_SHOWREMOVEDANDPRESENT)||((dwCheck & SVNSLC_SHOWDIRECTS)&&(entry->direct)))
808 if (bAllowCheck)
809 entry->checked = true;
811 AddEntry(entry, langID, listIndex++);
814 else if ((dwShow & showFlags)||((dwShow & SVNSLC_SHOWDIRECTFILES)&&(entry->direct)&&(!entry->IsFolder())))
816 m_arListArray.push_back(i);
817 if ((dwCheck & showFlags)||((dwCheck & SVNSLC_SHOWDIRECTS)&&(entry->direct)))
819 if (bAllowCheck)
820 entry->checked = true;
822 AddEntry(entry, langID, listIndex++);
824 else if ((dwShow & showFlags)||((dwShow & SVNSLC_SHOWDIRECTFOLDER)&&(entry->direct)&&entry->IsFolder()))
826 m_arListArray.push_back(i);
827 if ((dwCheck & showFlags)||((dwCheck & SVNSLC_SHOWDIRECTS)&&(entry->direct)))
829 if (bAllowCheck)
830 entry->checked = true;
832 AddEntry(entry, langID, listIndex++);
835 #endif
838 SetItemCount(listIndex);
840 m_ColumnManager.UpdateRelevance (m_arStatusArray, m_arListArray);
842 AdjustColumnWidths();
844 SetRedraw(TRUE);
845 GetStatisticsString();
847 CHeaderCtrl * pHeader = GetHeaderCtrl();
848 HDITEM HeaderItem = {0};
849 HeaderItem.mask = HDI_FORMAT;
850 for (int i=0; i<pHeader->GetItemCount(); ++i)
852 pHeader->GetItem(i, &HeaderItem);
853 HeaderItem.fmt &= ~(HDF_SORTDOWN | HDF_SORTUP);
854 pHeader->SetItem(i, &HeaderItem);
856 if (m_nSortedColumn)
858 pHeader->GetItem(m_nSortedColumn, &HeaderItem);
859 HeaderItem.fmt |= (m_bAscending ? HDF_SORTUP : HDF_SORTDOWN);
860 pHeader->SetItem(m_nSortedColumn, &HeaderItem);
863 if (nSelectedEntry)
865 SetItemState(nSelectedEntry, LVIS_SELECTED, LVIS_SELECTED);
866 EnsureVisible(nSelectedEntry, false);
868 else
870 // Restore the item at the top of the list.
871 for (int i=0;GetTopIndex() != nTopIndex;i++)
873 if ( !EnsureVisible(nTopIndex+i,false) )
874 break;
878 m_bEmpty = (GetItemCount() == 0);
879 Invalidate();
880 #endif
883 void CGitStatusListCtrl::AppendLFSLocks(bool onlyExisting)
885 for (int i = 0; i < m_LocksFileList.GetCount(); ++i)
887 auto gitpath = const_cast<CTGitPath*>(&m_LocksFileList[i]);
888 gitpath->m_Checked = FALSE;
890 bool found = false;
892 for (size_t j = 0; j < m_arStatusArray.size(); ++j)
894 auto gitpath2 = const_cast<CTGitPath*>(m_arStatusArray[j]);
896 if (gitpath2->GetGitPathString() == gitpath->GetGitPathString())
898 gitpath2->m_LFSLockOwner = gitpath->m_LFSLockOwner;
899 gitpath2->m_Action |= gitpath->m_Action;
900 found = true;
901 break;
905 if (!onlyExisting && !found)
906 m_arStatusArray.push_back(gitpath);
910 void CGitStatusListCtrl::StoreScrollPos()
912 m_sScrollPos.enabled = true;
913 GetOrigin(&m_sScrollPos.coordOrigin);
914 m_sScrollPos.selMark = GetSelectionMark();
915 POSITION posSelectedEntry = GetFirstSelectedItemPosition();
916 m_sScrollPos.nSelectedEntry = 0;
917 if (posSelectedEntry)
918 m_sScrollPos.nSelectedEntry = GetNextSelectedItem(posSelectedEntry);
921 void CGitStatusListCtrl::RestoreScrollPos()
923 if (!m_sScrollPos.enabled || CRegDWORD(L"Software\\TortoiseGit\\RememberFileListPosition", TRUE) != TRUE)
924 return;
926 if (m_sScrollPos.nSelectedEntry)
927 SetItemState(m_sScrollPos.nSelectedEntry, LVIS_SELECTED, LVIS_SELECTED);
929 // Restore the item at the top of the list.
930 const CSize scrollSize{m_sScrollPos.coordOrigin.x, m_sScrollPos.coordOrigin.y};
931 Scroll(scrollSize);
933 if (m_sScrollPos.selMark >= 0)
935 SetSelectionMark(m_sScrollPos.selMark);
936 SetItemState(m_sScrollPos.selMark, LVIS_FOCUSED, LVIS_FOCUSED);
938 m_sScrollPos.enabled = false;
941 // This probably should be moved to the commit window
942 void CGitStatusListCtrl::GitStageEntry(CTGitPath* entry)
944 CString cmd, out;
945 cmd.Format(L"git.exe add -- \"%s\"", entry->GetWinPath());
946 if (g_Git.Run(cmd, &out, CP_UTF8))
947 MessageBox(L"Error staging file", L"TortoiseGit", MB_OK | MB_ICONERROR);
950 // This probably should be moved to the commit window
951 void CGitStatusListCtrl::GitUnstageEntry(CTGitPath* entry)
953 CString cmd1, cmd2, out;
954 // git restore --staged would avoid the whole mess below but requires at least git version 2.23
955 if (entry->m_Action & CTGitPath::Actions::LOGACTIONS_ADDED)
956 cmd1.Format(L"git.exe rm -f --cached -- \"%s\"", static_cast<LPCTSTR>(entry->GetWinPathString()));
957 else if (entry->m_Action & CTGitPath::Actions::LOGACTIONS_DELETED)
958 cmd1.Format(L"git.exe reset -- \"%s\"", static_cast<LPCTSTR>(entry->GetWinPathString()));
959 else if (entry->m_Action & CTGitPath::Actions::LOGACTIONS_MODIFIED)
960 cmd1.Format(L"git.exe reset -- \"%s\"", static_cast<LPCTSTR>(entry->GetWinPathString()));
961 else if (entry->m_Action & CTGitPath::Actions::LOGACTIONS_REPLACED)
963 cmd1.Format(L"git.exe rm -f --cached -- \"%s\"", static_cast<LPCTSTR>(entry->GetWinPathString()));
964 cmd2.Format(L"git.exe reset -- \"%s\"", static_cast<LPCTSTR>(entry->GetGitOldPathString()));
966 else
967 return;
969 if (g_Git.Run(cmd1, &out, CP_UTF8))
971 MessageBox(L"Error unstaging file:\n" + out, L"TortoiseGit", MB_OK | MB_ICONERROR);
972 return;
974 if (!cmd2.IsEmpty() && g_Git.Run(cmd2, &out, CP_UTF8))
976 MessageBox(L"Error unstaging file:\n" + out, L"TortoiseGit", MB_OK | MB_ICONERROR);
977 return;
981 void CGitStatusListCtrl::UpdateSelectedFileStagingStatus(CTGitPath::StagingStatus newStatus)
983 CAutoWriteLock locker(m_guard);
984 POSITION pos = GetFirstSelectedItemPosition();
985 if (pos)
987 int nSelect = GetNextSelectedItem(pos);
988 auto p = GetListEntry(nSelect);
989 p->m_stagingStatus = newStatus;
991 ScopedInDecrement blocker(m_nBlockItemChangeHandler);
992 if (newStatus == CTGitPath::StagingStatus::PartiallyStaged)
993 ListView_SetItemState(m_hWnd, nSelect, INDEXTOSTATEIMAGEMASK(3), LVIS_STATEIMAGEMASK)
994 else if (newStatus == CTGitPath::StagingStatus::TotallyStaged)
995 SetCheck(nSelect, true);
996 else if (newStatus == CTGitPath::StagingStatus::TotallyUnstaged)
997 SetCheck(nSelect, false);
1000 //Invalidate();
1004 int CGitStatusListCtrl::GetColumnIndex(int mask)
1006 for (int i = 0; i < 32; ++i)
1007 if(mask&0x1)
1008 return i;
1009 else
1010 mask=mask>>1;
1011 return -1;
1014 CString CGitStatusListCtrl::GetCellText(int listIndex, int column)
1016 static CString from(MAKEINTRESOURCE(IDS_STATUSLIST_FROM));
1017 static bool abbreviateRenamings(static_cast<DWORD>(CRegDWORD(L"Software\\TortoiseGit\\AbbreviateRenamings", FALSE)) == TRUE);
1018 static bool relativeTimes = (CRegDWORD(L"Software\\TortoiseGit\\RelativeTimes", FALSE) != FALSE);
1019 static const CString empty;
1021 CAutoReadLock locker(m_guard);
1022 const auto* entry = GetListEntry(listIndex);
1023 if (!entry)
1024 return empty;
1026 switch (column)
1028 case 0: // relative path
1029 // similar code in FileDiffDlg.cpp::GetFilename
1030 if (!(entry->m_Action & (CTGitPath::LOGACTIONS_REPLACED | CTGitPath::LOGACTIONS_COPY) && !entry->GetGitOldPathString().IsEmpty()))
1031 return entry->GetGitPathString();
1033 if (!abbreviateRenamings)
1035 CString entryname = entry->GetGitPathString();
1036 entryname += L' ';
1037 // relative path
1038 entryname.AppendFormat(from, static_cast<LPCWSTR>(entry->GetGitOldPathString()));
1039 return entryname;
1042 return entry->GetAbbreviatedRename();
1044 case 1: // GITSLC_COLFILENAME
1045 return entry->GetFileOrDirectoryName();
1047 case 2: // GITSLC_COLEXT
1048 return entry->GetFileExtension();
1050 case 3: // GITSLC_COLSTATUS
1051 return entry->GetActionName();
1053 case 4: // GITSLC_COLADD
1054 return entry->m_StatAdd;
1056 case 5: // GITSLC_COLDEL
1057 return entry->m_StatDel;
1059 case 6: // GITSLC_COLMODIFICATIONDATE
1060 if (!(entry->m_Action & CTGitPath::LOGACTIONS_DELETED) && m_ColumnManager.IsRelevant(GetColumnIndex(GITSLC_COLMODIFICATIONDATE)))
1062 __int64 filetime = entry->GetLastWriteTime();
1063 if (filetime)
1064 return CLoglistUtils::FormatDateAndTime(CTime(CGit::filetime_to_time_t(filetime)), DATE_SHORTDATE, true, relativeTimes);
1066 return empty;
1068 case 7: // GITSLC_COLSIZE
1069 if (!(entry->IsDirectory() || !m_ColumnManager.IsRelevant(GetColumnIndex(GITSLC_COLSIZE))))
1071 wchar_t buf[100] = { 0 };
1072 StrFormatByteSize64(entry->GetFileSize(), buf, 100);
1073 return buf;
1075 return empty;
1077 case 8: // GITSLC_COLLFSLOCK
1078 return entry->m_LFSLockOwner;
1080 #if 0
1081 default: // user-defined properties
1082 if (column < m_ColumnManager.GetColumnCount())
1084 assert(m_ColumnManager.IsUserProp(column));
1086 const CString& name = m_ColumnManager.GetName(column);
1087 auto propEntry = m_PropertyMap.find(entry->GetPath());
1088 if (propEntry != m_PropertyMap.end())
1090 if (propEntry->second.HasProperty(name))
1092 const CString& propVal = propEntry->second[name];
1093 return propVal.IsEmpty()
1094 ? m_sNoPropValueText
1095 : propVal;
1099 #endif
1101 return empty;
1104 void CGitStatusListCtrl::AddEntry(size_t arStatusArrayIndex, CTGitPath * GitPath, WORD /*langID*/, int listIndex)
1106 CAutoWriteLock locker(m_guard);
1107 ScopedInDecrement blocker(m_nBlockItemChangeHandler);
1108 CString path = GitPath->GetGitPathString();
1110 // Load the icons *now* so the icons are cached when showing them later in the
1111 // WM_PAINT handler.
1112 // Problem is that (at least on Win10), loading the icons in the WM_PAINT
1113 // handler triggers an OLE operation, which should not happen in WM_PAINT at all
1114 // (see ..\VC\atlmfc\src\mfc\olemsgf.cpp, COleMessageFilter::OnMessagePending() for details about this)
1115 // By loading the icons here, they get cached and the OLE operation won't happen
1116 // later in the WM_PAINT handler.
1117 // This solves the 'hang' which happens in the commit dialog if images are
1118 // shown in the file list.
1119 int icon_idx = 0;
1120 if (GitPath->IsDirectory())
1122 icon_idx = m_nIconFolder;
1123 m_nShownSubmodules++;
1125 else
1127 icon_idx = SYS_IMAGE_LIST().GetPathIconIndex(*GitPath);
1128 m_nShownFiles++;
1130 switch (GitPath->m_Action)
1132 case CTGitPath::LOGACTIONS_ADDED:
1133 case CTGitPath::LOGACTIONS_COPY:
1134 m_nShownAdded++;
1135 break;
1136 case CTGitPath::LOGACTIONS_DELETED:
1137 case CTGitPath::LOGACTIONS_MISSING:
1138 case CTGitPath::LOGACTIONS_DELETED | CTGitPath::LOGACTIONS_MISSING:
1139 m_nShownDeleted++;
1140 break;
1141 case CTGitPath::LOGACTIONS_REPLACED:
1142 case CTGitPath::LOGACTIONS_MODIFIED:
1143 case CTGitPath::LOGACTIONS_MERGED:
1144 m_nShownModified++;
1145 break;
1146 case CTGitPath::LOGACTIONS_UNMERGED:
1147 case CTGitPath::LOGACTIONS_UNMERGED | CTGitPath::LOGACTIONS_ADDED:
1148 case CTGitPath::LOGACTIONS_UNMERGED | CTGitPath::LOGACTIONS_MODIFIED:
1149 m_nShownConflicted++;
1150 break;
1151 case CTGitPath::LOGACTIONS_UNVER:
1152 m_nShownUnversioned++;
1153 break;
1154 default:
1155 m_nShownUnversioned++;
1156 break;
1159 LVITEM lvItem = { 0 };
1160 lvItem.iItem = listIndex;
1161 lvItem.lParam = reinterpret_cast<LPARAM>(GitPath);
1162 lvItem.mask = LVIF_TEXT | LVIF_IMAGE | LVIF_STATE | LVIF_PARAM;
1163 lvItem.pszText = LPSTR_TEXTCALLBACK;
1164 lvItem.stateMask = LVIS_OVERLAYMASK;
1165 if (m_restorepaths.find(GitPath->GetWinPathString()) != m_restorepaths.end())
1166 lvItem.state = INDEXTOOVERLAYMASK(OVL_RESTORE);
1167 lvItem.iImage = icon_idx;
1168 InsertItem(&lvItem);
1170 m_arListArray.push_back(arStatusArrayIndex);
1172 if (m_bThreeStateCheckboxes) // if three-state, display the staging status in the checkboxes
1174 if (GitPath->m_stagingStatus == CTGitPath::StagingStatus::PartiallyStaged)
1175 ListView_SetItemState(m_hWnd, listIndex, INDEXTOSTATEIMAGEMASK(3), LVIS_STATEIMAGEMASK)
1176 else if (GitPath->m_stagingStatus == CTGitPath::StagingStatus::TotallyStaged)
1177 SetCheck(listIndex, true);
1178 else if (GitPath->m_stagingStatus == CTGitPath::StagingStatus::TotallyUnstaged)
1179 SetCheck(listIndex, false);
1181 else
1182 SetCheck(listIndex, GitPath->m_Checked);
1184 if (GitPath->m_Checked)
1185 m_nSelected++;
1187 SetItemGroup(listIndex, GitPath);
1190 int CGitStatusListCtrl::GetChangeListIdForPath(const CTGitPath* GitPath)
1192 auto pathChangelistIt = m_pathToChangelist.find(GitPath->GetGitPathString());
1193 if (pathChangelistIt == m_pathToChangelist.cend())
1194 return -1;
1196 auto it = m_changelists.find(pathChangelistIt->second);
1197 if (it == m_changelists.cend())
1198 return -1;
1200 return it->second;
1203 bool CGitStatusListCtrl::SetItemGroup(int item, const CTGitPath* pGitPath)
1205 CAutoWriteLock locker(m_guard);
1207 int groupindex = GetChangeListIdForPath(pGitPath);
1208 if (groupindex == -1)
1210 if ((pGitPath->m_Action & CTGitPath::LOGACTIONS_SKIPWORKTREE) || (pGitPath->m_Action & CTGitPath::LOGACTIONS_ASSUMEVALID))
1211 groupindex = 3;
1212 else if (pGitPath->m_Action & CTGitPath::LOGACTIONS_IGNORE)
1213 groupindex = 2;
1214 else if (pGitPath->m_Action & CTGitPath::LOGACTIONS_UNVER)
1215 groupindex = 1;
1216 else
1217 groupindex = pGitPath->m_ParentNo & (PARENT_MASK | MERGE_MASK);
1220 if (groupindex < 0)
1221 return false;
1222 LVITEM i = {0};
1223 i.mask = LVIF_GROUPID;
1224 i.iItem = item;
1225 i.iSubItem = 0;
1226 i.iGroupId = groupindex;
1228 return !!SetItem(&i);
1231 void CGitStatusListCtrl::OnHdnItemclick(NMHDR *pNMHDR, LRESULT *pResult)
1233 LPNMHEADER phdr = reinterpret_cast<LPNMHEADER>(pNMHDR);
1234 *pResult = 0;
1236 CAutoReadWeakLock lock(m_guard);
1237 if (!lock.IsAcquired())
1238 return;
1240 if (m_arStatusArray.empty())
1241 return;
1243 if (m_nSortedColumn == phdr->iItem)
1244 m_bAscending = !m_bAscending;
1245 else
1246 m_bAscending = TRUE;
1247 m_nSortedColumn = phdr->iItem;
1248 Show(m_dwShow, 0, m_bShowFolders,false,true);
1251 BOOL CGitStatusListCtrl::OnLvnItemchanged(NMHDR *pNMHDR, LRESULT *pResult)
1253 LPNMLISTVIEW pNMLV = reinterpret_cast<LPNMLISTVIEW>(pNMHDR);
1254 *pResult = 0;
1255 CWnd* pParent = GetLogicalParent();
1256 if (pParent && pParent->GetSafeHwnd())
1257 pParent->SendMessage(GITSLNM_ITEMCHANGED, pNMLV->iItem);
1259 if (m_nBlockItemChangeHandler)
1260 return FALSE;
1262 if ((pNMLV->uNewState==0)||(pNMLV->uNewState & LVIS_SELECTED)||(pNMLV->uNewState & LVIS_FOCUSED))
1263 return FALSE;
1265 CAutoWriteWeakLock writeLock(m_guard);
1266 if (!writeLock.IsAcquired())
1268 NotifyCheck();
1269 return FALSE;
1272 const bool bSelected = !!(ListView_GetItemState(m_hWnd, pNMLV->iItem, LVIS_SELECTED) & LVIS_SELECTED);
1273 const int nListItems = GetItemCount();
1275 // The checkbox rotates its states from the first to the last then wraps around, so we have to handle clicking a checked (2) checkbox to make
1276 // it go back to unckecked (1) state, instead of becoming indeterminate (3).
1277 // CListCtrl doesn't support three-state checkboxes natively, so this has to be a bit hackish.
1278 if (m_bThreeStateCheckboxes && pNMLV->uNewState == INDEXTOSTATEIMAGEMASK(3) && pNMLV->uOldState == INDEXTOSTATEIMAGEMASK(2) && pNMLV->uChanged == LVIF_STATE)
1280 ScopedInDecrement blocker(m_nBlockItemChangeHandler);
1281 ListView_SetItemState(m_hWnd, pNMLV->iItem, INDEXTOSTATEIMAGEMASK(1), LVIS_STATEIMAGEMASK)
1284 // GetCheck would return true when the three-state checkbox is in the indeterminate state as well,
1285 // but this will never happen because of the code above and also because this handler is blocked during AddEntry
1286 if (GetCheck(pNMLV->iItem)) // was the item checked?
1289 ScopedInDecrement blocker(m_nBlockItemChangeHandler);
1290 CheckEntry(pNMLV->iItem, nListItems);
1292 if (bSelected)
1294 ScopedInDecrement blocker(m_nBlockItemChangeHandler);
1295 POSITION pos = GetFirstSelectedItemPosition();
1296 int index;
1297 while ((index = GetNextSelectedItem(pos)) >= 0)
1299 if (index != pNMLV->iItem)
1300 CheckEntry(index, nListItems);
1304 else
1307 ScopedInDecrement blocker(m_nBlockItemChangeHandler);
1308 UncheckEntry(pNMLV->iItem, nListItems);
1310 if (bSelected)
1312 ScopedInDecrement blocker(m_nBlockItemChangeHandler);
1313 POSITION pos = GetFirstSelectedItemPosition();
1314 int index;
1315 while ((index = GetNextSelectedItem(pos)) >= 0)
1317 if (index != pNMLV->iItem)
1318 UncheckEntry(index, nListItems);
1323 GetStatisticsString();
1324 NotifyCheck();
1326 return FALSE;
1329 void CGitStatusListCtrl::CheckEntry(int index, int /*nListItems*/)
1331 CAutoWriteLock locker(m_guard);
1332 auto path = GetListEntry(index);
1333 if (!path)
1334 return;
1335 m_mapFilenameToChecked[path->GetGitPathString()] = true;
1336 SetCheck(index, TRUE);
1337 // if an unversioned item was checked, then we need to check if
1338 // the parent folders are unversioned too. If the parent folders actually
1339 // are unversioned, then check those too.
1340 #if 0
1341 if (entry->status == git_wc_status_unversioned)
1343 // we need to check the parent folder too
1344 const CTGitPath& folderpath = entry->path;
1345 for (int i=0; i< nListItems; ++i)
1347 FileEntry * testEntry = GetListEntry(i);
1348 ASSERT(testEntry);
1349 if (!testEntry)
1350 continue;
1351 if (!testEntry->checked)
1353 if (testEntry->path.IsAncestorOf(folderpath) && (!testEntry->path.IsEquivalentTo(folderpath)))
1355 SetEntryCheck(testEntry,i,true);
1356 m_nSelected++;
1361 bool bShift = !!(GetAsyncKeyState(VK_SHIFT) & 0x8000);
1362 if ( (entry->status == git_wc_status_deleted) || (m_bCheckChildrenWithParent) || (bShift) )
1364 // if a deleted folder gets checked, we have to check all
1365 // children of that folder too.
1366 if (entry->path.IsDirectory())
1367 SetCheckOnAllDescendentsOf(entry, true);
1369 // if a deleted file or folder gets checked, we have to
1370 // check all parents of this item too.
1371 for (int i=0; i<nListItems; ++i)
1373 FileEntry * testEntry = GetListEntry(i);
1374 ASSERT(testEntry);
1375 if (!testEntry)
1376 continue;
1377 if (!testEntry->checked)
1379 if (testEntry->path.IsAncestorOf(entry->path) && (!testEntry->path.IsEquivalentTo(entry->path)))
1381 if ((testEntry->status == git_wc_status_deleted)||(m_bCheckChildrenWithParent))
1383 SetEntryCheck(testEntry,i,true);
1384 m_nSelected++;
1385 // now we need to check all children of this parent folder
1386 SetCheckOnAllDescendentsOf(testEntry, true);
1392 #endif
1393 if ( !path->m_Checked )
1395 path->m_Checked = TRUE;
1396 m_nSelected++;
1398 if (m_bThreeStateCheckboxes)
1400 GitStageEntry(path);
1401 path->m_stagingStatus = CTGitPath::StagingStatus::TotallyStaged;
1405 void CGitStatusListCtrl::UncheckEntry(int index, int /*nListItems*/)
1407 CAutoWriteLock locker(m_guard);
1408 auto path = GetListEntry(index);
1409 if (!path)
1410 return;
1411 SetCheck(index, FALSE);
1412 m_mapFilenameToChecked[path->GetGitPathString()] = false;
1413 // item was unchecked
1414 #if 0
1415 if (entry->path.IsDirectory())
1417 // disable all files within an unselected folder, except when unchecking a folder with property changes
1418 bool bShift = !!(GetAsyncKeyState(VK_SHIFT) & 0x8000);
1419 if ( (entry->status != git_wc_status_modified) || (bShift) )
1421 SetCheckOnAllDescendentsOf(entry, false);
1424 else if (entry->status == git_wc_status_deleted)
1426 // a "deleted" file was unchecked, so uncheck all parent folders
1427 // and all children of those parents
1428 for (int i=0; i<nListItems; i++)
1430 FileEntry * testEntry = GetListEntry(i);
1431 ASSERT(testEntry);
1432 if (!testEntry)
1433 continue;
1434 if (testEntry->checked)
1436 if (testEntry->path.IsAncestorOf(entry->path))
1438 if (testEntry->status == git_wc_status_deleted)
1440 SetEntryCheck(testEntry,i,false);
1441 m_nSelected--;
1443 SetCheckOnAllDescendentsOf(testEntry, false);
1449 #endif
1450 if ( path->m_Checked )
1452 path->m_Checked = FALSE;
1453 m_nSelected--;
1455 if (m_bThreeStateCheckboxes)
1457 GitUnstageEntry(path);
1458 path->m_stagingStatus = CTGitPath::StagingStatus::TotallyUnstaged;
1461 void CGitStatusListCtrl::BuildStatistics()
1463 CAutoReadLock locker(m_guard);
1465 // now gather some statistics
1466 m_nUnversioned = 0;
1467 m_nNormal = 0;
1468 m_nModified = 0;
1469 m_nAdded = 0;
1470 m_nDeleted = 0;
1471 m_nConflicted = 0;
1472 m_nTotal = 0;
1473 m_nSelected = 0;
1474 m_nLineAdded = 0;
1475 m_nLineDeleted = 0;
1476 m_nRenamed = 0;
1478 for (size_t i = 0; i < m_arStatusArray.size(); ++i)
1480 int status = m_arStatusArray[i]->m_Action;
1482 m_nLineAdded += _wtol(m_arStatusArray[i]->m_StatAdd);
1483 m_nLineDeleted += _wtol(m_arStatusArray[i]->m_StatDel);
1485 if(status&(CTGitPath::LOGACTIONS_ADDED|CTGitPath::LOGACTIONS_COPY))
1486 m_nAdded++;
1488 if(status&CTGitPath::LOGACTIONS_DELETED)
1489 m_nDeleted++;
1491 if(status&(CTGitPath::LOGACTIONS_REPLACED|CTGitPath::LOGACTIONS_MODIFIED))
1492 m_nModified++;
1494 if(status&CTGitPath::LOGACTIONS_UNMERGED)
1495 m_nConflicted++;
1497 if(status&(CTGitPath::LOGACTIONS_IGNORE|CTGitPath::LOGACTIONS_UNVER))
1498 m_nUnversioned++;
1500 if(status&(CTGitPath::LOGACTIONS_REPLACED))
1501 m_nRenamed++;
1503 if (m_arStatusArray[i]->m_Checked)
1504 m_nSelected++;
1508 int CGitStatusListCtrl::GetGroupFromPoint(POINT * ppt)
1510 // the point must be relative to the upper left corner of the control
1512 if (!ppt)
1513 return -1;
1514 if (!IsGroupViewEnabled())
1515 return -1;
1517 POINT pt = *ppt;
1518 pt.x = 10;
1519 UINT flags = 0;
1520 int nItem = -1;
1521 RECT rc;
1522 GetWindowRect(&rc);
1523 while (((flags & LVHT_BELOW) == 0)&&(pt.y < rc.bottom))
1525 nItem = HitTest(pt, &flags);
1526 if ((flags & LVHT_ONITEM)||(flags & LVHT_EX_GROUP_HEADER))
1528 // the first item below the point
1530 // check if the point is too much right (i.e. if the point
1531 // is farther to the right than the width of the item)
1532 RECT r;
1533 GetItemRect(nItem, &r, LVIR_LABEL);
1534 if (ppt->x > r.right)
1535 return -1;
1537 LVITEM lv = {0};
1538 lv.mask = LVIF_GROUPID;
1539 lv.iItem = nItem;
1540 GetItem(&lv);
1541 int groupID = lv.iGroupId;
1542 // now we search upwards and check if the item above this one
1543 // belongs to another group. If it belongs to the same group,
1544 // we're not over a group header
1545 while (pt.y >= 0)
1547 pt.y -= 2;
1548 nItem = HitTest(pt, &flags);
1549 if ((flags & LVHT_ONITEM)&&(nItem >= 0))
1551 // the first item below the point
1552 LVITEM lv2 = {0};
1553 lv2.mask = LVIF_GROUPID;
1554 lv2.iItem = nItem;
1555 GetItem(&lv2);
1556 if (lv2.iGroupId != groupID)
1557 return groupID;
1558 else
1559 return -1;
1562 if (pt.y < 0)
1563 return groupID;
1564 return -1;
1566 pt.y += 2;
1568 return -1;
1571 void CGitStatusListCtrl::OnContextMenuGroup(CWnd * /*pWnd*/, CPoint point)
1573 POINT clientpoint = point;
1574 ScreenToClient(&clientpoint);
1575 if ((IsGroupViewEnabled())&&(GetGroupFromPoint(&clientpoint) >= 0))
1577 CAutoReadWeakLock readLock(m_guard);
1578 if (!readLock.IsAcquired())
1579 return;
1581 CMenu popup;
1582 if (popup.CreatePopupMenu())
1584 CString temp;
1585 temp.LoadString(IDS_STATUSLIST_CHECKGROUP);
1586 popup.AppendMenu(MF_STRING | MF_ENABLED, IDGITLC_CHECKGROUP, temp);
1587 temp.LoadString(IDS_STATUSLIST_UNCHECKGROUP);
1588 popup.AppendMenu(MF_STRING | MF_ENABLED, IDGITLC_UNCHECKGROUP, temp);
1589 int cmd = popup.TrackPopupMenu(TPM_RETURNCMD | TPM_LEFTALIGN | TPM_NONOTIFY, point.x, point.y, this);
1590 bool bCheck = false;
1591 switch (cmd)
1593 case IDGITLC_CHECKGROUP:
1594 bCheck = true;
1595 [[fallthrough]];
1596 case IDGITLC_UNCHECKGROUP:
1598 int group = GetGroupFromPoint(&clientpoint);
1599 // go through all items and check/uncheck those assigned to the group
1600 // but block the OnLvnItemChanged handler
1601 for (int i=0; i<GetItemCount(); ++i)
1603 LVITEM lv = { 0 };
1604 lv.mask = LVIF_GROUPID;
1605 lv.iItem = i;
1606 GetItem(&lv);
1608 if (lv.iGroupId == group)
1610 auto entry = GetListEntry(i);
1611 if (entry)
1613 bool bOldCheck = entry->m_Checked;
1614 SetEntryCheck(entry, i, bCheck);
1615 if (bCheck != bOldCheck)
1617 if (bCheck)
1618 m_nSelected++;
1619 else
1620 m_nSelected--;
1626 GetStatisticsString();
1627 NotifyCheck();
1629 break;
1635 void CGitStatusListCtrl::OnContextMenuList(CWnd * pWnd, CPoint point)
1637 bool bShift = !!(GetAsyncKeyState(VK_SHIFT) & 0x8000);
1639 CAutoWriteWeakLock writeLock(m_guard);
1640 if (!writeLock.IsAcquired())
1641 return;
1643 auto selectedCount = GetSelectedCount();
1644 int selIndex = GetSelectionMark();
1645 int selSubitem = -1;
1646 if (selectedCount > 0)
1648 CPoint pt = point;
1649 ScreenToClient(&pt);
1650 LVHITTESTINFO hittest = { 0 };
1651 hittest.flags = LVHT_ONITEM;
1652 hittest.pt = pt;
1653 if (this->SubItemHitTest(&hittest) >= 0)
1654 selSubitem = hittest.iSubItem;
1656 if ((point.x == -1) && (point.y == -1))
1658 CRect rect;
1659 GetItemRect(selIndex, &rect, LVIR_LABEL);
1660 ClientToScreen(&rect);
1661 point = rect.CenterPoint();
1663 if (selectedCount == 0 && m_bHasCheckboxes)
1665 // nothing selected could mean the context menu is requested for
1666 // a group header
1667 OnContextMenuGroup(pWnd, point);
1669 else if (selIndex >= 0)
1671 auto filepath = GetListEntry(selIndex);
1672 if (!filepath)
1673 return;
1675 //const CTGitPath& filepath = entry->path;
1676 int wcStatus = filepath->m_Action;
1677 // entry is selected, now show the popup menu
1678 CIconMenu popup;
1679 CMenu changelistSubMenu;
1680 CMenu ignoreSubMenu;
1681 CIconMenu clipSubMenu;
1682 CMenu shellMenu;
1683 if (popup.CreatePopupMenu())
1685 //Add Menu for version controlled file
1687 if (selectedCount > 0 && (wcStatus & CTGitPath::LOGACTIONS_UNMERGED))
1689 if (selectedCount == 1 && (m_dwContextMenus & GITSLC_POPCONFLICT))
1691 popup.AppendMenuIcon(IDGITLC_EDITCONFLICT, IDS_MENUCONFLICT, IDI_CONFLICT);
1692 popup.SetDefaultItem(IDGITLC_EDITCONFLICT, FALSE);
1694 if (m_dwContextMenus & GITSLC_POPRESOLVE)
1696 popup.AppendMenuIcon(IDGITLC_RESOLVECONFLICT, IDS_STATUSLIST_CONTEXT_RESOLVED, IDI_RESOLVE);
1697 CString tmp, mineTitle, theirsTitle;
1698 CAppUtils::GetConflictTitles(nullptr, mineTitle, nullptr, theirsTitle, nullptr, m_bIsRevertTheirMy);
1699 tmp.Format(IDS_SVNPROGRESS_MENURESOLVEUSING, static_cast<LPCWSTR>(theirsTitle));
1700 if (m_bIsRevertTheirMy)
1702 popup.AppendMenuIcon(IDGITLC_RESOLVEMINE, tmp, IDI_RESOLVE);
1703 tmp.Format(IDS_SVNPROGRESS_MENURESOLVEUSING, static_cast<LPCWSTR>(mineTitle));
1704 popup.AppendMenuIcon(IDGITLC_RESOLVETHEIRS, tmp, IDI_RESOLVE);
1706 else
1708 popup.AppendMenuIcon(IDGITLC_RESOLVETHEIRS, tmp, IDI_RESOLVE);
1709 tmp.Format(IDS_SVNPROGRESS_MENURESOLVEUSING, static_cast<LPCWSTR>(mineTitle));
1710 popup.AppendMenuIcon(IDGITLC_RESOLVEMINE, tmp, IDI_RESOLVE);
1713 if ((m_dwContextMenus & GITSLC_POPCONFLICT)||(m_dwContextMenus & GITSLC_POPRESOLVE))
1714 popup.AppendMenu(MF_SEPARATOR);
1717 if (selectedCount > 0)
1719 if (wcStatus & (CTGitPath::LOGACTIONS_UNVER | CTGitPath::LOGACTIONS_IGNORE))
1721 if (m_dwContextMenus & GITSLC_POPADD)
1723 popup.AppendMenuIcon(IDGITLC_ADD, IDS_STATUSLIST_CONTEXT_ADD, IDI_ADD);
1724 if (!filepath->IsDirectory() && bShift)
1726 popup.AppendMenuIcon(IDGITLC_ADD_EXE, IDS_STATUSLIST_CONTEXT_ADD_EXE, IDI_ADD);
1727 popup.AppendMenuIcon(IDGITLC_ADD_LINK, IDS_STATUSLIST_CONTEXT_ADD_LINK, IDI_ADD);
1730 if (m_dwContextMenus & GITSLC_POPCOMMIT)
1731 popup.AppendMenuIcon(IDGITLC_COMMIT, IDS_STATUSLIST_CONTEXT_COMMIT, IDI_COMMIT);
1735 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)
1737 bool bEntryAdded = false;
1738 if (m_dwContextMenus & GITSLC_POPCOMPAREWITHBASE)
1740 if(filepath->m_ParentNo & MERGE_MASK)
1741 popup.AppendMenuIcon(IDGITLC_COMPARE, IDS_TREE_DIFF, IDI_DIFF);
1742 else
1743 popup.AppendMenuIcon(IDGITLC_COMPARE, IDS_LOG_COMPAREWITHBASE, IDI_DIFF);
1745 if (!(wcStatus & (CTGitPath::LOGACTIONS_UNMERGED)) || selectedCount != 1)
1746 popup.SetDefaultItem(IDGITLC_COMPARE, FALSE);
1747 bEntryAdded = true;
1750 if (!g_Git.IsInitRepos() && (m_dwContextMenus & GITSLC_POPGNUDIFF))
1752 popup.AppendMenuIcon(IDGITLC_GNUDIFF1, IDS_LOG_POPUP_GNUDIFF, IDI_DIFF);
1753 bEntryAdded = true;
1756 if ((m_dwContextMenus & this->GetContextMenuBit(IDGITLC_COMPAREWC)) && m_bHasWC)
1758 if (!m_CurrentVersion.IsEmpty())
1760 popup.AppendMenuIcon(IDGITLC_COMPAREWC, IDS_LOG_POPUP_COMPARE, IDI_DIFF);
1762 CGitHash parentHash;
1763 CString title;
1764 if (GetParentCommitInfo(m_CurrentVersion, filepath->m_ParentNo & PARENT_MASK, parentHash, title))
1766 CString str;
1767 str.LoadString(IDS_LOG_POPUP_COMPARE_PARENT_WC);
1768 popup.AppendMenuIcon(IDGITLC_COMPAREPARENTWC, str + L": " + title, IDI_DIFF);
1770 bEntryAdded = true;
1774 if (bEntryAdded)
1775 popup.AppendMenu(MF_SEPARATOR);
1778 if (!m_Rev1.IsEmpty() && !m_Rev2.IsEmpty())
1780 if (m_dwContextMenus & this->GetContextMenuBit(IDGITLC_COMPARETWOREVISIONS))
1782 popup.AppendMenuIcon(IDGITLC_COMPARETWOREVISIONS, IDS_LOG_POPUP_COMPARETWO, IDI_DIFF);
1783 popup.SetDefaultItem(IDGITLC_COMPARETWOREVISIONS, FALSE);
1785 if (m_dwContextMenus & this->GetContextMenuBit(IDGITLC_GNUDIFF2REVISIONS))
1786 popup.AppendMenuIcon(IDGITLC_GNUDIFF2REVISIONS, IDS_LOG_POPUP_GNUDIFF, IDI_DIFF);
1789 //Select Multi item
1790 if (selectedCount > 0)
1792 if (selectedCount == 2 && (m_dwContextMenus & GITSLC_POPCOMPARETWOFILES))
1794 POSITION pos = GetFirstSelectedItemPosition();
1795 int index = GetNextSelectedItem(pos);
1796 if (index >= 0)
1798 auto entry2 = GetListEntry(index);
1799 bool firstEntryExistsAndIsFile = entry2 && !entry2->IsDirectory();
1800 index = GetNextSelectedItem(pos);
1801 if (index >= 0)
1803 entry2 = GetListEntry(index);
1804 if (firstEntryExistsAndIsFile && entry2 && !entry2->IsDirectory())
1805 popup.AppendMenuIcon(IDGITLC_COMPARETWOFILES, IDS_STATUSLIST_CONTEXT_COMPARETWOFILES, IDI_DIFF);
1811 if (selectedCount > 0 && (!(wcStatus & (CTGitPath::LOGACTIONS_UNVER | CTGitPath::LOGACTIONS_IGNORE))) && m_bHasWC)
1813 if ((m_dwContextMenus & GITSLC_POPCOMMIT) && m_CurrentVersion.IsEmpty() && !(wcStatus & (CTGitPath::LOGACTIONS_SKIPWORKTREE | CTGitPath::LOGACTIONS_ASSUMEVALID)))
1814 popup.AppendMenuIcon(IDGITLC_COMMIT, IDS_STATUSLIST_CONTEXT_COMMIT, IDI_COMMIT);
1816 if ((m_dwContextMenus & GITSLC_POPREVERT) && m_CurrentVersion.IsEmpty())
1817 popup.AppendMenuIcon(IDGITLC_REVERT, IDS_MENUREVERT, IDI_REVERT);
1819 if ((m_dwContextMenus & GITSLC_POPSKIPWORKTREE) && m_CurrentVersion.IsEmpty() && !(wcStatus & (CTGitPath::LOGACTIONS_ADDED | CTGitPath::LOGACTIONS_UNMERGED | CTGitPath::LOGACTIONS_SKIPWORKTREE)))
1820 popup.AppendMenuIcon(IDGITLC_SKIPWORKTREE, IDS_STATUSLIST_SKIPWORKTREE);
1822 if ((m_dwContextMenus & GITSLC_POPASSUMEVALID) && m_CurrentVersion.IsEmpty() && !(wcStatus & (CTGitPath::LOGACTIONS_ADDED | CTGitPath::LOGACTIONS_DELETED | CTGitPath::LOGACTIONS_UNMERGED | CTGitPath::LOGACTIONS_ASSUMEVALID)))
1823 popup.AppendMenuIcon(IDGITLC_ASSUMEVALID, IDS_MENUASSUMEVALID);
1825 if ((m_dwContextMenus & GITSLC_POPUNSETIGNORELOCALCHANGES) && m_CurrentVersion.IsEmpty() && (wcStatus & (CTGitPath::LOGACTIONS_SKIPWORKTREE | CTGitPath::LOGACTIONS_ASSUMEVALID)))
1826 popup.AppendMenuIcon(IDGITLC_UNSETIGNORELOCALCHANGES, IDS_STATUSLIST_UNSETIGNORELOCALCHANGES);
1828 if (m_dwContextMenus & GITSLC_POPRESTORE && !filepath->IsDirectory())
1830 if (m_restorepaths.find(filepath->GetWinPathString()) == m_restorepaths.end())
1831 popup.AppendMenuIcon(IDGITLC_CREATERESTORE, IDS_MENUCREATERESTORE, IDI_RESTORE);
1832 else
1833 popup.AppendMenuIcon(IDGITLC_RESTOREPATH, IDS_MENURESTORE, IDI_RESTORE);
1836 if ((m_dwContextMenus & (GITSLC_POPLFSLOCK | GITSLC_POPLFSUNLOCK)) && m_CurrentVersion.IsEmpty() && !(wcStatus & CTGitPath::LOGACTIONS_UNMERGED))
1837 AppendLocksMenuItems(popup);
1839 if ((m_dwContextMenus & GetContextMenuBit(IDGITLC_REVERTTOREV)) && !m_CurrentVersion.IsEmpty() && !(wcStatus & CTGitPath::LOGACTIONS_DELETED))
1841 popup.AppendMenuIcon(IDGITLC_REVERTTOREV, IDS_LOG_POPUP_REVERTTOREV, IDI_REVERT);
1844 if ((m_dwContextMenus & GetContextMenuBit(IDGITLC_REVERTTOPARENT)) && !m_CurrentVersion.IsEmpty())
1846 CGitHash parentHash;
1847 CString title;
1848 if (GetParentCommitInfo(m_CurrentVersion, filepath->m_ParentNo & PARENT_MASK, parentHash, title))
1850 CString str;
1851 str.LoadString(IDS_LOG_POPUP_REVERTTOPARENT);
1852 popup.AppendMenuIcon(IDGITLC_REVERTTOPARENT, str + L": " + title, IDI_REVERT);
1857 if (selectedCount == 1 && !(wcStatus & (CTGitPath::LOGACTIONS_UNVER | CTGitPath::LOGACTIONS_IGNORE)))
1859 if (m_dwContextMenus & GITSLC_POPSHOWLOG)
1860 popup.AppendMenuIcon(IDGITLC_LOG, IDS_REPOBROWSE_SHOWLOG, IDI_LOG);
1861 if (m_dwContextMenus & GITSLC_POPSHOWLOGSUBMODULE && filepath->IsDirectory())
1862 popup.AppendMenuIcon(IDGITLC_LOGSUBMODULE, IDS_LOG_SUBMODULE, IDI_LOG);
1863 if (m_dwContextMenus & GITSLC_POPSHOWLOGOLDNAME && (wcStatus & (CTGitPath::LOGACTIONS_REPLACED|CTGitPath::LOGACTIONS_COPY) && !filepath->GetGitOldPathString().IsEmpty()))
1864 popup.AppendMenuIcon(IDGITLC_LOGOLDNAME, IDS_STATUSLIST_SHOWLOGOLDNAME, IDI_LOG);
1865 if ((m_dwContextMenus & GITSLC_POPBLAME) && !filepath->IsDirectory() && !(wcStatus & CTGitPath::LOGACTIONS_DELETED) && !((wcStatus & CTGitPath::LOGACTIONS_ADDED) && m_CurrentVersion.IsEmpty()) && m_bHasWC)
1866 popup.AppendMenuIcon(IDGITLC_BLAME, IDS_MENUBLAME, IDI_BLAME);
1869 if (selectedCount > 0)
1871 if ((m_dwContextMenus & GetContextMenuBit(IDGITLC_EXPORT)) && !(wcStatus & (CTGitPath::LOGACTIONS_DELETED | CTGitPath::LOGACTIONS_MISSING)))
1872 popup.AppendMenuIcon(IDGITLC_EXPORT, IDS_LOG_POPUP_EXPORT, IDI_EXPORT);
1875 if (selectedCount == 1)
1877 if (m_dwContextMenus & this->GetContextMenuBit(IDGITLC_SAVEAS) && ! filepath->IsDirectory() && !(wcStatus & CTGitPath::LOGACTIONS_DELETED))
1878 popup.AppendMenuIcon(IDGITLC_SAVEAS, IDS_LOG_POPUP_SAVE, IDI_SAVEAS);
1880 if (m_dwContextMenus & GITSLC_POPOPEN && !filepath->IsDirectory() && !(wcStatus & (CTGitPath::LOGACTIONS_DELETED | CTGitPath::LOGACTIONS_MISSING)))
1882 popup.AppendMenuIcon(IDGITLC_VIEWREV, IDS_LOG_POPUP_VIEWREV, IDI_NOTEPAD);
1883 popup.AppendMenuIcon(IDGITLC_OPEN, IDS_REPOBROWSE_OPEN, IDI_OPEN);
1884 popup.AppendMenuIcon(IDGITLC_OPENWITH, IDS_LOG_POPUP_OPENWITH, IDI_OPEN);
1887 if (m_dwContextMenus & GITSLC_POPEXPLORE && !(wcStatus & CTGitPath::LOGACTIONS_DELETED) && m_bHasWC)
1888 popup.AppendMenuIcon(IDGITLC_EXPLORE, IDS_STATUSLIST_CONTEXT_EXPLORE, IDI_EXPLORER);
1890 if (m_dwContextMenus & GITSLC_POPPREPAREDIFF && !(wcStatus & CTGitPath::LOGACTIONS_DELETED))
1892 popup.AppendMenu(MF_SEPARATOR);
1893 popup.AppendMenuIcon(IDGITLC_PREPAREDIFF, IDS_PREPAREDIFF, IDI_DIFF);
1894 UpdateDiffWithFileFromReg();
1895 if (!m_sMarkForDiffFilename.IsEmpty())
1897 CString diffWith;
1898 if (filepath->GetGitPathString() == m_sMarkForDiffFilename)
1899 diffWith = m_sMarkForDiffVersion;
1900 else
1902 PathCompactPathEx(CStrBuf(diffWith, 2 * GIT_HASH_SIZE), m_sMarkForDiffFilename, 2 * GIT_HASH_SIZE, 0);
1903 if (m_sMarkForDiffVersion != GitRev::GetWorkingCopy() || PathIsRelative(m_sMarkForDiffFilename))
1904 diffWith += L':' + m_sMarkForDiffVersion.Left(g_Git.GetShortHASHLength());
1906 CString menuEntry;
1907 menuEntry.Format(IDS_MENUDIFFNOW, static_cast<LPCWSTR>(diffWith));
1908 popup.AppendMenuIcon(IDGITLC_PREPAREDIFF_COMPARE, menuEntry, IDI_DIFF);
1912 if (selectedCount > 0)
1914 // if (((wcStatus == git_wc_status_unversioned)||(wcStatus == git_wc_status_ignored))&&(m_dwContextMenus & SVNSLC_POPDELETE))
1915 // popup.AppendMenuIcon(IDSVNLC_DELETE, IDS_MENUREMOVE, IDI_DELETE);
1916 // 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))
1917 // {
1918 // if (bShift)
1919 // popup.AppendMenuIcon(IDGitLC_REMOVE, IDS_MENUREMOVEKEEP, IDI_DELETE);
1920 // else
1921 // popup.AppendMenuIcon(IDGitLC_REMOVE, IDS_MENUREMOVE, IDI_DELETE);
1922 // }
1923 if ((wcStatus & (CTGitPath::LOGACTIONS_UNVER | CTGitPath::LOGACTIONS_IGNORE | CTGitPath::LOGACTIONS_MISSING))/*||(wcStatus == git_wc_status_deleted)*/)
1925 if (m_dwContextMenus & GITSLC_POPDELETE)
1926 popup.AppendMenuIcon(IDGITLC_DELETE, IDS_MENUREMOVE, IDI_DELETE);
1928 if ( (wcStatus & CTGitPath::LOGACTIONS_UNVER || wcStatus & CTGitPath::LOGACTIONS_DELETED) )
1930 if (m_dwContextMenus & GITSLC_POPIGNORE)
1932 CTGitPathList ignorelist;
1933 FillListOfSelectedItemPaths(ignorelist);
1934 //check if all selected entries have the same extension
1935 bool bSameExt = true;
1936 CString sExt;
1937 for (int i=0; i<ignorelist.GetCount(); ++i)
1939 if (sExt.IsEmpty() && (i==0))
1940 sExt = ignorelist[i].GetFileExtension();
1941 else if (sExt.CompareNoCase(ignorelist[i].GetFileExtension())!=0)
1942 bSameExt = false;
1944 if (bSameExt)
1946 if (ignoreSubMenu.CreateMenu())
1948 CString ignorepath;
1949 if (ignorelist.GetCount()==1)
1950 ignorepath = ignorelist[0].GetFileOrDirectoryName();
1951 else
1952 ignorepath.Format(IDS_MENUIGNOREMULTIPLE, ignorelist.GetCount());
1953 ignoreSubMenu.AppendMenu(MF_STRING | MF_ENABLED, IDGITLC_IGNORE, ignorepath);
1954 if (!sExt.IsEmpty())
1956 ignorepath = L'*' + sExt;
1957 ignoreSubMenu.AppendMenu(MF_STRING | MF_ENABLED, IDGITLC_IGNOREMASK, ignorepath);
1959 if (ignorelist.GetCount() == 1 && !ignorelist[0].GetContainingDirectory().GetGitPathString().IsEmpty())
1960 ignoreSubMenu.AppendMenu(MF_STRING | MF_ENABLED, IDGITLC_IGNOREFOLDER, ignorelist[0].GetContainingDirectory().GetGitPathString());
1961 CString temp;
1962 temp.LoadString(IDS_MENUIGNORE);
1963 popup.InsertMenu(static_cast<UINT>(-1), MF_BYPOSITION | MF_POPUP, reinterpret_cast<UINT_PTR>(ignoreSubMenu.m_hMenu), temp);
1966 else
1968 CString temp;
1969 if (ignorelist.GetCount()==1)
1970 temp.LoadString(IDS_MENUIGNORE);
1971 else
1972 temp.Format(IDS_MENUIGNOREMULTIPLE, ignorelist.GetCount());
1973 popup.AppendMenuIcon(IDGITLC_IGNORE, temp, IDI_IGNORE);
1974 if (!sExt.IsEmpty())
1976 temp.Format(IDS_MENUIGNOREMULTIPLEMASK, ignorelist.GetCount());
1977 popup.AppendMenuIcon(IDGITLC_IGNOREMASK, temp, IDI_IGNORE);
1986 if (selectedCount > 0)
1988 popup.AppendMenu(MF_SEPARATOR);
1990 if (clipSubMenu.CreatePopupMenu())
1992 CString temp;
1993 clipSubMenu.AppendMenuIcon(IDGITLC_COPYFULL, IDS_STATUSLIST_CONTEXT_COPYFULLPATHS, IDI_COPYCLIP);
1994 clipSubMenu.AppendMenuIcon(IDGITLC_COPYRELPATHS, IDS_STATUSLIST_CONTEXT_COPYRELPATHS, IDI_COPYCLIP);
1995 clipSubMenu.AppendMenuIcon(IDGITLC_COPYFILENAMES, IDS_STATUSLIST_CONTEXT_COPYFILENAMES, IDI_COPYCLIP);
1996 clipSubMenu.AppendMenuIcon(IDGITLC_COPYEXT, IDS_STATUSLIST_CONTEXT_COPYEXT, IDI_COPYCLIP);
1997 if (selSubitem >= 0)
1999 temp.Format(IDS_STATUSLIST_CONTEXT_COPYCOL, static_cast<LPCWSTR>(m_ColumnManager.GetName(selSubitem)));
2000 clipSubMenu.AppendMenuIcon(IDGITLC_COPYCOL, temp, IDI_COPYCLIP);
2002 temp.LoadString(IDS_LOG_POPUP_COPYTOCLIPBOARD);
2003 popup.InsertMenu(static_cast<UINT>(-1), MF_BYPOSITION | MF_POPUP, reinterpret_cast<UINT_PTR>(clipSubMenu.m_hMenu), temp);
2006 if ((m_dwContextMenus & GITSLC_POPCHANGELISTS)
2007 &&(wcStatus != git_wc_status_unversioned)&&(wcStatus != git_wc_status_none))
2009 popup.AppendMenu(MF_SEPARATOR);
2010 // changelist commands
2011 if (HasChangelistInSelection())
2012 popup.AppendMenuIcon(IDGITLC_REMOVEFROMCS, IDS_STATUSLIST_CONTEXT_REMOVEFROMCS);
2013 if (changelistSubMenu.CreateMenu())
2015 CString temp;
2016 temp.LoadString(IDS_STATUSLIST_CONTEXT_CREATECS);
2017 changelistSubMenu.AppendMenu(MF_STRING | MF_ENABLED, IDGITLC_CREATECS, temp);
2020 changelistSubMenu.AppendMenu(MF_SEPARATOR);
2021 changelistSubMenu.AppendMenu(MF_STRING | MF_ENABLED, IDGITLC_CREATEIGNORECS, GITSLC_IGNORECHANGELIST);
2024 if (!m_changelists.empty())
2026 // find the changelist names
2027 bool bNeedSeparator = true;
2028 int cmdID = IDGITLC_MOVETOCS;
2029 for (auto it = m_changelists.cbegin(); it != m_changelists.cend(); ++it, ++cmdID)
2031 if (it->first.Compare(GITSLC_IGNORECHANGELIST))
2033 if (bNeedSeparator)
2035 changelistSubMenu.AppendMenu(MF_SEPARATOR);
2036 bNeedSeparator = false;
2038 changelistSubMenu.AppendMenu(MF_STRING | MF_ENABLED, cmdID, it->first);
2042 temp.LoadString(IDS_STATUSLIST_CONTEXT_MOVETOCS);
2043 popup.AppendMenu(MF_POPUP|MF_STRING, reinterpret_cast<UINT_PTR>(changelistSubMenu.GetSafeHmenu()), temp);
2044 temp.LoadString(IDS_STATUSLIST_CONTEXT_KEEPCHANGELISTS);
2045 popup.AppendMenu(MF_STRING | (m_bKeepChangeLists ? MF_CHECKED : 0), IDGITLC_KEEPCHANGELISTS, temp);
2050 m_hShellMenu = nullptr;
2051 if (selectedCount > 0 && !(wcStatus & (CTGitPath::LOGACTIONS_DELETED | CTGitPath::LOGACTIONS_MISSING)) && m_bHasWC && m_CurrentVersion.IsEmpty() && shellMenu.CreatePopupMenu())
2053 // insert the shell context menu
2054 popup.AppendMenu(MF_SEPARATOR);
2055 popup.InsertMenu(static_cast<UINT>(-1), MF_BYPOSITION | MF_POPUP, reinterpret_cast<UINT_PTR>(shellMenu.m_hMenu), CString(MAKEINTRESOURCE(IDS_STATUSLIST_CONTEXT_SHELL)));
2056 m_hShellMenu = shellMenu.m_hMenu;
2059 int cmd = popup.TrackPopupMenu(TPM_RETURNCMD | TPM_LEFTALIGN | TPM_RIGHTBUTTON, point.x, point.y, this);
2060 g_IContext2 = nullptr;
2061 g_IContext3 = nullptr;
2062 if (m_pContextMenu)
2064 if (cmd >= SHELL_MIN_CMD && cmd <= SHELL_MAX_CMD) // see if returned idCommand belongs to shell menu entries)
2066 CMINVOKECOMMANDINFOEX cmi = { 0 };
2067 cmi.cbSize = sizeof(CMINVOKECOMMANDINFOEX);
2068 cmi.fMask = CMIC_MASK_UNICODE | CMIC_MASK_PTINVOKE;
2069 if (GetKeyState(VK_CONTROL) < 0)
2070 cmi.fMask |= CMIC_MASK_CONTROL_DOWN;
2071 if (bShift)
2072 cmi.fMask |= CMIC_MASK_SHIFT_DOWN;
2073 cmi.hwnd = m_hWnd;
2074 cmi.lpVerb = MAKEINTRESOURCEA(cmd - SHELL_MIN_CMD);
2075 cmi.lpVerbW = MAKEINTRESOURCEW(cmd - SHELL_MIN_CMD);
2076 cmi.nShow = SW_SHOWNORMAL;
2077 cmi.ptInvoke = point;
2079 m_pContextMenu->InvokeCommand(reinterpret_cast<LPCMINVOKECOMMANDINFO>(&cmi));
2081 cmd = 0;
2083 m_pContextMenu->Release();
2084 m_pContextMenu = nullptr;
2086 if (g_pFolderhook)
2088 delete g_pFolderhook;
2089 g_pFolderhook = nullptr;
2091 if (g_psfDesktopFolder)
2093 g_psfDesktopFolder->Release();
2094 g_psfDesktopFolder = nullptr;
2096 for (int i = 0; i < g_pidlArrayItems; i++)
2098 if (g_pidlArray[i])
2099 CoTaskMemFree(g_pidlArray[i]);
2101 if (g_pidlArray)
2102 CoTaskMemFree(g_pidlArray);
2103 g_pidlArray = nullptr;
2104 g_pidlArrayItems = 0;
2106 m_bWaitCursor = true;
2107 bShift = !!(GetAsyncKeyState(VK_SHIFT) & 0x8000);
2108 //int iItemCountBeforeMenuCmd = GetItemCount();
2109 //bool bForce = false;
2110 switch (cmd)
2112 case IDGITLC_VIEWREV:
2113 OpenFile(filepath, ALTERNATIVEEDITOR);
2114 break;
2116 case IDGITLC_OPEN:
2117 OpenFile(filepath,OPEN);
2118 break;
2120 case IDGITLC_OPENWITH:
2121 OpenFile(filepath,OPEN_WITH);
2122 break;
2124 case IDGITLC_LFSLOCK:
2125 case IDGITLC_LFSUNLOCK:
2127 CTGitPathList paths;
2128 FillListOfSelectedItemPaths(paths, true);
2130 CGitProgressDlg progDlg;
2131 LFSSetLockedProgressCommand lfsCommand(cmd == IDGITLC_LFSLOCK, false);
2132 progDlg.SetCommand(&lfsCommand);
2133 lfsCommand.SetPathList(paths);
2134 progDlg.SetItemCount(paths.GetCount());
2135 progDlg.DoModal();
2137 // reset unchecked status
2138 POSITION pos = GetFirstSelectedItemPosition();
2139 int index;
2140 while ((index = GetNextSelectedItem(pos)) >= 0)
2141 m_mapFilenameToChecked.erase(GetListEntry(index)->GetGitPathString());
2143 RefreshParent();
2145 break;
2147 case IDGITLC_EXPLORE:
2148 CAppUtils::ExploreTo(GetSafeHwnd(), g_Git.CombinePath(filepath));
2149 break;
2151 case IDGITLC_PREPAREDIFF:
2152 m_sMarkForDiffFilename = filepath->GetGitPathString();
2153 m_sMarkForDiffVersion = m_CurrentVersion.ToString();
2154 break;
2156 case IDGITLC_PREPAREDIFF_COMPARE:
2158 if (auto reg = CRegString(L"Software\\TortoiseGit\\DiffLater", L""); m_sMarkForDiffFilename == reg)
2159 reg.removeValue();
2160 CTGitPath savedFile(m_sMarkForDiffFilename);
2161 CGitDiff::Diff(GetParentHWND(), filepath, &savedFile, m_CurrentVersion.ToString(), m_sMarkForDiffVersion, false, false, 0, bShift);
2163 break;
2165 case IDGITLC_CREATERESTORE:
2167 POSITION pos = GetFirstSelectedItemPosition();
2168 while (pos)
2170 int index = GetNextSelectedItem(pos);
2171 auto entry2 = GetListEntry(index);
2172 if (!entry2 || entry2->IsDirectory())
2173 continue;
2174 if (m_restorepaths.find(entry2->GetWinPathString()) != m_restorepaths.end())
2175 continue;
2176 CTGitPath tempFile = CTempFiles::Instance().GetTempFilePath(false);
2177 // delete the temp file: the temp file has the FILE_ATTRIBUTE_TEMPORARY flag set
2178 // and copying the real file over it would leave that temp flag.
2179 DeleteFile(tempFile.GetWinPath());
2180 if (CopyFile(g_Git.CombinePath(entry2), tempFile.GetWinPath(), FALSE))
2182 m_restorepaths[entry2->GetWinPathString()] = tempFile.GetWinPathString();
2183 SetItemState(index, INDEXTOOVERLAYMASK(OVL_RESTORE), LVIS_OVERLAYMASK);
2186 Invalidate();
2188 break;
2190 case IDGITLC_RESTOREPATH:
2192 if (CMessageBox::Show(GetParentHWND(), IDS_STATUSLIST_RESTOREPATH, IDS_APPNAME, 2, IDI_QUESTION, IDS_RESTOREBUTTON, IDS_ABORTBUTTON) == 2)
2193 break;
2194 POSITION pos = GetFirstSelectedItemPosition();
2195 while (pos)
2197 int index = GetNextSelectedItem(pos);
2198 auto entry2 = GetListEntry(index);
2199 if (!entry2)
2200 continue;
2201 if (m_restorepaths.find(entry2->GetWinPathString()) == m_restorepaths.end())
2202 continue;
2203 if (CopyFile(m_restorepaths[entry2->GetWinPathString()], g_Git.CombinePath(entry2), FALSE))
2205 CPathUtils::Touch(entry2->GetWinPathString());
2206 m_restorepaths.erase(entry2->GetWinPathString());
2207 SetItemState(index, 0, LVIS_OVERLAYMASK);
2210 Invalidate();
2212 break;
2214 // Compare current version and work copy.
2215 case IDGITLC_COMPAREWC:
2216 case IDGITLC_COMPAREPARENTWC:
2218 if (!CheckMultipleDiffs())
2219 break;
2220 POSITION pos = GetFirstSelectedItemPosition();
2221 while ( pos )
2223 int index = GetNextSelectedItem(pos);
2224 StartDiffWC(index, cmd == IDGITLC_COMPAREPARENTWC);
2227 break;
2229 // Compare with base version. when current version is zero, compare workcopy and HEAD.
2230 case IDGITLC_COMPARE:
2232 if (!CheckMultipleDiffs())
2233 break;
2234 POSITION pos = GetFirstSelectedItemPosition();
2235 while ( pos )
2237 int index = GetNextSelectedItem(pos);
2238 StartDiff(index);
2241 break;
2243 case IDGITLC_COMPARETWOREVISIONS:
2245 if (!CheckMultipleDiffs())
2246 break;
2247 POSITION pos = GetFirstSelectedItemPosition();
2248 while ( pos )
2250 int index = GetNextSelectedItem(pos);
2251 StartDiffTwo(index);
2254 break;
2256 case IDGITLC_COMPARETWOFILES:
2258 POSITION pos = GetFirstSelectedItemPosition();
2259 if (pos)
2261 auto firstfilepath = GetListEntry(GetNextSelectedItem(pos));
2262 if (!firstfilepath)
2263 break;
2265 auto secondfilepath = GetListEntry(GetNextSelectedItem(pos));
2266 if (!secondfilepath)
2267 break;
2269 CString sCmd;
2270 if (m_CurrentVersion.IsEmpty())
2271 sCmd.Format(L"/command:diff /path:\"%s\" /startrev:%s /path2:\"%s\" /endrev:%s /hwnd:%p", firstfilepath->GetWinPath(), firstfilepath->Exists() ? GIT_REV_ZERO : L"HEAD", secondfilepath->GetWinPath(), secondfilepath->Exists() ? GIT_REV_ZERO : L"HEAD", reinterpret_cast<void*>(m_hWnd));
2272 else
2273 sCmd.Format(L"/command:diff /path:\"%s\" /startrev:%s /path2:\"%s\" /endrev:%s /hwnd:%p", firstfilepath->GetWinPath(), firstfilepath->m_Action & CTGitPath::LOGACTIONS_DELETED ? static_cast<LPCWSTR>(m_CurrentVersion.ToString() + L"~1") : static_cast<LPCWSTR>(m_CurrentVersion.ToString()), secondfilepath->GetWinPath(), secondfilepath->m_Action & CTGitPath::LOGACTIONS_DELETED ? static_cast<LPCWSTR>(m_CurrentVersion.ToString() + L"~1") : static_cast<LPCWSTR>(m_CurrentVersion.ToString()), reinterpret_cast<void*>(m_hWnd));
2274 if (bShift)
2275 sCmd += L" /alternative";
2276 CAppUtils::RunTortoiseGitProc(sCmd);
2279 break;
2281 case IDGITLC_GNUDIFF1:
2283 int diffContext = g_Git.GetConfigValueInt32(L"diff.context", -1);
2284 CString fullTempFile = GetTempFile();
2285 if (fullTempFile.IsEmpty())
2287 ::MessageBox(m_hWnd, L"Could not create temp file.", L"TortoiseGit", MB_OK | MB_ICONERROR);
2288 break;
2290 std::wofstream outStream(fullTempFile);
2292 POSITION pos = GetFirstSelectedItemPosition();
2293 while (pos)
2295 auto selectedFilepath = GetListEntry(GetNextSelectedItem(pos));
2296 auto tempfile = GetTempFile();
2297 if (m_CurrentVersion.IsEmpty())
2299 CString fromwhere;
2300 if (m_amend)
2301 fromwhere = L"~1";
2302 if (g_Git.GetUnifiedDiff(*selectedFilepath, GitRev::GetHead() + fromwhere, GitRev::GetWorkingCopy(), tempfile, false, false, diffContext, false))
2304 ::MessageBox(m_hWnd, g_Git.GetGitLastErr(L"Could not get unified diff.", CGit::GIT_CMD_DIFF), L"TortoiseGit", MB_OK);
2305 break;
2308 else
2310 if ((selectedFilepath->m_ParentNo & (PARENT_MASK | MERGE_MASK)) == 0)
2312 if (g_Git.GetUnifiedDiff(*selectedFilepath, m_CurrentVersion.ToString() + L"~1", m_CurrentVersion.ToString(), tempfile, false, false, diffContext, false))
2314 ::MessageBox(m_hWnd, g_Git.GetGitLastErr(L"Could not get unified diff.", CGit::GIT_CMD_DIFF), L"TortoiseGit", MB_OK);
2315 break;
2318 else
2320 CString str;
2321 if (!(selectedFilepath->m_ParentNo & MERGE_MASK))
2322 str.Format(L"%s^%d", static_cast<LPCWSTR>(m_CurrentVersion.ToString()), (selectedFilepath->m_ParentNo & PARENT_MASK) + 1);
2324 if (g_Git.GetUnifiedDiff(*selectedFilepath, str, m_CurrentVersion.ToString(), tempfile, !!(selectedFilepath->m_ParentNo & MERGE_MASK), false, diffContext, false))
2326 ::MessageBox(m_hWnd, g_Git.GetGitLastErr(L"Could not get unified diff.", CGit::GIT_CMD_DIFF), L"TortoiseGit", MB_OK);
2327 break;
2331 std::wifstream inStream(tempfile);
2332 outStream << inStream.rdbuf();
2334 outStream.close();
2335 SetFileAttributes(fullTempFile, FILE_ATTRIBUTE_READONLY);
2336 if (m_CurrentVersion.IsEmpty())
2337 CAppUtils::StartUnifiedDiffViewer(fullTempFile, GitRev::GetHead() + (m_amend ? L"~1" : L""), FALSE, bShift);
2338 else
2339 CAppUtils::StartUnifiedDiffViewer(fullTempFile, m_CurrentVersion.ToString(), FALSE, bShift);
2341 break;
2343 case IDGITLC_GNUDIFF2REVISIONS:
2345 if (!CheckMultipleDiffs())
2346 break;
2347 POSITION pos = GetFirstSelectedItemPosition();
2348 while (pos)
2350 auto entry = GetListEntry(GetNextSelectedItem(pos));
2351 CAppUtils::StartShowUnifiedDiff(m_hWnd, *entry, m_Rev2.ToString(), *entry, m_Rev1.ToString(), bShift);
2354 break;
2356 case IDGITLC_ADD:
2357 case IDGITLC_ADD_EXE:
2358 case IDGITLC_ADD_LINK:
2360 CTGitPathList paths;
2361 FillListOfSelectedItemPaths(paths, true);
2363 CGitProgressDlg progDlg;
2364 AddProgressCommand addCommand;
2365 progDlg.SetCommand(&addCommand);
2366 addCommand.SetShowCommitButtonAfterAdd((m_dwContextMenus & GITSLC_POPCOMMIT) != 0);
2367 addCommand.SetPathList(paths);
2368 if (cmd == IDGITLC_ADD_EXE)
2369 addCommand.SetExecutable();
2370 else if (cmd == IDGITLC_ADD_LINK)
2371 addCommand.SetSymlink();
2372 progDlg.SetItemCount(paths.GetCount());
2373 progDlg.DoModal();
2375 // reset unchecked status
2376 POSITION pos = GetFirstSelectedItemPosition();
2377 int index;
2378 while ((index = GetNextSelectedItem(pos)) >= 0)
2379 m_mapFilenameToChecked.erase(GetListEntry(index)->GetGitPathString());
2381 RefreshParent();
2383 break;
2385 case IDGITLC_DELETE:
2386 DeleteSelectedFiles();
2387 break;
2389 case IDGITLC_BLAME:
2391 CAppUtils::LaunchTortoiseBlame(g_Git.CombinePath(filepath), m_CurrentVersion.ToString());
2393 break;
2395 case IDGITLC_LOG:
2396 case IDGITLC_LOGSUBMODULE:
2398 CString sCmd;
2399 sCmd.Format(L"/command:log /path:\"%s\"", static_cast<LPCWSTR>(g_Git.CombinePath(filepath)));
2400 if (cmd == IDGITLC_LOG)
2402 if (filepath->IsDirectory())
2403 sCmd += L" /submodule";
2404 if (!m_sDisplayedBranch.IsEmpty())
2405 sCmd += L" /range:\"" + m_sDisplayedBranch + L'"';
2406 else
2407 sCmd += L" /endrev:\"" + m_CurrentVersion.ToString() + '"';
2409 CAppUtils::RunTortoiseGitProc(sCmd, false, !(cmd == IDGITLC_LOGSUBMODULE));
2411 break;
2413 case IDGITLC_LOGOLDNAME:
2415 CTGitPath oldName(filepath->GetGitOldPathString());
2416 CString sCmd;
2417 sCmd.Format(L"/command:log /path:\"%s\"", static_cast<LPCWSTR>(g_Git.CombinePath(oldName)));
2418 if (filepath->IsDirectory())
2419 sCmd += L" /submodule";
2420 if (!m_sDisplayedBranch.IsEmpty())
2421 sCmd += L" /range:\"" + m_sDisplayedBranch + L'"';
2422 CAppUtils::RunTortoiseGitProc(sCmd);
2424 break;
2426 case IDGITLC_EDITCONFLICT:
2428 if (CAppUtils::ConflictEdit(GetParentHWND(), *filepath, bShift, m_bIsRevertTheirMy, GetLogicalParent() ? GetLogicalParent()->GetSafeHwnd() : nullptr))
2430 CString conflictedFile = g_Git.CombinePath(filepath);
2431 if (!PathFileExists(conflictedFile))
2433 RefreshParent();
2434 break;
2436 StoreScrollPos();
2437 Show(m_dwShow, 0, m_bShowFolders, 0, true);
2440 break;
2442 case IDGITLC_RESOLVETHEIRS: //follow up
2443 case IDGITLC_RESOLVEMINE: //follow up
2444 case IDGITLC_RESOLVECONFLICT:
2446 if (CMessageBox::Show(GetParentHWND(), IDS_PROC_RESOLVE, IDS_APPNAME, MB_ICONQUESTION | MB_YESNO) == IDYES)
2448 ResolveWith resolveWith = ResolveWith::Current;
2449 if (((!this->m_bIsRevertTheirMy) && cmd == IDGITLC_RESOLVETHEIRS) || ((this->m_bIsRevertTheirMy) && cmd == IDGITLC_RESOLVEMINE))
2450 resolveWith = ResolveWith::Theirs;
2451 else if (((!this->m_bIsRevertTheirMy) && cmd == IDGITLC_RESOLVEMINE) || ((this->m_bIsRevertTheirMy) && cmd == IDGITLC_RESOLVETHEIRS))
2452 resolveWith = ResolveWith::Mine;
2454 CTGitPathList targetList;
2455 FillListOfSelectedItemPaths(targetList);
2457 CGitProgressDlg progDlg;
2458 ResolveProgressCommand resolveCommand{ resolveWith };
2459 progDlg.SetAutoClose(GitProgressAutoClose::AUTOCLOSE_IF_NO_ERRORS);
2460 progDlg.SetCommand(&resolveCommand);
2461 progDlg.SetItemCount(targetList.GetCount());
2462 resolveCommand.SetPathList(targetList);
2463 progDlg.DoModal();
2464 if (progDlg.DidErrorsOccur() || resolveWith != ResolveWith::Current)
2466 RefreshParent();
2467 break;
2470 std::set<CString> resolvedConflictFiles;
2471 std::for_each(targetList.cbegin(), targetList.cend(), [&resolvedConflictFiles](auto& path) { resolvedConflictFiles.emplace(path.GetGitPathString()); });
2472 const int nListboxEntries = GetItemCount();
2473 for (int nItem = 0; nItem < nListboxEntries; ++nItem)
2475 auto path = GetListEntry(nItem);
2476 if (!resolvedConflictFiles.contains(path->GetGitPathString()))
2477 continue;
2478 path->m_Action |= CTGitPath::LOGACTIONS_MODIFIED;
2479 path->m_Action &= ~CTGitPath::LOGACTIONS_UNMERGED;
2482 StoreScrollPos();
2483 Show(m_dwShow, 0, m_bShowFolders,0,true);
2486 break;
2488 case IDGITLC_IGNORE:
2490 CTGitPathList ignorelist;
2491 //std::vector<CString> toremove;
2492 FillListOfSelectedItemPaths(ignorelist, true);
2494 if (!CAppUtils::IgnoreFile(GetParentHWND(), ignorelist, false))
2495 break;
2497 RefreshParent();
2499 break;
2501 case IDGITLC_IGNOREMASK:
2503 CString common;
2504 CTGitPathList ignorelist;
2505 FillListOfSelectedItemPaths(ignorelist, true);
2507 if (!CAppUtils::IgnoreFile(GetParentHWND(), ignorelist, true))
2508 break;
2510 RefreshParent();
2512 break;
2514 case IDGITLC_IGNOREFOLDER:
2516 CTGitPathList ignorelist;
2517 ignorelist.AddPath(filepath->GetContainingDirectory());
2519 if (!CAppUtils::IgnoreFile(GetParentHWND(), ignorelist, false))
2520 break;
2522 RefreshParent();
2524 break;
2525 case IDGITLC_COMMIT:
2527 CTGitPathList targetList;
2528 FillListOfSelectedItemPaths(targetList);
2529 CTGitPath tempFile = CTempFiles::Instance().GetTempFilePath(false);
2530 VERIFY(targetList.WriteToFile(tempFile.GetWinPathString()));
2531 CString commandline = L"/command:commit /pathfile:\"";
2532 commandline += tempFile.GetWinPathString();
2533 commandline += L'"';
2534 commandline += L" /deletepathfile";
2535 CAppUtils::RunTortoiseGitProc(commandline);
2537 break;
2538 case IDGITLC_REVERT:
2540 // If at least one item is not in the status "added"
2541 // we ask for a confirmation
2542 BOOL bConfirm = FALSE;
2543 POSITION pos = GetFirstSelectedItemPosition();
2544 int index;
2545 while ((index = GetNextSelectedItem(pos)) >= 0)
2547 auto fentry = GetListEntry(index);
2548 if(fentry && fentry->m_Action &CTGitPath::LOGACTIONS_MODIFIED && !fentry->IsDirectory())
2550 bConfirm = TRUE;
2551 break;
2555 CString str;
2556 str.Format(IDS_PROC_WARNREVERT, selectedCount);
2558 if (!bConfirm || MessageBox(str, L"TortoiseGit", MB_YESNO | MB_ICONQUESTION) == IDYES)
2560 CTGitPathList targetList;
2561 FillListOfSelectedItemPaths(targetList);
2563 CString revertToCommit = L"HEAD";
2564 if (m_amend)
2565 revertToCommit = L"HEAD~1";
2567 CGitProgressDlg progDlg;
2568 RevertProgressCommand revertCommand{ revertToCommit };
2569 progDlg.SetAutoClose(GitProgressAutoClose::AUTOCLOSE_IF_NO_OPTIONS);
2570 progDlg.SetCommand(&revertCommand);
2571 progDlg.SetItemCount(targetList.GetCount());
2572 revertCommand.SetPathList(targetList);
2573 progDlg.DoModal();
2574 if (progDlg.DidErrorsOccur())
2576 RefreshParent();
2577 break;
2579 else
2581 bool updateStatusList = false;
2582 std::set<CString> revertedFiles;
2583 std::for_each(targetList.cbegin(), targetList.cend(), [&revertedFiles](auto& path) { revertedFiles.emplace(path.GetGitPathString()); });
2584 std::vector<int> toRemove;
2585 const int nListboxEntries = GetItemCount();
2586 for (int nItem = 0; nItem < nListboxEntries; ++nItem)
2588 auto path = GetListEntry(nItem);
2589 if (!path || !revertedFiles.contains(path->GetGitPathString()))
2590 continue;
2591 if (!path->IsDirectory())
2593 if (path->m_Action & CTGitPath::LOGACTIONS_ADDED)
2595 path->m_Action = CTGitPath::LOGACTIONS_UNVER;
2596 SetEntryCheck(path,nItem,false);
2597 #if 0 // revert an added file and some entry will be cloned (part 1/2)
2598 SetItemGroup(nItem,1);
2599 this->m_StatusFileList.RemoveItem(*path);
2600 this->m_UnRevFileList.AddPath(*path);
2601 //this->m_IgnoreFileList.RemoveItem(*path);
2602 #else
2603 updateStatusList = true;
2604 break;
2605 #endif
2607 else
2609 if (GetCheck(nItem))
2610 m_nSelected--;
2611 toRemove.emplace_back(nItem);
2612 if (toRemove.size() == revertedFiles.size())
2613 break;
2616 else if (path->IsDirectory())
2618 updateStatusList = true;
2619 break;
2622 #if 0 // revert an added file and some entry will be cloned (part 2/2)
2623 Show(m_dwShow, 0, m_bShowFolders,updateStatusList,true);
2624 NotifyCheck();
2625 #else
2626 if (updateStatusList)
2627 RefreshParent();
2628 else
2630 SetRedraw(FALSE);
2631 std::for_each(toRemove.rbegin(), toRemove.rend(), [&](int nItem) { RemoveListEntry(nItem); });
2632 SetRedraw(TRUE);
2634 #endif
2638 break;
2640 case IDGITLC_ASSUMEVALID:
2641 SetGitIndexFlagsForSelectedFiles(IDS_PROC_MARK_ASSUMEVALID, BST_CHECKED, BST_INDETERMINATE);
2642 break;
2643 case IDGITLC_SKIPWORKTREE:
2644 SetGitIndexFlagsForSelectedFiles(IDS_PROC_MARK_SKIPWORKTREE, BST_INDETERMINATE, BST_CHECKED);
2645 break;
2646 case IDGITLC_UNSETIGNORELOCALCHANGES:
2647 SetGitIndexFlagsForSelectedFiles(IDS_PROC_UNSET_IGNORELOCALCHANGES, BST_UNCHECKED, BST_UNCHECKED);
2648 break;
2649 case IDGITLC_COPYFULL:
2650 case IDGITLC_COPYRELPATHS:
2651 case IDGITLC_COPYFILENAMES:
2652 CopySelectedEntriesToClipboard(GITSLC_COLFILENAME, cmd);
2653 break;
2654 case IDGITLC_COPYEXT:
2655 CopySelectedEntriesToClipboard(static_cast<DWORD>(-1), 0);
2656 break;
2657 case IDGITLC_COPYCOL:
2658 CopySelectedEntriesToClipboard(DWORD(1) << selSubitem, 0);
2659 break;
2660 case IDGITLC_EXPORT:
2661 FilesExport();
2662 break;
2663 case IDGITLC_SAVEAS:
2664 FileSaveAs(filepath);
2665 break;
2667 case IDGITLC_REVERTTOREV:
2668 RevertSelectedItemToVersion();
2669 break;
2670 case IDGITLC_REVERTTOPARENT:
2671 RevertSelectedItemToVersion(true);
2672 break;
2673 #if 0
2674 case IDSVNLC_COMMIT:
2676 CTSVNPathList targetList;
2677 FillListOfSelectedItemPaths(targetList);
2678 CTSVNPath tempFile = CTempFiles::Instance().GetTempFilePath(false);
2679 VERIFY(targetList.WriteToFile(tempFile.GetWinPathString()));
2680 CString commandline = CPathUtils::GetAppDirectory();
2681 commandline += L"TortoiseGitProc.exe /command:commit /pathfile:\"";
2682 commandline += tempFile.GetWinPathString();
2683 commandline += L'"';
2684 commandline += L" /deletepathfile";
2685 CAppUtils::LaunchApplication(commandline, nullptr, false);
2687 break;
2688 #endif
2689 case IDGITLC_CREATEIGNORECS:
2690 MoveToChangelist(GITSLC_IGNORECHANGELIST);
2691 SaveChangelists();
2692 break;
2693 case IDGITLC_REMOVEFROMCS:
2694 RemoveFromChangelist();
2695 SaveChangelists();
2696 break;
2697 case IDGITLC_CREATECS:
2699 CCreateChangelistDlg dlg;
2700 if (dlg.DoModal() == IDOK)
2702 MoveToChangelist(dlg.m_sName);
2703 SaveChangelists();
2706 break;
2707 case IDGITLC_KEEPCHANGELISTS:
2708 m_regKeepChangeLists = m_bKeepChangeLists = !m_bKeepChangeLists;
2709 break;
2710 default:
2712 if (cmd < IDGITLC_MOVETOCS)
2713 break;
2715 // find the changelist name
2716 CString sChangelist;
2717 int cmdID = IDGITLC_MOVETOCS;
2719 SetRedraw(FALSE);
2720 for (auto it = m_changelists.cbegin(); it != m_changelists.cend(); ++it, ++cmdID)
2722 if (cmd == cmdID)
2724 sChangelist = it->first;
2725 break;
2729 if (!sChangelist.IsEmpty())
2731 MoveToChangelist(sChangelist);
2732 SaveChangelists();
2734 SetRedraw(TRUE);
2736 break;
2738 } // switch (cmd)
2739 m_bWaitCursor = false;
2740 GetStatisticsString();
2741 //int iItemCountAfterMenuCmd = GetItemCount();
2742 //if (iItemCountAfterMenuCmd != iItemCountBeforeMenuCmd)
2744 // CWnd* pParent = GetParent();
2745 // if (pParent && pParent->GetSafeHwnd())
2746 // {
2747 // pParent->SendMessage(SVNSLNM_ITEMCOUNTCHANGED);
2748 // }
2750 } // if (popup.CreatePopupMenu())
2751 } // if (selIndex >= 0)
2754 void CGitStatusListCtrl::AppendLocksMenuItems(CIconMenu& popup)
2756 if (!CTGitPath(g_Git.m_CurrentDir).HasLFS())
2757 return;
2759 if (!m_ColumnManager.IsVisible(m_ColumnManager.GetColumnByName(IDS_STATUSLIST_COLLFSLOCK)))
2761 // User disabled IDS_STATUSLIST_COLLFSLOCK
2762 // So we haven't asked lock list data from server in CGitStatusListCtrl::GetStatus
2763 // But locking & unlocking is still valid operations (except for folders).
2765 POSITION pos = GetFirstSelectedItemPosition();
2766 int index;
2767 while ((index = GetNextSelectedItem(pos)) >= 0)
2769 auto path = GetListEntry(index);
2770 if (path->IsDirectory())
2771 return;
2774 popup.AppendMenuIcon(IDGITLC_LFSLOCK, IDS_PROGRS_TITLE_LFS_LOCK, IDI_LFSLOCK);
2775 popup.AppendMenuIcon(IDGITLC_LFSUNLOCK, IDS_PROGRS_TITLE_LFS_UNLOCK, IDI_LFSUNLOCK);
2776 return;
2779 bool hasLocked = false;
2780 bool hasUnLocked = false;
2782 POSITION pos = GetFirstSelectedItemPosition();
2783 int index;
2784 while ((index = GetNextSelectedItem(pos)) >= 0)
2786 auto path = GetListEntry(index);
2788 if (path->IsDirectory())
2789 return;
2791 if (path->m_Action & GITSLC_SHOWLFSLOCKS)
2792 hasLocked = true;
2793 else
2794 hasUnLocked = true;
2797 if (hasUnLocked && !hasLocked)
2798 popup.AppendMenuIcon(IDGITLC_LFSLOCK, IDS_PROGRS_TITLE_LFS_LOCK, IDI_LFSLOCK);
2800 if (hasLocked && !hasUnLocked)
2801 popup.AppendMenuIcon(IDGITLC_LFSUNLOCK, IDS_PROGRS_TITLE_LFS_UNLOCK, IDI_LFSUNLOCK);
2804 void CGitStatusListCtrl::MoveToChangelist(const CString& name)
2806 CAutoReadLock locker(m_guard);
2808 POSITION pos = GetFirstSelectedItemPosition();
2809 while (pos)
2811 int index = GetNextSelectedItem(pos);
2812 auto pGitPath = GetListEntry(index);
2813 if (name.Compare(GITSLC_IGNORECHANGELIST) == 0)
2814 SetEntryCheck(pGitPath, index, false);
2815 m_pathToChangelist.insert_or_assign(pGitPath->GetGitPathString(), name);
2818 PrepareGroups();
2820 for (int i = 0; i < GetItemCount(); ++i)
2821 SetItemGroup(i, GetListEntry(i));
2824 void CGitStatusListCtrl::RemoveFromChangelist()
2826 CAutoReadLock locker(m_guard);
2828 POSITION pos = GetFirstSelectedItemPosition();
2829 while (pos)
2831 auto pGitPath = GetListEntry(GetNextSelectedItem(pos));
2832 m_pathToChangelist.erase(pGitPath->GetGitPathString());
2835 PrepareGroups();
2837 for (int i = 0; i < GetItemCount(); ++i)
2838 SetItemGroup(i, GetListEntry(i));
2841 void CGitStatusListCtrl::SetGitIndexFlagsForSelectedFiles(UINT message, BOOL assumevalid, BOOL skipworktree)
2843 if (CMessageBox::Show(GetParentHWND(), message, IDS_APPNAME, MB_YESNO | MB_DEFBUTTON2 | MB_ICONQUESTION) != IDYES)
2844 return;
2846 CAutoReadLock locker(m_guard);
2848 CAutoRepository repository(g_Git.GetGitRepository());
2849 if (!repository)
2851 MessageBox(g_Git.GetLibGit2LastErr(), L"TortoiseGit", MB_ICONERROR);
2852 return;
2855 CAutoIndex gitindex;
2856 if (git_repository_index(gitindex.GetPointer(), repository))
2858 MessageBox(g_Git.GetLibGit2LastErr(), L"TortoiseGit", MB_ICONERROR);
2859 return;
2862 POSITION pos = GetFirstSelectedItemPosition();
2863 int index = -1;
2864 while ((index = GetNextSelectedItem(pos)) >= 0)
2866 auto path = GetListEntry(index);
2867 if (path == nullptr)
2868 continue;
2870 size_t idx;
2871 if (!git_index_find(&idx, gitindex, CUnicodeUtils::GetUTF8(path->GetGitPathString())))
2873 git_index_entry *e = const_cast<git_index_entry *>(git_index_get_byindex(gitindex, idx)); // HACK
2874 if (assumevalid == BST_UNCHECKED)
2875 e->flags &= ~GIT_INDEX_ENTRY_VALID;
2876 else if (assumevalid == BST_CHECKED)
2877 e->flags |= GIT_INDEX_ENTRY_VALID;
2878 if (skipworktree == BST_UNCHECKED)
2879 e->flags_extended &= ~GIT_INDEX_ENTRY_SKIP_WORKTREE;
2880 else if (skipworktree == BST_CHECKED)
2881 e->flags_extended |= GIT_INDEX_ENTRY_SKIP_WORKTREE;
2882 git_index_add(gitindex, e);
2884 else
2885 MessageBox(g_Git.GetLibGit2LastErr(), L"TortoiseGit", MB_ICONERROR);
2888 if (git_index_write(gitindex))
2890 MessageBox(g_Git.GetLibGit2LastErr(), L"TortoiseGit", MB_ICONERROR);
2891 return;
2894 RefreshParent();
2897 void CGitStatusListCtrl::OnContextMenu(CWnd* pWnd, CPoint point)
2899 __super::OnContextMenu(pWnd, point);
2900 if (pWnd == this)
2901 OnContextMenuList(pWnd, point);
2904 void CGitStatusListCtrl::OnNMDblclk(NMHDR *pNMHDR, LRESULT *pResult)
2906 LPNMLISTVIEW pNMLV = reinterpret_cast<LPNMLISTVIEW>(pNMHDR);
2907 *pResult = 0;
2909 CAutoReadWeakLock readLock(m_guard);
2910 if (!readLock.IsAcquired())
2911 return;
2913 if (pNMLV->iItem < 0 || !m_bEnableDblClick)
2914 return;
2916 if (m_bHasCheckboxes)
2918 CPoint point(pNMLV->ptAction);
2919 UINT uFlags = 0;
2920 HitTest(point, &uFlags);
2921 if (uFlags == LVHT_ONITEMSTATEICON)
2922 return;
2925 auto file = GetListEntry(pNMLV->iItem);
2927 if (file->m_Action & (CTGitPath::LOGACTIONS_UNVER | CTGitPath::LOGACTIONS_IGNORE)) {
2928 StartDiffWC(pNMLV->iItem);
2929 return;
2931 if( file->m_Action&CTGitPath::LOGACTIONS_UNMERGED )
2933 if (CAppUtils::ConflictEdit(GetParentHWND(), *file, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000), m_bIsRevertTheirMy, GetLogicalParent() ? GetLogicalParent()->GetSafeHwnd() : nullptr))
2935 CString conflictedFile = g_Git.CombinePath(file);
2936 if (!PathFileExists(conflictedFile))
2938 RefreshParent();
2939 return;
2941 StoreScrollPos();
2942 Show(m_dwShow, 0, m_bShowFolders, 0, true);
2945 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))
2946 return;
2947 else
2949 if (!m_Rev1.IsEmpty() && !m_Rev2.IsEmpty())
2950 StartDiffTwo(pNMLV->iItem);
2951 else
2952 StartDiff(pNMLV->iItem);
2955 void CGitStatusListCtrl::StartDiffTwo(int fileindex)
2957 if(fileindex<0)
2958 return;
2960 CAutoReadLock locker(m_guard);
2961 auto ptr = GetListEntry(fileindex);
2962 if (!ptr)
2963 return;
2964 CTGitPath file1 = *ptr;
2966 if (file1.m_Action & CTGitPath::LOGACTIONS_ADDED)
2967 CGitDiff::DiffNull(GetParentHWND(), &file1, m_Rev1.ToString(), true, 0, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000));
2968 else if (file1.m_Action & CTGitPath::LOGACTIONS_DELETED)
2969 CGitDiff::DiffNull(GetParentHWND(), &file1, m_Rev2.ToString(), false, 0, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000));
2970 else
2971 CGitDiff::Diff(GetParentHWND(), &file1, &file1, m_Rev1.ToString(), m_Rev2.ToString(), false, false, 0, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000));
2974 void CGitStatusListCtrl::StartDiffWC(int fileindex, bool parent)
2976 if(fileindex<0)
2977 return;
2979 CAutoReadLock locker(m_guard);
2980 auto ptr = GetListEntry(fileindex);
2981 if (!ptr)
2982 return;
2983 CTGitPath file1 = *ptr;
2984 file1.m_Action = 0; // reset action, so that diff is not started as added/deleted file; see issue #1757
2986 CString version;
2987 if (parent)
2989 CGitHash parentHash;
2990 CString title;
2991 if (!GetParentCommitInfo(m_CurrentVersion, file1.m_ParentNo & PARENT_MASK, parentHash, title))
2993 MessageBox(g_Git.GetGitLastErr(L"Could not get parent hash for \"" + m_CurrentVersion.ToString() + L"\"."), L"TortoiseGit", MB_ICONERROR);
2994 return;
2996 version = parentHash.ToString();
2998 else
2999 version = m_CurrentVersion.ToString();
3001 CGitDiff::Diff(GetParentHWND(), &file1, &file1, GIT_REV_ZERO, version, false, false, 0, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000));
3004 void CGitStatusListCtrl::StartDiff(int fileindex)
3006 if(fileindex<0)
3007 return;
3009 CAutoReadLock locker(m_guard);
3010 auto ptr = GetListEntry(fileindex);
3011 if (!ptr)
3012 return;
3013 CTGitPath file1 = *ptr;
3014 CTGitPath file2;
3015 if(file1.m_Action & (CTGitPath::LOGACTIONS_REPLACED|CTGitPath::LOGACTIONS_COPY))
3016 file2.SetFromGit(file1.GetGitOldPathString());
3017 else
3018 file2=file1;
3020 if (m_CurrentVersion.IsEmpty())
3022 CString fromwhere;
3023 if(m_amend && (file1.m_Action & CTGitPath::LOGACTIONS_ADDED) == 0)
3024 fromwhere = L"~1";
3025 if( g_Git.IsInitRepos())
3026 CGitDiff::DiffNull(GetParentHWND(), GetListEntry(fileindex),
3027 GIT_REV_ZERO, true, 0, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000));
3028 else if( file1.m_Action&CTGitPath::LOGACTIONS_ADDED )
3029 CGitDiff::DiffNull(GetParentHWND(), GetListEntry(fileindex),
3030 m_CurrentVersion.ToString() + fromwhere, true, 0, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000));
3031 else if( file1.m_Action&CTGitPath::LOGACTIONS_DELETED )
3032 CGitDiff::DiffNull(GetParentHWND(), GetListEntry(fileindex),
3033 GitRev::GetHead() + fromwhere, false, 0, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000));
3034 else
3035 CGitDiff::Diff(GetParentHWND(), &file1,&file2,
3036 CString(GIT_REV_ZERO),
3037 GitRev::GetHead() + fromwhere, false, false, 0, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000));
3039 else
3041 CGitHash hash;
3042 CString fromwhere = m_CurrentVersion.ToString() + L"~1";
3043 if(m_amend)
3044 fromwhere = m_CurrentVersion.ToString() + L"~2";
3045 bool revfail = !!g_Git.GetHash(hash, fromwhere);
3046 if (revfail || (file1.m_Action & file1.LOGACTIONS_ADDED))
3047 CGitDiff::DiffNull(GetParentHWND(), &file1, m_CurrentVersion.ToString(), true, 0, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000));
3048 else if (file1.m_Action & file1.LOGACTIONS_DELETED)
3050 if (file1.m_ParentNo > 0)
3051 fromwhere.Format(L"%s^%d", static_cast<LPCWSTR>(m_CurrentVersion.ToString()), file1.m_ParentNo + 1);
3053 CGitDiff::DiffNull(GetParentHWND(), &file1, fromwhere, false, 0, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000));
3055 else
3057 if( file1.m_ParentNo & MERGE_MASK)
3059 CTGitPath base, theirs, mine, merge;
3061 CString temppath;
3062 GetTempPath(temppath);
3063 temppath.TrimRight(L'\\');
3065 mine.SetFromGit(temppath + L'\\' + file1.GetFileOrDirectoryName() + L".LOCAL" + file1.GetFileExtension());
3066 theirs.SetFromGit(temppath + L'\\' + file1.GetFileOrDirectoryName() + L".REMOTE" + file1.GetFileExtension());
3067 base.SetFromGit(temppath + L'\\' + file1.GetFileOrDirectoryName() + L".BASE" + file1.GetFileExtension());
3069 CFile tempfile;
3070 //create a empty file, incase stage is not three
3071 tempfile.Open(mine.GetWinPathString(),CFile::modeCreate|CFile::modeReadWrite);
3072 tempfile.Close();
3073 tempfile.Open(theirs.GetWinPathString(),CFile::modeCreate|CFile::modeReadWrite);
3074 tempfile.Close();
3075 tempfile.Open(base.GetWinPathString(),CFile::modeCreate|CFile::modeReadWrite);
3076 tempfile.Close();
3078 merge.SetFromGit(temppath + L'\\' + file1.GetFileOrDirectoryName() + L".Merged" + file1.GetFileExtension());
3080 int parent1=-1, parent2 =-1;
3081 for (size_t i = 0; i < m_arStatusArray.size(); ++i)
3083 if(m_arStatusArray[i]->GetGitPathString() == file1.GetGitPathString())
3085 if(m_arStatusArray[i]->m_ParentNo & MERGE_MASK)
3088 else
3090 if(parent1<0)
3091 parent1 = m_arStatusArray[i]->m_ParentNo & PARENT_MASK;
3092 else if (parent2 <0)
3093 parent2 = m_arStatusArray[i]->m_ParentNo & PARENT_MASK;
3098 if (g_Git.GetOneFile(m_CurrentVersion.ToString(), file1, merge.GetWinPathString()))
3099 CMessageBox::Show(GetParentHWND(), IDS_STATUSLIST_FAILEDGETMERGEFILE, IDS_APPNAME, MB_OK | MB_ICONERROR);
3101 if(parent1>=0)
3103 CString str;
3104 str.Format(L"%s^%d", static_cast<LPCWSTR>(m_CurrentVersion.ToString()), parent1 + 1);
3106 if (g_Git.GetOneFile(str, file1, mine.GetWinPathString()))
3107 CMessageBox::Show(GetParentHWND(), IDS_STATUSLIST_FAILEDGETMERGEFILE, IDS_APPNAME, MB_OK | MB_ICONERROR);
3110 if(parent2>=0)
3112 CString str;
3113 str.Format(L"%s^%d", static_cast<LPCWSTR>(m_CurrentVersion.ToString()), parent2 + 1);
3115 if (g_Git.GetOneFile(str, file1, theirs.GetWinPathString()))
3116 CMessageBox::Show(GetParentHWND(), IDS_STATUSLIST_FAILEDGETMERGEFILE, IDS_APPNAME, MB_OK | MB_ICONERROR);
3119 if(parent1>=0 && parent2>=0)
3121 CString cmd, output;
3122 cmd.Format(L"git.exe merge-base -- %s^%d %s^%d", static_cast<LPCWSTR>(m_CurrentVersion.ToString()), parent1 + 1,
3123 static_cast<LPCWSTR>(m_CurrentVersion.ToString()), parent2 + 1);
3125 if (!g_Git.Run(cmd, &output, nullptr, CP_UTF8))
3127 if (g_Git.GetOneFile(output.Left(2 * GIT_HASH_SIZE), file1, base.GetWinPathString()))
3128 CMessageBox::Show(GetParentHWND(), IDS_STATUSLIST_FAILEDGETBASEFILE, IDS_APPNAME, MB_OK | MB_ICONERROR);
3131 CAppUtils::StartExtMerge(!!(GetAsyncKeyState(VK_SHIFT) & 0x8000), base, theirs, mine, merge, L"BASE", L"REMOTE", L"LOCAL");
3133 else
3135 CString str;
3136 if( (file1.m_ParentNo&PARENT_MASK) == 0)
3137 str = L"~1";
3138 else
3139 str.Format(L"^%d", (file1.m_ParentNo & PARENT_MASK) + 1);
3140 CGitDiff::Diff(GetParentHWND(), &file1,&file2,
3141 m_CurrentVersion.ToString(),
3142 m_CurrentVersion.ToString() + str, false, false, 0, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000));
3148 void CGitStatusListCtrl::UpdateDiffWithFileFromReg()
3150 static CString lastDiffLaterFile;
3151 if (CString diffLaterFile = CRegString(L"Software\\TortoiseGit\\DiffLater", L""); !diffLaterFile.IsEmpty() && lastDiffLaterFile != diffLaterFile)
3153 lastDiffLaterFile = diffLaterFile;
3154 m_sMarkForDiffFilename = diffLaterFile;
3155 m_sMarkForDiffVersion = GitRev::GetWorkingCopy();
3159 CString CGitStatusListCtrl::GetStatisticsString(bool simple)
3161 CString sNormal = CString(MAKEINTRESOURCE(IDS_STATUSNORMAL));
3162 CString sAdded = CString(MAKEINTRESOURCE(IDS_STATUSADDED));
3163 CString sDeleted = CString(MAKEINTRESOURCE(IDS_STATUSDELETED));
3164 CString sModified = CString(MAKEINTRESOURCE(IDS_STATUSMODIFIED));
3165 CString sConflicted = CString(MAKEINTRESOURCE(IDS_STATUSCONFLICTED));
3166 CString sUnversioned = CString(MAKEINTRESOURCE(IDS_STATUSUNVERSIONED));
3167 CString sRenamed = CString(MAKEINTRESOURCE(IDS_STATUSREPLACED));
3168 CString sToolTip;
3169 if(simple)
3171 sToolTip.Format(IDS_STATUSLIST_STATUSLINE1,
3172 this->m_nLineAdded,this->m_nLineDeleted,
3173 static_cast<LPCWSTR>(sModified), m_nModified,
3174 static_cast<LPCWSTR>(sAdded), m_nAdded,
3175 static_cast<LPCWSTR>(sDeleted), m_nDeleted,
3176 static_cast<LPCWSTR>(sRenamed), m_nRenamed
3179 else
3181 sToolTip.Format(IDS_STATUSLIST_STATUSLINE2,
3182 this->m_nLineAdded,this->m_nLineDeleted,
3183 static_cast<LPCWSTR>(sNormal), m_nNormal,
3184 static_cast<LPCWSTR>(sUnversioned), m_nUnversioned,
3185 static_cast<LPCWSTR>(sModified), m_nModified,
3186 static_cast<LPCWSTR>(sAdded), m_nAdded,
3187 static_cast<LPCWSTR>(sDeleted), m_nDeleted,
3188 static_cast<LPCWSTR>(sConflicted), m_nConflicted
3191 CString sStats;
3192 sStats.FormatMessage(IDS_COMMITDLG_STATISTICSFORMAT, m_nSelected, GetItemCount());
3193 if (m_pStatLabel)
3194 m_pStatLabel->SetWindowText(sStats);
3196 if (m_pSelectButton)
3198 if (m_nSelected == 0)
3199 m_pSelectButton->SetCheck(BST_UNCHECKED);
3200 else if (m_nSelected != GetItemCount())
3201 m_pSelectButton->SetCheck(BST_INDETERMINATE);
3202 else
3203 m_pSelectButton->SetCheck(BST_CHECKED);
3206 if (m_pConfirmButton)
3207 m_pConfirmButton->EnableWindow(m_nSelected>0);
3209 return sToolTip;
3212 CString CGitStatusListCtrl::GetCommonDirectory(bool bStrict)
3214 CAutoReadLock locker(m_guard);
3215 if (!bStrict)
3217 // not strict means that the selected folder has priority
3218 if (!m_StatusFileList.GetCommonDirectory().IsEmpty())
3219 return m_StatusFileList.GetCommonDirectory().GetWinPath();
3222 CTGitPathList list;
3223 const int nListItems = GetItemCount();
3224 for (int i=0; i<nListItems; ++i)
3226 auto* entry = GetListEntry(i);
3227 if (entry->IsEmpty())
3228 continue;
3229 list.AddPath(*entry);
3231 return list.GetCommonRoot().GetWinPath();
3234 void CGitStatusListCtrl::SelectAll(bool bSelect, bool /*bIncludeNoCommits*/)
3236 CWaitCursor waitCursor;
3237 // block here so the LVN_ITEMCHANGED messages
3238 // get ignored
3239 ScopedInDecrement blocker(m_nBlockItemChangeHandler);
3242 CAutoWriteLock locker(m_guard);
3243 SetRedraw(FALSE);
3245 const int nListItems = GetItemCount();
3246 if (bSelect)
3247 m_nSelected = nListItems;
3248 else
3249 m_nSelected = 0;
3251 for (int i=0; i<nListItems; ++i)
3253 auto path = GetListEntry(i);
3254 if (!path)
3255 continue;
3256 //if ((bIncludeNoCommits)||(entry->GetChangeList().Compare(SVNSLC_IGNORECHANGELIST)))
3257 SetEntryCheck(path,i,bSelect);
3260 SetRedraw(TRUE);
3261 GetStatisticsString();
3262 NotifyCheck();
3265 void CGitStatusListCtrl::Check(DWORD dwCheck, bool check)
3267 CWaitCursor waitCursor;
3268 // block here so the LVN_ITEMCHANGED messages
3269 // get ignored
3271 CAutoWriteLock locker(m_guard);
3272 SetRedraw(FALSE);
3273 ScopedInDecrement blocker(m_nBlockItemChangeHandler);
3275 const int nListItems = GetItemCount();
3277 for (int i = 0; i < nListItems; ++i)
3279 auto entry = GetListEntry(i);
3280 if (!entry)
3281 continue;
3283 DWORD showFlags = entry->m_Action;
3284 if (entry->IsDirectory())
3285 showFlags |= GITSLC_SHOWSUBMODULES;
3286 else
3287 showFlags |= GITSLC_SHOWFILES;
3289 UINT state = ListView_GetItemState(m_hWnd, i, LVIS_STATEIMAGEMASK); // GetCheck would return true for a partially staged file
3290 if (check && (showFlags & dwCheck) && state != INDEXTOSTATEIMAGEMASK(2) && !(entry->IsDirectory() && m_bDoNotAutoselectSubmodules && !(dwCheck & GITSLC_SHOWSUBMODULES)))
3292 // was unchecked or indeterminate
3293 SetEntryCheck(entry, i, true);
3294 if (state == INDEXTOSTATEIMAGEMASK(1)) // was unchecked
3295 ++m_nSelected;
3297 else if (!check && (showFlags & dwCheck) && GetCheck(i))
3299 SetEntryCheck(entry, i, false);
3300 m_nSelected--;
3304 SetRedraw(TRUE);
3305 GetStatisticsString();
3306 NotifyCheck();
3309 void CGitStatusListCtrl::OnLvnGetInfoTip(NMHDR *pNMHDR, LRESULT *pResult)
3311 LPNMLVGETINFOTIP pGetInfoTip = reinterpret_cast<LPNMLVGETINFOTIP>(pNMHDR);
3312 *pResult = 0;
3313 if (CRegDWORD(L"Software\\TortoiseGit\\ShowListFullPathTooltip", TRUE) != TRUE)
3314 return;
3316 CAutoReadWeakLock readLock(m_guard);
3317 if (!readLock.IsAcquired())
3318 return;
3320 auto entry = GetListEntry(pGetInfoTip->iItem);
3322 if (entry)
3323 if (pGetInfoTip->cchTextMax > entry->GetGitPathString().GetLength() + g_Git.m_CurrentDir.GetLength())
3325 CString str = g_Git.CombinePath(entry->GetWinPathString());
3326 wcsncpy_s(pGetInfoTip->pszText, pGetInfoTip->cchTextMax, str.GetBuffer(), pGetInfoTip->cchTextMax - 1);
3330 BOOL CGitStatusListCtrl::OnNMCustomdraw(NMHDR* pNMHDR, LRESULT* pResult)
3332 NMLVCUSTOMDRAW* pLVCD = reinterpret_cast<NMLVCUSTOMDRAW*>( pNMHDR );
3334 // Take the default processing unless we set this to something else below.
3335 *pResult = CDRF_DODEFAULT;
3337 // First thing - check the draw stage. If it's the control's prepaint
3338 // stage, then tell Windows we want messages for every item.
3340 switch (pLVCD->nmcd.dwDrawStage)
3342 case CDDS_PREPAINT:
3343 if (pLVCD->dwItemType == LVCDI_GROUP)
3345 if (CTheme::Instance().IsDarkTheme())
3347 LVGROUP gInfo = { sizeof(LVGROUP) };
3348 gInfo.mask = LVGF_STATE | LVGF_HEADER | LVGF_GROUPID;
3349 SendMessage(LVM_GETGROUPINFO, pLVCD->nmcd.dwItemSpec, (LPARAM)&gInfo);
3351 ::SetTextColor(pLVCD->nmcd.hdc, RGB(200, 0, 0));
3352 RECT labelRect = { 0 };
3353 labelRect.top = LVGGR_LABEL;
3354 SendMessage(LVM_GETGROUPRECT, pLVCD->nmcd.dwItemSpec, (LPARAM)&labelRect);
3355 ::DrawText(pLVCD->nmcd.hdc, gInfo.pszHeader, gInfo.cchHeader, &labelRect, DT_HIDEPREFIX | DT_NOPREFIX | DT_SINGLELINE);
3357 RECT groupRect = { 0 };
3358 groupRect.top = LVGGR_HEADER;
3359 SendMessage(LVM_GETGROUPRECT, pLVCD->nmcd.dwItemSpec, (LPARAM)&groupRect);
3361 auto pen = CreatePen(PS_SOLID, 2, RGB(180, 0, 0));
3362 auto oldPen = SelectObject(pLVCD->nmcd.hdc, pen);
3363 auto y = (groupRect.top + groupRect.bottom) / 2;
3364 MoveToEx(pLVCD->nmcd.hdc, labelRect.right + 4, y, nullptr);
3365 LineTo(pLVCD->nmcd.hdc, groupRect.right, y);
3366 SelectObject(pLVCD->nmcd.hdc, oldPen);
3367 DeleteObject(pen);
3369 *pResult = CDRF_SKIPDEFAULT;
3370 break;
3373 *pResult = CDRF_NOTIFYITEMDRAW;
3374 break;
3375 case CDDS_ITEMPREPAINT:
3377 // This is the prepaint stage for an item. Here's where we set the
3378 // item's text color.
3380 // Tell Windows we want a callback (be)for(e) painting the subitems.
3381 *pResult = CDRF_NOTIFYSUBITEMDRAW;
3383 CAutoReadWeakLock readLock(m_guard, 0);
3384 if (!readLock.IsAcquired())
3385 return TRUE;
3387 if (m_arStatusArray.size() > static_cast<DWORD_PTR>(pLVCD->nmcd.dwItemSpec))
3389 auto entry = GetListEntry(static_cast<int>(pLVCD->nmcd.dwItemSpec));
3390 if (!entry)
3391 return TRUE;
3393 // coloring
3394 // ========
3395 // black : unversioned, normal
3396 // purple : added
3397 // blue : modified
3398 // brown : missing, deleted, replaced
3399 // green : merged (or potential merges)
3400 // red : conflicts or sure conflicts
3401 COLORREF crText;
3402 if(entry->m_Action & CTGitPath::LOGACTIONS_GRAY)
3403 crText = CTheme::Instance().GetThemeColor(GetSysColor(COLOR_GRAYTEXT));
3404 else if(entry->m_Action & CTGitPath::LOGACTIONS_UNMERGED)
3405 crText = CTheme::Instance().GetThemeColor(m_Colors.GetColor(CColors::Conflict), true);
3406 else if(entry->m_Action & (CTGitPath::LOGACTIONS_MODIFIED))
3407 crText = CTheme::Instance().GetThemeColor(m_Colors.GetColor(CColors::Modified), true);
3408 else if(entry->m_Action & (CTGitPath::LOGACTIONS_ADDED|CTGitPath::LOGACTIONS_COPY))
3409 crText = CTheme::Instance().GetThemeColor(m_Colors.GetColor(CColors::Added), true);
3410 else if(entry->m_Action & CTGitPath::LOGACTIONS_DELETED)
3411 crText = CTheme::Instance().GetThemeColor(m_Colors.GetColor(CColors::Deleted), true);
3412 else if(entry->m_Action & CTGitPath::LOGACTIONS_REPLACED)
3413 crText = CTheme::Instance().GetThemeColor(m_Colors.GetColor(CColors::Renamed), true);
3414 else if(entry->m_Action & CTGitPath::LOGACTIONS_MERGED)
3415 crText = CTheme::Instance().GetThemeColor(m_Colors.GetColor(CColors::Merged), true);
3416 else
3417 crText = CTheme::Instance().GetThemeColor(GetSysColor(COLOR_WINDOWTEXT));
3418 // Store the color back in the NMLVCUSTOMDRAW struct.
3419 pLVCD->clrText = crText;
3422 break;
3424 case CDDS_ITEMPREPAINT | CDDS_ITEM | CDDS_SUBITEM:
3425 return FALSE; // allow parent to handle this
3426 break;
3428 return TRUE;
3431 void CGitStatusListCtrl::OnLvnGetdispinfo(NMHDR* pNMHDR, LRESULT* pResult)
3433 auto pDispInfo = reinterpret_cast<NMLVDISPINFO*>(pNMHDR);
3434 *pResult = 0;
3436 // Create a pointer to the item
3437 LV_ITEM* pItem = &(pDispInfo)->item;
3439 CAutoReadWeakLock readLock(m_guard, 0);
3440 if (readLock.IsAcquired())
3442 if (pItem->mask & LVIF_TEXT)
3444 CString text = GetCellText(pItem->iItem, pItem->iSubItem);
3445 lstrcpyn(pItem->pszText, text, pItem->cchTextMax - 1);
3448 else
3449 pItem->mask = 0;
3452 BOOL CGitStatusListCtrl::OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message)
3454 if (pWnd != this)
3455 return CListCtrl::OnSetCursor(pWnd, nHitTest, message);
3456 if (!m_bWaitCursor && !m_bBusy)
3458 HCURSOR hCur = LoadCursor(nullptr, IDC_ARROW);
3459 SetCursor(hCur);
3460 return CListCtrl::OnSetCursor(pWnd, nHitTest, message);
3462 HCURSOR hCur = LoadCursor(nullptr, IDC_WAIT);
3463 SetCursor(hCur);
3464 return TRUE;
3467 void CGitStatusListCtrl::RemoveListEntry(int index)
3469 CAutoWriteLock locker(m_guard);
3470 DeleteItem(index);
3472 m_arStatusArray.erase(m_arStatusArray.cbegin() + index);
3474 #if 0
3475 delete m_arStatusArray[m_arListArray[index]];
3476 m_arStatusArray.erase(m_arStatusArray.begin()+m_arListArray[index]);
3477 m_arListArray.erase(m_arListArray.begin()+index);
3478 for (int i = index; i < static_cast<int>(m_arListArray.size()); ++i)
3480 m_arListArray[i]--;
3482 #endif
3485 ///< Set a checkbox on an entry in the listbox
3486 // NEVER, EVER call SetCheck directly, because you'll end-up with the checkboxes and the 'checked' flag getting out of sync
3487 void CGitStatusListCtrl::SetEntryCheck(CTGitPath* pEntry, int listboxIndex, bool bCheck)
3489 CAutoWriteLock locker(m_guard);
3490 pEntry->m_Checked = bCheck;
3491 m_mapFilenameToChecked[pEntry->GetGitPathString()] = bCheck;
3492 if (m_bThreeStateCheckboxes)
3494 if (bCheck)
3496 GitStageEntry(pEntry);
3497 pEntry->m_stagingStatus = CTGitPath::StagingStatus::TotallyStaged;
3499 else
3501 GitUnstageEntry(pEntry);
3502 pEntry->m_stagingStatus = CTGitPath::StagingStatus::TotallyUnstaged;
3505 SetCheck(listboxIndex, bCheck);
3508 void CGitStatusListCtrl::ResetChecked(const CTGitPath& entry)
3510 CAutoWriteLock locker(m_guard);
3511 CTGitPath adjustedEntry;
3512 if (g_Git.m_CurrentDir[g_Git.m_CurrentDir.GetLength() - 1] == L'\\')
3513 adjustedEntry.SetFromWin(entry.GetWinPathString().Right(entry.GetWinPathString().GetLength() - g_Git.m_CurrentDir.GetLength()));
3514 else
3515 adjustedEntry.SetFromWin(entry.GetWinPathString().Right(entry.GetWinPathString().GetLength() - g_Git.m_CurrentDir.GetLength() - 1));
3516 if (entry.IsDirectory())
3518 STRING_VECTOR toDelete;
3519 for (auto it = m_mapFilenameToChecked.begin(); it != m_mapFilenameToChecked.end(); ++it)
3521 if (adjustedEntry.IsAncestorOf(it->first))
3522 toDelete.emplace_back(it->first);
3524 for (const auto& file : toDelete)
3525 m_mapFilenameToChecked.erase(file);
3526 return;
3528 m_mapFilenameToChecked.erase(adjustedEntry.GetGitPathString());
3531 #if 0
3532 void CGitStatusListCtrl::SetCheckOnAllDescendentsOf(const FileEntry* parentEntry, bool bCheck)
3534 CAutoWriteLock locker(m_guard);
3535 const int nListItems = GetItemCount();
3536 for (int j=0; j< nListItems ; ++j)
3538 FileEntry * childEntry = GetListEntry(j);
3539 ASSERT(childEntry);
3540 if (!childEntry || childEntry == parentEntry)
3541 continue;
3542 if (childEntry->checked != bCheck)
3544 if (parentEntry->path.IsAncestorOf(childEntry->path))
3546 SetEntryCheck(childEntry,j,bCheck);
3547 if(bCheck)
3548 m_nSelected++;
3549 else
3550 m_nSelected--;
3555 #endif
3557 void CGitStatusListCtrl::WriteCheckedNamesToPathList(CTGitPathList& pathList)
3559 pathList.Clear();
3560 CAutoReadLock locker(m_guard);
3561 const int nListItems = GetItemCount();
3562 for (int i = 0; i< nListItems; ++i)
3564 auto entry = GetListEntry(i);
3565 if (entry->m_Checked)
3566 pathList.AddPath(*entry);
3568 pathList.SortByPathname();
3572 /// Build a path list of all the selected items in the list (NOTE - SELECTED, not CHECKED)
3573 void CGitStatusListCtrl::FillListOfSelectedItemPaths(CTGitPathList& pathList, bool /*bNoIgnored*/)
3575 pathList.Clear();
3577 CAutoReadLock locker(m_guard);
3578 POSITION pos = GetFirstSelectedItemPosition();
3579 int index;
3580 while ((index = GetNextSelectedItem(pos)) >= 0)
3582 auto entry = GetListEntry(index);
3583 //if ((bNoIgnored)&&(entry->status == git_wc_status_ignored))
3584 // continue;
3585 pathList.AddPath(*entry);
3589 UINT CGitStatusListCtrl::OnGetDlgCode()
3591 // we want to process the return key and not have that one
3592 // routed to the default pushbutton
3593 return CListCtrl::OnGetDlgCode() | DLGC_WANTALLKEYS;
3596 void CGitStatusListCtrl::OnNMReturn(NMHDR * /*pNMHDR*/, LRESULT *pResult)
3598 *pResult = 0;
3599 CAutoReadWeakLock readLock(m_guard);
3600 if (!readLock.IsAcquired())
3601 return;
3602 if (!CheckMultipleDiffs())
3603 return;
3604 bool needsRefresh = false;
3605 bool resolvedTreeConfict = false;
3606 POSITION pos = GetFirstSelectedItemPosition();
3607 while ( pos )
3609 int index = GetNextSelectedItem(pos);
3610 if (index < 0)
3611 return;
3612 auto file = GetListEntry(index);
3613 if (file == nullptr)
3614 return;
3615 if (file->m_Action & (CTGitPath::LOGACTIONS_UNVER | CTGitPath::LOGACTIONS_IGNORE))
3616 StartDiffWC(index);
3617 else if ((file->m_Action & CTGitPath::LOGACTIONS_UNMERGED))
3619 if (CAppUtils::ConflictEdit(GetParentHWND(), *file, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000), m_bIsRevertTheirMy, GetLogicalParent() ? GetLogicalParent()->GetSafeHwnd() : nullptr))
3621 CString conflictedFile = g_Git.CombinePath(file);
3622 needsRefresh = needsRefresh || !PathFileExists(conflictedFile);
3623 resolvedTreeConfict = resolvedTreeConfict || (file->m_Action & CTGitPath::LOGACTIONS_UNMERGED) == 0;
3626 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))
3627 continue;
3628 else
3630 if (!m_Rev1.IsEmpty() && !m_Rev2.IsEmpty())
3631 StartDiffTwo(index);
3632 else
3633 StartDiff(index);
3636 if (needsRefresh)
3637 RefreshParent();
3638 else if (resolvedTreeConfict)
3640 StoreScrollPos();
3641 Show(m_dwShow, 0, m_bShowFolders, 0, true);
3645 void CGitStatusListCtrl::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags)
3647 // Since we catch all keystrokes (to have the enter key processed here instead
3648 // of routed to the default pushbutton) we have to make sure that other
3649 // keys like Tab and Esc still do what they're supposed to do
3650 // Tab = change focus to next/previous control
3651 // Esc = quit the dialog
3652 switch (nChar)
3654 case (VK_TAB):
3656 ::PostMessage(GetLogicalParent()->GetSafeHwnd(), WM_NEXTDLGCTL, GetKeyState(VK_SHIFT)&0x8000, 0);
3657 return;
3659 break;
3660 case (VK_ESCAPE):
3662 ::SendMessage(GetLogicalParent()->GetSafeHwnd(), WM_CLOSE, 0, 0);
3664 break;
3667 CListCtrl::OnKeyDown(nChar, nRepCnt, nFlags);
3670 void CGitStatusListCtrl::PreSubclassWindow()
3672 __super::PreSubclassWindow();
3673 EnableToolTips(TRUE);
3674 SetWindowTheme(GetSafeHwnd(), L"Explorer", nullptr);
3677 void CGitStatusListCtrl::OnPaint()
3679 LRESULT defres = Default();
3680 if (m_bBusy || m_bEmpty || m_bTooManyItems)
3682 CString str;
3683 if (m_bBusy)
3685 if (m_sBusy.IsEmpty())
3686 str.LoadString(IDS_STATUSLIST_BUSYMSG);
3687 else
3688 str = m_sBusy;
3690 else if (m_bTooManyItems)
3692 if (m_sTooManyItems.IsEmpty())
3693 m_sTooManyItems.LoadString(IDS_STATUSLIST_TOOMANYITEMS);
3694 str = m_sTooManyItems;
3696 else
3698 if (m_sEmpty.IsEmpty())
3699 str.LoadString(IDS_STATUSLIST_EMPTYMSG);
3700 else
3701 str = m_sEmpty;
3703 COLORREF clrText = CTheme::Instance().GetThemeColor(::GetSysColor(COLOR_WINDOWTEXT));
3704 COLORREF clrTextBk;
3705 if (IsWindowEnabled())
3706 clrTextBk = CTheme::Instance().IsDarkTheme() ? CTheme::darkBkColor : ::GetSysColor(COLOR_WINDOW);
3707 else
3708 clrTextBk = CTheme::Instance().GetThemeColor(::GetSysColor(COLOR_3DFACE));
3710 CRect rc;
3711 GetClientRect(&rc);
3712 CHeaderCtrl* pHC = GetHeaderCtrl();
3713 if (pHC)
3715 CRect rcH;
3716 pHC->GetItemRect(0, &rcH);
3717 rc.top += rcH.bottom;
3719 CDC* pDC = GetDC();
3721 CMyMemDC memDC(pDC, &rc);
3723 memDC.SetTextColor(clrText);
3724 memDC.SetBkColor(clrTextBk);
3725 memDC.BitBlt(rc.left, rc.top, rc.Width(), rc.Height(), pDC, rc.left, rc.top, SRCCOPY);
3726 rc.top += 10;
3727 CGdiObject* oldfont = memDC.SelectObject(CGdiObject::FromHandle(m_uiFont));
3728 memDC.DrawText(str, rc, DT_CENTER | DT_VCENTER |
3729 DT_WORDBREAK | DT_NOPREFIX | DT_NOCLIP);
3730 memDC.SelectObject(oldfont);
3732 ReleaseDC(pDC);
3734 if (defres)
3736 // the Default() call did not process the WM_PAINT message!
3737 // Validate the update region ourselves to avoid
3738 // an endless loop repainting
3739 CRect rc;
3740 GetUpdateRect(&rc, FALSE);
3741 if (!rc.IsRectEmpty())
3742 ValidateRect(rc);
3746 void CGitStatusListCtrl::OnBeginDrag(NMHDR* pNMHDR, LRESULT* pResult)
3748 LPNMLISTVIEW pNMLV = reinterpret_cast<LPNMLISTVIEW>(pNMHDR);
3750 CAutoReadLock locker(m_guard);
3752 CTGitPathList pathList;
3753 FillListOfSelectedItemPaths(pathList);
3754 if (pathList.IsEmpty())
3755 return;
3757 auto pdsrc = std::make_unique<CIDropSource>();
3758 if (!pdsrc)
3759 return;
3760 pdsrc->AddRef();
3762 GitDataObject* pdobj = new GitDataObject(pathList, m_Rev2.IsEmpty() ? m_CurrentVersion : m_Rev2);
3763 if (!pdobj)
3764 return;
3765 pdobj->AddRef();
3767 CDragSourceHelper dragsrchelper;
3769 SetRedraw(false);
3770 dragsrchelper.InitializeFromWindow(m_hWnd, pNMLV->ptAction, pdobj);
3771 SetRedraw(true);
3772 //dragsrchelper.InitializeFromBitmap()
3773 pdsrc->m_pIDataObj = pdobj;
3774 pdsrc->m_pIDataObj->AddRef();
3776 // Initiate the Drag & Drop
3777 DWORD dwEffect;
3778 m_bOwnDrag = true;
3779 ::DoDragDrop(pdobj, pdsrc.get(), DROPEFFECT_MOVE | DROPEFFECT_COPY, &dwEffect);
3780 m_bOwnDrag = false;
3781 pdsrc->Release();
3782 pdsrc.release();
3783 pdobj->Release();
3785 *pResult = 0;
3788 bool CGitStatusListCtrl::EnableFileDrop()
3790 m_bFileDropsEnabled = true;
3791 return true;
3794 bool CGitStatusListCtrl::HasPath(const CTGitPath& path)
3796 CAutoReadLock locker(m_guard);
3797 CTGitPath adjustedEntry;
3798 if (g_Git.m_CurrentDir[g_Git.m_CurrentDir.GetLength() - 1] == L'\\')
3799 adjustedEntry.SetFromWin(path.GetWinPathString().Right(path.GetWinPathString().GetLength() - g_Git.m_CurrentDir.GetLength()));
3800 else
3801 adjustedEntry.SetFromWin(path.GetWinPathString().Right(path.GetWinPathString().GetLength() - g_Git.m_CurrentDir.GetLength() - 1));
3802 for (size_t i=0; i < m_arStatusArray.size(); ++i)
3804 if (m_arStatusArray[i]->IsEquivalentTo(adjustedEntry))
3805 return true;
3808 return false;
3811 BOOL CGitStatusListCtrl::PreTranslateMessage(MSG* pMsg)
3813 if (pMsg->message == WM_KEYDOWN)
3815 switch (pMsg->wParam)
3817 case 'A':
3819 if (GetAsyncKeyState(VK_CONTROL)&0x8000)
3821 // select all entries
3822 for (int i=0; i<GetItemCount(); ++i)
3823 SetItemState(i, LVIS_SELECTED, LVIS_SELECTED);
3824 return TRUE;
3827 break;
3828 case 'C':
3829 case VK_INSERT:
3831 if (GetAsyncKeyState(VK_CONTROL)&0x8000)
3833 // copy all selected paths to the clipboard
3834 if (GetAsyncKeyState(VK_SHIFT)&0x8000)
3835 CopySelectedEntriesToClipboard(GITSLC_COLFILENAME | GITSLC_COLSTATUS, IDGITLC_COPYRELPATHS);
3836 else
3837 CopySelectedEntriesToClipboard(GITSLC_COLFILENAME, IDGITLC_COPYRELPATHS);
3838 return TRUE;
3841 break;
3842 case VK_DELETE:
3844 if ((GetSelectedCount() > 0) && (m_dwContextMenus & GITSLC_POPDELETE))
3846 CAutoReadLock locker(m_guard);
3847 auto filepath = GetListEntry(GetSelectionMark());
3848 if (filepath != nullptr && (filepath->m_Action & (CTGitPath::LOGACTIONS_UNVER | CTGitPath::LOGACTIONS_IGNORE)))
3849 DeleteSelectedFiles();
3852 break;
3856 return __super::PreTranslateMessage(pMsg);
3859 bool CGitStatusListCtrl::CopySelectedEntriesToClipboard(DWORD dwCols, int cmd)
3861 if (GetSelectedCount() == 0)
3862 return false;
3864 CString sClipboard;
3866 bool bMultipleColumnSelected = ((dwCols & dwCols - 1) != 0); // multiple columns are selected (clear least signifient bit and check for zero)
3868 #define ADDTOCLIPBOARDSTRING(x) sClipboard += (sClipboard.IsEmpty() || (sClipboard.Right(1)==L"\n")) ? (x) : ('\t' + x)
3869 #define ADDNEWLINETOCLIPBOARDSTRING() sClipboard += (sClipboard.IsEmpty()) ? L"" : L"\r\n"
3871 // first add the column titles as the first line
3872 DWORD selection = 0;
3873 int count = m_ColumnManager.GetColumnCount();
3874 for (int column = 0; column < count; ++column)
3876 if ((dwCols == -1 && m_ColumnManager.IsVisible(column)) || (column < GITSLC_NUMCOLUMNS && (dwCols & (1 << column))))
3878 if (bMultipleColumnSelected)
3879 ADDTOCLIPBOARDSTRING(m_ColumnManager.GetName(column));
3881 selection |= 1 << column;
3885 if (bMultipleColumnSelected)
3886 ADDNEWLINETOCLIPBOARDSTRING();
3888 // maybe clear first line when only one column is selected (btw by select not by dwCols) is simpler(not faster) way
3889 // but why no title on single column output ?
3890 // if (selection & selection-1) == 0 ) sClipboard = "";
3892 CAutoReadLock locker(m_guard);
3894 POSITION pos = GetFirstSelectedItemPosition();
3895 while (pos)
3897 int index = GetNextSelectedItem(pos);
3898 // we selected only cols we want, so not other then select test needed
3899 for (int column = 0; column < count; ++column)
3901 if (cmd && (GITSLC_COLFILENAME & (1 << column)))
3903 auto* entry = GetListEntry(index);
3904 if (entry)
3906 CString sPath;
3907 switch (cmd)
3909 case IDGITLC_COPYFULL:
3910 sPath = g_Git.CombinePath(entry);
3911 break;
3912 case IDGITLC_COPYRELPATHS:
3913 sPath = entry->GetGitPathString();
3914 break;
3915 case IDGITLC_COPYFILENAMES:
3916 sPath = entry->GetFileOrDirectoryName();
3917 break;
3919 ADDTOCLIPBOARDSTRING(sPath);
3922 else if (selection & (1 << column))
3923 ADDTOCLIPBOARDSTRING(GetCellText(index, column));
3926 ADDNEWLINETOCLIPBOARDSTRING();
3929 return CStringUtils::WriteAsciiStringToClipboard(sClipboard);
3932 bool CGitStatusListCtrl::HasChangelistInSelection()
3934 CAutoReadLock locker(m_guard);
3935 POSITION pos = GetFirstSelectedItemPosition();
3936 int index;
3937 while ((index = GetNextSelectedItem(pos)) >= 0)
3939 auto pGitPath = GetListEntry(index);
3940 auto it = m_pathToChangelist.find(pGitPath->GetGitPathString());
3941 if (it != m_pathToChangelist.cend())
3942 return true;
3944 return false;
3947 bool CGitStatusListCtrl::PrepareGroups(bool bForce /* = false */)
3949 CAutoWriteLock locker(m_guard);
3950 bool bHasGroups=false;
3951 int max =0;
3953 for (size_t i = 0; i < m_arStatusArray.size(); ++i)
3955 int ParentNo = m_arStatusArray[i]->m_ParentNo&PARENT_MASK;
3956 if( ParentNo > max)
3957 max=m_arStatusArray[i]->m_ParentNo&PARENT_MASK;
3960 if (((m_dwShow & GITSLC_SHOWUNVERSIONED) && !m_UnRevFileList.IsEmpty()) ||
3961 ((m_dwShow & GITSLC_SHOWIGNORED) && !m_IgnoreFileList.IsEmpty()) ||
3962 (m_dwShow & (GITSLC_SHOWASSUMEVALID | GITSLC_SHOWSKIPWORKTREE) && !m_LocalChangesIgnoredFileList.IsEmpty()) ||
3963 max > 0 || !m_pathToChangelist.empty() || bForce)
3965 bHasGroups = true;
3968 RemoveAllGroups();
3969 EnableGroupView(bHasGroups);
3971 wchar_t groupname[1024] = { 0 };
3972 int groupindex = 0;
3974 if(bHasGroups)
3976 LVGROUP grp = {0};
3977 grp.cbSize = sizeof(LVGROUP);
3978 grp.mask = LVGF_ALIGN | LVGF_GROUPID | LVGF_HEADER;
3979 groupindex=0;
3981 //if(m_UnRevFileList.GetCount()>0)
3982 if(max >0)
3984 wcsncpy_s(groupname, static_cast<LPCWSTR>(CString(MAKEINTRESOURCE(IDS_STATUSLIST_GROUP_MERGEDFILES))), _TRUNCATE);
3985 grp.pszHeader = groupname;
3986 grp.iGroupId = MERGE_MASK;
3987 grp.uAlign = LVGA_HEADER_LEFT;
3988 InsertGroup(0, &grp);
3990 CAutoRepository repository(g_Git.GetGitRepository());
3991 if (!repository)
3992 MessageBox(CGit::GetLibGit2LastErr(L"Could not open repository."), L"TortoiseGit", MB_OK | MB_ICONERROR);
3993 for (groupindex = 0; groupindex <= max; ++groupindex)
3995 CString str;
3996 str.Format(IDS_STATUSLIST_GROUP_DIFFWITHPARENT, groupindex + 1);
3997 if (repository)
3999 CString rev;
4000 rev.Format(L"%s^%d", static_cast<LPCWSTR>(m_CurrentVersion.ToString()), groupindex + 1);
4001 CGitHash hash;
4002 if (!CGit::GetHash(repository, hash, rev))
4003 str += L": " + hash.ToString(g_Git.GetShortHASHLength());
4005 grp.pszHeader = str.GetBuffer();
4006 str.ReleaseBuffer();
4007 grp.iGroupId = groupindex;
4008 grp.uAlign = LVGA_HEADER_LEFT;
4009 InsertGroup(groupindex, &grp);
4012 else
4014 wcsncpy_s(groupname, static_cast<LPCWSTR>(CString(MAKEINTRESOURCE(IDS_STATUSLIST_GROUP_MODIFIEDFILES))), _TRUNCATE);
4015 grp.pszHeader = groupname;
4016 grp.iGroupId = groupindex;
4017 grp.uAlign = LVGA_HEADER_LEFT;
4018 InsertGroup(groupindex++, &grp);
4021 wcsncpy_s(groupname, static_cast<LPCWSTR>(CString(MAKEINTRESOURCE(IDS_STATUSLIST_GROUP_NOTVERSIONEDFILES))), _TRUNCATE);
4022 grp.pszHeader = groupname;
4023 grp.iGroupId = groupindex;
4024 grp.uAlign = LVGA_HEADER_LEFT;
4025 InsertGroup(groupindex++, &grp);
4028 //if(m_IgnoreFileList.GetCount()>0)
4030 wcsncpy_s(groupname, static_cast<LPCWSTR>(CString(MAKEINTRESOURCE(IDS_STATUSLIST_GROUP_IGNOREDFILES))), _TRUNCATE);
4031 grp.pszHeader = groupname;
4032 grp.iGroupId = groupindex;
4033 grp.uAlign = LVGA_HEADER_LEFT;
4034 InsertGroup(groupindex++, &grp);
4038 wcsncpy_s(groupname, static_cast<LPCWSTR>(CString(MAKEINTRESOURCE(IDS_STATUSLIST_GROUP_IGNORELOCALCHANGES))), _TRUNCATE);
4039 grp.pszHeader = groupname;
4040 grp.iGroupId = groupindex;
4041 grp.uAlign = LVGA_HEADER_LEFT;
4042 InsertGroup(groupindex++, &grp);
4047 m_bHasIgnoreGroup = false;
4049 // now add the items which don't belong to a group
4050 LVGROUP grp = {0};
4051 grp.cbSize = sizeof(LVGROUP);
4052 grp.mask = LVGF_ALIGN | LVGF_GROUPID | LVGF_HEADER;
4053 CString sUnassignedName(MAKEINTRESOURCE(IDS_STATUSLIST_UNASSIGNED_CHANGESET));
4054 wcsncpy_s(groupname, static_cast<LPCWSTR>(sUnassignedName), _TRUNCATE);
4055 grp.pszHeader = groupname;
4056 grp.iGroupId = groupindex;
4057 grp.uAlign = LVGA_HEADER_LEFT;
4058 InsertGroup(groupindex++, &grp);
4060 // Refresh changelist to group index map
4061 m_changelists.clear();
4062 for (auto it = m_pathToChangelist.cbegin(); it != m_pathToChangelist.cend(); ++it)
4063 m_changelists.insert_or_assign(it->second, 0);
4065 for (auto it = m_changelists.begin(); it != m_changelists.end(); ++it)
4067 if (it->first.Compare(GITSLC_IGNORECHANGELIST) != 0)
4069 LVGROUP grpchglst = { 0 };
4070 grpchglst.cbSize = sizeof(LVGROUP);
4071 grpchglst.mask = LVGF_ALIGN | LVGF_GROUPID | LVGF_HEADER;
4072 wcsncpy_s(groupname, it->first, _TRUNCATE);
4073 grpchglst.pszHeader = groupname;
4074 grpchglst.iGroupId = groupindex;
4075 grpchglst.uAlign = LVGA_HEADER_LEFT;
4076 it->second = InsertGroup(groupindex++, &grpchglst);
4078 else
4079 m_bHasIgnoreGroup = true;
4082 if (m_bHasIgnoreGroup)
4084 // and now add the group 'ignore-on-commit'
4085 std::map<CString,int>::iterator it = m_changelists.find(GITSLC_IGNORECHANGELIST);
4086 if (it != m_changelists.end())
4088 grp.cbSize = sizeof(LVGROUP);
4089 grp.mask = LVGF_ALIGN | LVGF_GROUPID | LVGF_HEADER;
4090 wcsncpy_s(groupname, GITSLC_IGNORECHANGELIST, _TRUNCATE);
4091 grp.pszHeader = groupname;
4092 grp.iGroupId = groupindex;
4093 grp.uAlign = LVGA_HEADER_LEFT;
4094 it->second = InsertGroup(groupindex, &grp);
4097 return bHasGroups;
4100 void CGitStatusListCtrl::NotifyCheck()
4102 CWnd* pParent = GetLogicalParent();
4103 if (pParent && pParent->GetSafeHwnd())
4105 pParent->SendMessage(GITSLNM_CHECKCHANGED, m_nSelected);
4109 int CGitStatusListCtrl::UpdateFileList(const CTGitPathList* list, bool getStagingStatus)
4111 CAutoWriteLock locker(m_guard);
4112 m_CurrentVersion.Empty();
4114 ATLASSERT(!(m_amend && !m_bIncludedStaged)); // just a safeguard that we always show all files if we want to amend (amending should only be the used from commitdlg)
4115 g_Git.GetWorkingTreeChanges(m_StatusFileList, m_amend, list, m_bIncludedStaged, getStagingStatus);
4117 BOOL bDeleteChecked = FALSE;
4118 int deleteFromIndex = 0;
4119 bool needsRefresh = false;
4120 for (int i = 0; i < m_StatusFileList.GetCount(); ++i)
4122 auto gitpatch = const_cast<CTGitPath*>(&m_StatusFileList[i]);
4123 gitpatch->m_Checked = TRUE;
4125 if ((gitpatch->m_Action & (CTGitPath::LOGACTIONS_ADDED | CTGitPath::LOGACTIONS_REPLACED | CTGitPath::LOGACTIONS_MODIFIED)) && !gitpatch->Exists())
4127 if (!bDeleteChecked)
4129 CString message;
4130 message.Format(IDS_ASK_REMOVE_FROM_INDEX, gitpatch->GetWinPath());
4131 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);
4133 if (deleteFromIndex == 1)
4135 if (CString err; g_Git.Run(L"git.exe checkout -- \"" + gitpatch->GetWinPathString() + L'"', &err, CP_UTF8))
4136 MessageBox(L"Restoring from index failed:\n" + err, L"TortoiseGit", MB_ICONERROR);
4137 else
4138 needsRefresh = true;
4140 else if (deleteFromIndex == 2)
4142 if (CString err; g_Git.Run(L"git.exe rm -f --cache -- \"" + gitpatch->GetWinPathString() + L'"', &err, CP_UTF8))
4143 MessageBox(L"Removing from index failed:\n" + err, L"TortoiseGit", MB_ICONERROR);
4144 else
4145 needsRefresh = true;
4149 m_arStatusArray.push_back(&m_StatusFileList[i]);
4152 if (needsRefresh)
4153 MessageBox(L"Due to changes to the index, please refresh the dialog (e.g., by pressing F5).", L"TortoiseGit", MB_ICONINFORMATION);
4155 return 0;
4158 int CGitStatusListCtrl::UpdateWithGitPathList(CTGitPathList &list)
4160 CAutoWriteLock locker(m_guard);
4161 m_arStatusArray.clear();
4162 for (int i = 0; i < list.GetCount(); ++i)
4164 auto gitpath = const_cast<CTGitPath*>(&list[i]);
4166 if(gitpath ->m_Action & CTGitPath::LOGACTIONS_HIDE)
4167 continue;
4169 gitpath->m_Checked = TRUE;
4170 m_arStatusArray.push_back(&list[i]);
4172 return 0;
4175 int CGitStatusListCtrl::InsertUnRevListFromPreCalculatedList(const CTGitPathList& list)
4177 CAutoWriteLock locker(m_guard);
4178 m_UnRevFileList = list;
4179 for (int i = 0; i < m_UnRevFileList.GetCount(); ++i)
4181 auto gitpatch = const_cast<CTGitPath*>(&m_UnRevFileList[i]);
4182 gitpatch->m_Checked = FALSE;
4183 m_arStatusArray.push_back(&m_UnRevFileList[i]);
4185 return 0;
4188 int CGitStatusListCtrl::UpdateLFSLockedFileList(bool onlyExisting)
4190 CAutoWriteLock locker(m_guard);
4191 if (CString err; m_LocksFileList.FillLFSLocks(GITSLC_SHOWLFSLOCKS, &err))
4193 MessageBox(L"Failed to get LFS locks file list\n" + err, L"TortoiseGit", MB_OK | MB_ICONERROR);
4194 return -1;
4197 AppendLFSLocks(onlyExisting);
4199 return 0;
4202 int CGitStatusListCtrl::UpdateUnRevFileList(const CTGitPathList* List)
4204 CAutoWriteLock locker(m_guard);
4205 if (CString err; m_UnRevFileList.FillUnRev(CTGitPath::LOGACTIONS_UNVER, List, &err))
4207 MessageBox(L"Failed to get UnRev file list\n" + err, L"TortoiseGit", MB_OK | MB_ICONERROR);
4208 return -1;
4211 if (m_StatusFileList.m_Action & CTGitPath::LOGACTIONS_DELETED)
4213 int unrev = 0;
4214 int status = 0;
4215 while (unrev < m_UnRevFileList.GetCount() && status < m_StatusFileList.GetCount())
4217 auto cmp = CTGitPath::Compare(m_UnRevFileList[unrev], m_StatusFileList[status]);
4218 if (cmp < 1)
4220 ++unrev;
4221 continue;
4223 if (cmp == 1)
4224 m_UnRevFileList.RemovePath(m_StatusFileList[status]);
4225 ++status;
4229 for (int i = 0; i < m_UnRevFileList.GetCount(); ++i)
4231 auto gitpatch = const_cast<CTGitPath*>(&m_UnRevFileList[i]);
4232 gitpatch->m_Checked = FALSE;
4233 m_arStatusArray.push_back(&m_UnRevFileList[i]);
4235 return 0;
4238 int CGitStatusListCtrl::UpdateIgnoreFileList(const CTGitPathList* List)
4240 CAutoWriteLock locker(m_guard);
4241 if (CString err; m_IgnoreFileList.FillUnRev(CTGitPath::LOGACTIONS_IGNORE, List, &err))
4243 MessageBox(L"Failed to get Ignore file list\n" + err, L"TortoiseGit", MB_OK | MB_ICONERROR);
4244 return -1;
4247 for (int i = 0; i < m_IgnoreFileList.GetCount(); ++i)
4249 auto gitpatch = const_cast<CTGitPath*>(&m_IgnoreFileList[i]);
4250 gitpatch->m_Checked = FALSE;
4251 m_arStatusArray.push_back(&m_IgnoreFileList[i]);
4253 return 0;
4256 int CGitStatusListCtrl::UpdateLocalChangesIgnoredFileList(const CTGitPathList* list)
4258 CAutoWriteLock locker(m_guard);
4259 m_LocalChangesIgnoredFileList.FillBasedOnIndexFlags(GIT_INDEX_ENTRY_VALID, GIT_INDEX_ENTRY_SKIP_WORKTREE, list);
4260 for (int i = 0; i < m_LocalChangesIgnoredFileList.GetCount(); ++i)
4262 auto gitpatch = const_cast<CTGitPath*>(&m_LocalChangesIgnoredFileList[i]);
4263 gitpatch->m_Checked = FALSE;
4264 m_arStatusArray.push_back(&m_LocalChangesIgnoredFileList[i]);
4266 return 0;
4269 int CGitStatusListCtrl::UpdateFileList(int mask, bool once, const CTGitPathList* pList, bool getStagingStatus)
4271 CAutoWriteLock locker(m_guard);
4272 auto List = (pList && pList->GetCount() >= 1 && !(*pList)[0].GetWinPathString().IsEmpty()) ? pList : nullptr;
4273 if(mask&CGitStatusListCtrl::FILELIST_MODIFY)
4275 if(once || (!(m_FileLoaded&CGitStatusListCtrl::FILELIST_MODIFY)))
4277 UpdateFileList(List, getStagingStatus);
4278 m_FileLoaded|=CGitStatusListCtrl::FILELIST_MODIFY;
4281 if (mask & CGitStatusListCtrl::FILELIST_UNVER || mask & CGitStatusListCtrl::FILELIST_IGNORE)
4283 if(once || (!(m_FileLoaded&CGitStatusListCtrl::FILELIST_UNVER)))
4285 UpdateUnRevFileList(List);
4286 m_FileLoaded|=CGitStatusListCtrl::FILELIST_UNVER;
4288 if(mask&CGitStatusListCtrl::FILELIST_IGNORE && (once || (!(m_FileLoaded&CGitStatusListCtrl::FILELIST_IGNORE))))
4290 UpdateIgnoreFileList(List);
4291 m_FileLoaded |= CGitStatusListCtrl::FILELIST_IGNORE;
4294 if (mask & CGitStatusListCtrl::FILELIST_LOCALCHANGESIGNORED && (once || (!(m_FileLoaded & CGitStatusListCtrl::FILELIST_LOCALCHANGESIGNORED))))
4296 UpdateLocalChangesIgnoredFileList(List);
4297 m_FileLoaded |= CGitStatusListCtrl::FILELIST_LOCALCHANGESIGNORED;
4299 if (mask & CGitStatusListCtrl::FILELIST_LOCKS)
4301 if (once || (!(m_FileLoaded&CGitStatusListCtrl::FILELIST_LOCKS)))
4303 UpdateLFSLockedFileList(false);
4304 m_FileLoaded |= CGitStatusListCtrl::FILELIST_LOCKS;
4308 return 0;
4311 void CGitStatusListCtrl::Clear()
4313 CAutoWriteLock locker(m_guard);
4314 m_FileLoaded=0;
4315 this->DeleteAllItems();
4316 this->m_arListArray.clear();
4317 this->m_arStatusArray.clear();
4318 this->m_changelists.clear();
4319 this->m_pathToChangelist.clear();
4322 bool CGitStatusListCtrl::CheckMultipleDiffs()
4324 UINT selCount = GetSelectedCount();
4325 if (selCount > max(DWORD(3), static_cast<DWORD>(CRegDWORD(L"Software\\TortoiseGit\\NumDiffWarning", 10))))
4327 CString message;
4328 message.Format(IDS_STATUSLIST_WARN_MAXDIFF, selCount);
4329 return MessageBox(message, L"TortoiseGit", MB_YESNO | MB_ICONQUESTION) == IDYES;
4331 return true;
4334 //////////////////////////////////////////////////////////////////////////
4335 bool CGitStatusListCtrlDropTarget::OnDrop(FORMATETC* pFmtEtc, STGMEDIUM& medium, DWORD* /*pdwEffect*/, POINTL pt)
4337 if (pFmtEtc->cfFormat == CF_HDROP && medium.tymed == TYMED_HGLOBAL)
4339 HDROP hDrop = static_cast<HDROP>(GlobalLock(medium.hGlobal));
4340 if (hDrop)
4342 wchar_t szFileName[MAX_PATH] = { 0 };
4344 UINT cFiles = DragQueryFile(hDrop, 0xFFFFFFFF, nullptr, 0);
4346 POINT clientpoint;
4347 clientpoint.x = pt.x;
4348 clientpoint.y = pt.y;
4349 ScreenToClient(m_hTargetWnd, &clientpoint);
4350 if ((m_pGitStatusListCtrl->IsGroupViewEnabled()) && (m_pGitStatusListCtrl->GetGroupFromPoint(&clientpoint) >= 0))
4352 #if 0
4353 CTGitPathList changelistItems;
4354 for (UINT i = 0; i < cFiles; ++i)
4356 if (DragQueryFile(hDrop, i, szFileName, _countof(szFileName)))
4357 changelistItems.AddPath(CTGitPath(szFileName));
4359 // find the changelist name
4360 CString sChangelist;
4361 LONG_PTR nGroup = m_pGitStatusListCtrl->GetGroupFromPoint(&clientpoint);
4362 for (std::map<CString, int>::iterator it = m_pGitStatusListCtrl->m_changelists.begin(); it != m_pGitStatusListCtrl->m_changelists.end(); ++it)
4363 if (it->second == nGroup)
4364 sChangelist = it->first;
4366 if (!sChangelist.IsEmpty())
4368 CGit git;
4369 if (git.AddToChangeList(changelistItems, sChangelist, git_depth_empty))
4371 for (int l=0; l<changelistItems.GetCount(); ++l)
4373 int index = m_pGitStatusListCtrl->GetIndex(changelistItems[l]);
4374 if (index >= 0)
4376 auto e = m_pGitStatusListCtrl->GetListEntry(index);
4377 if (e)
4379 e->changelist = sChangelist;
4380 if (!e->IsFolder())
4382 if (m_pGitStatusListCtrl->m_changelists.find(e->changelist) != m_pGitStatusListCtrl->m_changelists.end())
4383 m_pGitStatusListCtrl->SetItemGroup(index, m_pGitStatusListCtrl->m_changelists[e->changelist]);
4384 else
4385 m_pGitStatusListCtrl->SetItemGroup(index, 0);
4389 else
4391 HWND hParentWnd = GetParent(m_hTargetWnd);
4392 if (hParentWnd)
4393 ::SendMessage(hParentWnd, CGitStatusListCtrl::GITSLNM_ADDFILE, 0, reinterpret_cast<LPARAM>(changelistItems[l].GetWinPath()));
4397 else
4398 CMessageBox::Show(m_pGitStatusListCtrl->m_hWnd, git.GetLastErrorMessage(), L"TortoiseGit", MB_ICONERROR);
4400 else
4402 SVN git;
4403 if (git.RemoveFromChangeList(changelistItems, CStringArray(), git_depth_empty))
4405 for (int l=0; l<changelistItems.GetCount(); ++l)
4407 int index = m_pGitStatusListCtrl->GetIndex(changelistItems[l]);
4408 if (index >= 0)
4410 auto e = m_pGitStatusListCtrl->GetListEntry(index);
4411 if (e)
4413 e->changelist = sChangelist;
4414 m_pGitStatusListCtrl->SetItemGroup(index, 0);
4417 else
4419 HWND hParentWnd = GetParent(m_hTargetWnd);
4420 if (hParentWnd)
4421 ::SendMessage(hParentWnd, CGitStatusListCtrl::GITSLNM_ADDFILE, 0, reinterpret_cast<LPARAM>(changelistItems[l].GetWinPath()));
4425 else
4426 CMessageBox::Show(m_pGitStatusListCtrl->m_hWnd, git.GetLastErrorMessage(), L"TortoiseGit", MB_ICONERROR);
4428 #endif
4430 else
4432 for (UINT i = 0; i < cFiles; ++i)
4434 if (!DragQueryFile(hDrop, i, szFileName, _countof(szFileName)))
4435 continue;
4437 HWND hParentWnd = GetParent(m_hTargetWnd);
4438 if (hParentWnd)
4439 ::SendMessage(hParentWnd, CGitStatusListCtrl::GITSLNM_ADDFILE, 0, reinterpret_cast<LPARAM>(szFileName));
4443 GlobalUnlock(medium.hGlobal);
4445 return true; //let base free the medium
4448 HRESULT STDMETHODCALLTYPE CGitStatusListCtrlDropTarget::DragOver(DWORD grfKeyState, POINTL pt, DWORD __RPC_FAR* pdwEffect)
4450 CIDropTarget::DragOver(grfKeyState, pt, pdwEffect);
4451 *pdwEffect = DROPEFFECT_COPY;
4452 if (m_pGitStatusListCtrl)
4454 POINT clientpoint;
4455 clientpoint.x = pt.x;
4456 clientpoint.y = pt.y;
4457 ScreenToClient(m_hTargetWnd, &clientpoint);
4458 if ((m_pGitStatusListCtrl->IsGroupViewEnabled()) && (m_pGitStatusListCtrl->GetGroupFromPoint(&clientpoint) >= 0))
4459 *pdwEffect = DROPEFFECT_NONE;
4460 else if ((!m_pGitStatusListCtrl->m_bFileDropsEnabled) || (m_pGitStatusListCtrl->m_bOwnDrag))
4461 *pdwEffect = DROPEFFECT_NONE;
4463 return S_OK;
4466 void CGitStatusListCtrl::FilesExport()
4468 CAutoReadLock locker(m_guard);
4469 CString exportDir;
4470 // export all changed files to a folder
4471 CBrowseFolder browseFolder;
4472 browseFolder.m_style = BIF_EDITBOX | BIF_NEWDIALOGSTYLE | BIF_RETURNFSANCESTORS | BIF_RETURNONLYFSDIRS;
4473 if (browseFolder.Show(GetParentHWND(), exportDir) != CBrowseFolder::OK)
4474 return;
4476 POSITION pos = GetFirstSelectedItemPosition();
4477 int index;
4478 while ((index = GetNextSelectedItem(pos)) >= 0)
4480 auto fd = GetListEntry(index);
4481 // we cannot export directories or folders
4482 if ((fd->m_Action & CTGitPath::LOGACTIONS_DELETED) || fd->IsDirectory())
4483 continue;
4485 CPathUtils::MakeSureDirectoryPathExists(exportDir + L'\\' + fd->GetContainingDirectory().GetWinPathString());
4486 CString filename = exportDir + L'\\' + fd->GetWinPathString();
4487 if (m_CurrentVersion.IsEmpty())
4489 if (!CopyFile(g_Git.CombinePath(fd), filename, false))
4491 MessageBox(CFormatMessageWrapper(), L"TortoiseGit", MB_OK | MB_ICONERROR);
4492 return;
4495 else
4497 if (g_Git.GetOneFile(m_CurrentVersion.ToString(), *fd, filename))
4499 CString out;
4500 out.FormatMessage(IDS_STATUSLIST_CHECKOUTFILEFAILED, static_cast<LPCWSTR>(fd->GetGitPathString()), static_cast<LPCWSTR>(m_CurrentVersion.ToString()), static_cast<LPCWSTR>(filename));
4501 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)
4502 return;
4508 void CGitStatusListCtrl::FileSaveAs(const CTGitPath* path)
4510 CAutoReadLock locker(m_guard);
4511 CString filename;
4512 filename.Format(L"%s\\%s-%s%s", static_cast<LPCWSTR>(g_Git.CombinePath(path->GetContainingDirectory())), static_cast<LPCWSTR>(path->GetBaseFilename()), static_cast<LPCWSTR>(m_CurrentVersion.ToString(g_Git.GetShortHASHLength())), static_cast<LPCWSTR>(path->GetFileExtension()));
4513 if (!CAppUtils::FileOpenSave(filename, nullptr, 0, 0, false, GetSafeHwnd()))
4514 return;
4515 if (m_CurrentVersion.IsEmpty())
4517 if (!CopyFile(g_Git.CombinePath(path), filename, false))
4519 MessageBox(CFormatMessageWrapper(), L"TortoiseGit", MB_OK | MB_ICONERROR);
4520 return;
4523 else
4525 if (g_Git.GetOneFile(m_CurrentVersion.ToString(), *path, filename))
4527 CString out;
4528 out.FormatMessage(IDS_STATUSLIST_CHECKOUTFILEFAILED, static_cast<LPCWSTR>(path->GetGitPathString()), static_cast<LPCWSTR>(m_CurrentVersion.ToString()), static_cast<LPCWSTR>(filename));
4529 CMessageBox::Show(GetParentHWND(), g_Git.GetGitLastErr(out, CGit::GIT_CMD_GETONEFILE), L"TortoiseGit", MB_OK);
4530 return;
4535 bool CGitStatusListCtrl::GetParentCommitInfo(const CGitHash& hash, const int parentNo, CGitHash& parentHash, CString& title)
4537 if (hash.IsEmpty())
4538 return false;
4540 CString revStr;
4541 revStr.Format(L"%s^%d", static_cast<LPCWSTR>(hash.ToString()), parentNo + 1);
4543 if (g_Git.GetHash(parentHash, revStr) != 0)
4544 return false;
4546 GitRev rev;
4547 if (rev.GetCommit(parentHash.ToString()) == 0)
4549 CString commitTitle = rev.GetSubject();
4550 if (commitTitle.GetLength() > 23) // 20 + length("...")
4552 commitTitle.Truncate(20);
4553 commitTitle += L"...";
4555 title.Format(L"\"%s\" (%s)", static_cast<LPCWSTR>(CStringUtils::EscapeAccellerators(commitTitle)), static_cast<LPCWSTR>(parentHash.ToString(g_Git.GetShortHASHLength())));
4557 else
4558 title.Format(L"(%s)", static_cast<LPCWSTR>(parentHash.ToString(g_Git.GetShortHASHLength())));
4560 return true;
4563 int CGitStatusListCtrl::RevertSelectedItemToVersion(bool parent)
4565 CAutoReadLock locker(m_guard);
4566 if (m_CurrentVersion.IsEmpty())
4567 return 0;
4569 const bool useRecycleBin = CRegDWORD(L"Software\\TortoiseGit\\RevertWithRecycleBin", TRUE);
4571 POSITION pos = GetFirstSelectedItemPosition();
4572 int index;
4573 std::map<CString, int> versionMap;
4574 while ((index = GetNextSelectedItem(pos)) >= 0)
4576 auto fentry = GetListEntry(index);
4577 CString version;
4578 if (parent)
4580 CGitHash parentHash;
4581 CString title;
4582 if (!GetParentCommitInfo(m_CurrentVersion, fentry->m_ParentNo & PARENT_MASK, parentHash, title))
4584 MessageBox(g_Git.GetGitLastErr(L"Could not get parent hash for \"" + m_CurrentVersion.ToString() + L"\"."), L"TortoiseGit", MB_ICONERROR);
4585 continue;
4588 version = parentHash.ToString();
4590 else
4591 version = m_CurrentVersion.ToString();
4593 CString filename = fentry->GetGitPathString();
4594 if (!fentry->GetGitOldPathString().IsEmpty())
4595 filename = fentry->GetGitOldPathString();
4596 boolean isAdded = parent && (fentry->m_Action & CTGitPath::LOGACTIONS_ADDED);
4597 if (CTGitPath path = g_Git.CombinePath(filename); useRecycleBin && !isAdded && !path.IsDirectory())
4598 path.Delete(useRecycleBin, true);
4599 CString cmd, out;
4600 cmd.Format(L"git.exe checkout %s -- \"%s\"", static_cast<LPCWSTR>(version), static_cast<LPCWSTR>(filename)); // remember to use --end-of-options as soon as version is not a hash any more
4601 if (isAdded) // HACK for issue #4097
4602 cmd.Format(L"git.exe rm --cached --ignore-unmatch -- \"%s\"", static_cast<LPCWSTR>(filename));
4603 if (g_Git.Run(cmd, &out, CP_UTF8))
4605 if (CMessageBox::Show(GetSafeHwnd(), out, L"TortoiseGit", 1, IDI_WARNING, CString(MAKEINTRESOURCE(IDS_IGNOREBUTTON)), CString(MAKEINTRESOURCE(IDS_ABORTBUTTON))) == 2)
4606 break;
4608 else
4609 versionMap[version]++;
4612 CString out;
4613 for (auto it = versionMap.cbegin(); it != versionMap.cend(); ++it)
4615 CString versionEntry;
4616 versionEntry.FormatMessage(IDS_STATUSLIST_FILESREVERTED, it->second, static_cast<LPCWSTR>(it->first));
4617 out += versionEntry + L"\r\n";
4619 if (!out.IsEmpty())
4621 RefreshParent();
4622 CMessageBox::Show(GetParentHWND(), out, L"TortoiseGit", MB_OK);
4624 return 0;
4627 void CGitStatusListCtrl::OpenFile(const CTGitPath* filepath, int mode)
4629 CString file;
4630 if (m_CurrentVersion.IsEmpty())
4631 file = g_Git.CombinePath(filepath);
4632 else
4634 file = CTempFiles::Instance().GetTempFilePath(false, *filepath, m_CurrentVersion).GetWinPathString();
4635 CString cmd,out;
4636 if(g_Git.GetOneFile(m_CurrentVersion.ToString(), *filepath, file))
4638 out.FormatMessage(IDS_STATUSLIST_CHECKOUTFILEFAILED, static_cast<LPCWSTR>(filepath->GetGitPathString()), static_cast<LPCWSTR>(m_CurrentVersion.ToString()), static_cast<LPCWSTR>(file));
4639 CMessageBox::Show(GetParentHWND(), g_Git.GetGitLastErr(out, CGit::GIT_CMD_GETONEFILE), L"TortoiseGit", MB_OK);
4640 return;
4642 SetFileAttributes(file, FILE_ATTRIBUTE_READONLY);
4644 if(mode == ALTERNATIVEEDITOR)
4646 CAppUtils::LaunchAlternativeEditor(file);
4647 return;
4650 if (mode == OPEN)
4651 CAppUtils::ShellOpen(file, GetSafeHwnd());
4652 else
4653 CAppUtils::ShowOpenWithDialog(file, GetSafeHwnd());
4656 void CGitStatusListCtrl::DeleteSelectedFiles()
4658 CAutoWriteLock locker(m_guard);
4659 //Collect paths
4660 std::vector<int> selectIndex;
4662 POSITION pos = GetFirstSelectedItemPosition();
4663 int index;
4664 while ((index = GetNextSelectedItem(pos)) >= 0)
4665 selectIndex.push_back(index);
4667 CAutoRepository repo = g_Git.GetGitRepository();
4668 if (!repo)
4670 MessageBox(g_Git.GetLibGit2LastErr(L"Could not open repository."), L"TortoiseGit", MB_OK);
4671 return;
4673 CAutoIndex gitIndex;
4674 if (git_repository_index(gitIndex.GetPointer(), repo))
4676 g_Git.GetLibGit2LastErr(L"Could not open index.");
4677 return;
4679 int needWriteIndex = 0;
4681 //Create file-list ('\0' separated) for SHFileOperation
4682 CString filelist;
4683 for (size_t i = 0; i < selectIndex.size(); ++i)
4685 index = selectIndex[i];
4687 auto path = GetListEntry(index);
4688 if (path == nullptr)
4689 continue;
4691 // do not report errors as we could remove an unversioned file
4692 needWriteIndex += git_index_remove_bypath(gitIndex, CUnicodeUtils::GetUTF8(path->GetGitPathString())) == 0 ? 1 : 0;
4694 if (!path->Exists())
4695 continue;
4697 filelist += path->GetWinPathString();
4698 filelist += L'|';
4700 filelist += L'|';
4701 const int len = filelist.GetLength();
4702 auto buf = std::make_unique<wchar_t[]>(len + sizeof(wchar_t));
4703 wcscpy_s(buf.get(), len + sizeof(wchar_t), filelist);
4704 CStringUtils::PipesToNulls(buf.get(), len);
4705 SHFILEOPSTRUCT fileop;
4706 fileop.hwnd = this->m_hWnd;
4707 fileop.wFunc = FO_DELETE;
4708 fileop.pFrom = buf.get();
4709 fileop.pTo = nullptr;
4710 fileop.fFlags = FOF_NO_CONNECTED_ELEMENTS | ((GetAsyncKeyState(VK_SHIFT) & 0x8000) ? 0 : FOF_ALLOWUNDO);
4711 fileop.lpszProgressTitle = L"deleting file";
4712 int result = SHFileOperation(&fileop);
4714 if ((result == 0 || len == 1) && (!fileop.fAnyOperationsAborted))
4716 if (needWriteIndex && git_index_write(gitIndex))
4717 MessageBox(g_Git.GetLibGit2LastErr(L"Could not write index."), L"TortoiseGit", MB_OK);
4719 if (needWriteIndex)
4721 RefreshParent();
4722 return;
4725 SetRedraw(FALSE);
4726 POSITION pos2 = nullptr;
4727 while ((pos2 = GetFirstSelectedItemPosition()) != nullptr)
4729 int index2 = GetNextSelectedItem(pos2);
4730 if (GetCheck(index2))
4731 m_nSelected--;
4732 m_nTotal--;
4734 RemoveListEntry(index2);
4736 SetRedraw(TRUE);
4740 BOOL CGitStatusListCtrl::OnWndMsg(UINT message, WPARAM wParam, LPARAM lParam, LRESULT* pResult)
4742 switch (message)
4744 case WM_MENUCHAR: // only supported by IContextMenu3
4745 if (g_IContext3)
4747 g_IContext3->HandleMenuMsg2(message, wParam, lParam, pResult);
4748 return TRUE;
4750 break;
4752 case WM_DRAWITEM:
4753 case WM_MEASUREITEM:
4754 if (wParam)
4755 break; // if wParam != 0 then the message is not menu-related
4757 case WM_INITMENU:
4758 case WM_INITMENUPOPUP:
4760 HMENU hMenu = reinterpret_cast<HMENU>(wParam);
4761 if ((hMenu == m_hShellMenu) && (GetMenuItemCount(hMenu) == 0))
4763 // the shell submenu is populated only on request, i.e. right
4764 // before the submenu is shown
4765 if (g_pFolderhook)
4767 delete g_pFolderhook;
4768 g_pFolderhook = nullptr;
4770 CTGitPathList targetList;
4771 FillListOfSelectedItemPaths(targetList);
4772 if (!targetList.IsEmpty())
4774 // get IShellFolder interface of Desktop (root of shell namespace)
4775 if (g_psfDesktopFolder)
4776 g_psfDesktopFolder->Release();
4777 SHGetDesktopFolder(&g_psfDesktopFolder); // needed to obtain full qualified pidl
4779 // ParseDisplayName creates a PIDL from a file system path relative to the IShellFolder interface
4780 // but since we use the Desktop as our interface and the Desktop is the namespace root
4781 // that means that it's a fully qualified PIDL, which is what we need
4783 if (g_pidlArray)
4785 for (int i = 0; i < g_pidlArrayItems; i++)
4787 if (g_pidlArray[i])
4788 CoTaskMemFree(g_pidlArray[i]);
4790 CoTaskMemFree(g_pidlArray);
4791 g_pidlArray = nullptr;
4792 g_pidlArrayItems = 0;
4794 size_t bufferSize;
4795 if (SizeTMult(static_cast<size_t>(targetList.GetCount()) + 10, sizeof(LPITEMIDLIST), &bufferSize) != S_OK)
4796 return TRUE;
4797 g_pidlArray = static_cast<LPITEMIDLIST*>(CoTaskMemAlloc(bufferSize));
4798 if (!g_pidlArray)
4799 return TRUE;
4800 SecureZeroMemory(g_pidlArray, bufferSize);
4801 int succeededItems = 0;
4802 PIDLIST_RELATIVE pidl = nullptr;
4804 size_t bufsize = 1024;
4805 auto filepath = std::make_unique<WCHAR[]>(bufsize);
4806 for (int i = 0; i < targetList.GetCount(); ++i)
4808 CString fullPath = g_Git.CombinePath(targetList[i].GetWinPath());
4809 if (bufsize < static_cast<size_t>(fullPath.GetLength()))
4811 bufsize = static_cast<size_t>(fullPath.GetLength()) + 3;
4812 filepath = std::make_unique<WCHAR[]>(bufsize);
4814 wcscpy_s(filepath.get(), bufsize, fullPath);
4815 if (SUCCEEDED(g_psfDesktopFolder->ParseDisplayName(nullptr, 0, filepath.get(), nullptr, &pidl, nullptr)))
4816 g_pidlArray[succeededItems++] = pidl; // copy pidl to pidlArray
4818 if (succeededItems == 0)
4820 CoTaskMemFree(g_pidlArray);
4821 g_pidlArray = nullptr;
4824 g_pidlArrayItems = succeededItems;
4826 if (g_pidlArrayItems)
4828 CString ext = targetList[0].GetFileExtension();
4830 ASSOCIATIONELEMENT const rgAssocItem[] =
4832 { ASSOCCLASS_PROGID_STR, nullptr, ext },
4833 { ASSOCCLASS_SYSTEM_STR, nullptr, ext },
4834 { ASSOCCLASS_APP_STR, nullptr, ext },
4835 { ASSOCCLASS_STAR, nullptr, nullptr },
4836 { ASSOCCLASS_FOLDER, nullptr, nullptr },
4838 IQueryAssociations* pIQueryAssociations = nullptr;
4839 if (FAILED(AssocCreateForClasses(rgAssocItem, ARRAYSIZE(rgAssocItem), IID_IQueryAssociations, reinterpret_cast<void**>(&pIQueryAssociations))))
4840 pIQueryAssociations = nullptr; // not a problem, it works without this
4842 g_pFolderhook = new CIShellFolderHook(g_psfDesktopFolder, targetList);
4843 LPCONTEXTMENU icm1 = nullptr;
4845 DEFCONTEXTMENU dcm = { 0 };
4846 dcm.hwnd = m_hWnd;
4847 dcm.psf = g_pFolderhook;
4848 dcm.cidl = g_pidlArrayItems;
4849 dcm.apidl = const_cast<PCUITEMID_CHILD_ARRAY>(g_pidlArray);
4850 dcm.punkAssociationInfo = pIQueryAssociations;
4851 if (SUCCEEDED(SHCreateDefaultContextMenu(&dcm, IID_IContextMenu, reinterpret_cast<void**>(&icm1))))
4853 int iMenuType = 0; // to know which version of IContextMenu is supported
4854 if (icm1)
4855 { // since we got an IContextMenu interface we can now obtain the higher version interfaces via that
4856 if (icm1->QueryInterface(IID_IContextMenu3, reinterpret_cast<void**>(&m_pContextMenu)) == S_OK)
4857 iMenuType = 3;
4858 else if (icm1->QueryInterface(IID_IContextMenu2, reinterpret_cast<void**>(&m_pContextMenu)) == S_OK)
4859 iMenuType = 2;
4861 if (m_pContextMenu)
4862 icm1->Release(); // we can now release version 1 interface, cause we got a higher one
4863 else
4865 // since no higher versions were found
4866 // redirect ppContextMenu to version 1 interface
4867 iMenuType = 1;
4868 m_pContextMenu = icm1;
4871 if (m_pContextMenu)
4873 // lets fill the our popup menu
4874 UINT flags = CMF_NORMAL;
4875 flags |= (GetKeyState(VK_SHIFT) & 0x8000) != 0 ? CMF_EXTENDEDVERBS : 0;
4876 m_pContextMenu->QueryContextMenu(hMenu, 0, SHELL_MIN_CMD, SHELL_MAX_CMD, flags);
4879 // subclass window to handle menu related messages in CShellContextMenu
4880 if (iMenuType > 1) // only subclass if its version 2 or 3
4882 if (iMenuType == 2)
4883 g_IContext2 = static_cast<LPCONTEXTMENU2>(m_pContextMenu);
4884 else // version 3
4885 g_IContext3 = static_cast<LPCONTEXTMENU3>(m_pContextMenu);
4889 if (pIQueryAssociations)
4890 pIQueryAssociations->Release();
4893 if (g_IContext3)
4894 g_IContext3->HandleMenuMsg2(message, wParam, lParam, pResult);
4895 else if (g_IContext2)
4896 g_IContext2->HandleMenuMsg(message, wParam, lParam);
4897 return TRUE;
4901 break;
4902 default:
4903 break;
4906 return __super::OnWndMsg(message, wParam, lParam, pResult);
4909 CTGitPath* CGitStatusListCtrl::GetListEntry(int index)
4911 ATLASSERT(m_guard.GetCurrentThreadStatus());
4912 if (static_cast<size_t>(index) >= m_arListArray.size())
4914 ATLASSERT(FALSE);
4915 return nullptr;
4917 if (m_arListArray[index] >= m_arStatusArray.size())
4919 ATLASSERT(FALSE);
4920 return nullptr;
4922 return const_cast<CTGitPath*>(m_arStatusArray[m_arListArray[index]]);
4925 ULONG CGitStatusListCtrl::GetGestureStatus(CPoint /*ptTouch*/)
4927 return 0;
4930 #define CHANGELIST_FILE_NAME L"tgitchangelist"
4932 void CGitStatusListCtrl::LoadChangelists()
4934 CString tgitChangelistPath;
4935 if (!GitAdminDir::GetWorktreeAdminDirPath(g_Git.m_CurrentDir, tgitChangelistPath))
4937 CMessageBox::Show(GetSafeHwnd(), IDS_ERR_CHANGELIST_LOAD, IDS_APPNAME, MB_ICONERROR);
4938 return;
4941 tgitChangelistPath += CHANGELIST_FILE_NAME;
4943 if (!PathFileExists(tgitChangelistPath))
4944 return;
4948 CStdioFile file(tgitChangelistPath, CFile::typeText | CFile::modeRead | CFile::shareDenyWrite);
4949 CString changelistName = GITSLC_IGNORECHANGELIST;
4950 CString strLine;
4951 while (file.ReadString(strLine))
4953 strLine = strLine.Trim();
4954 if (strLine.IsEmpty())
4955 continue;
4957 if (CStringUtils::StartsWith(strLine, L"<") && CStringUtils::EndsWith(strLine, L">"))
4958 { //this is changelist name
4959 changelistName = strLine.Mid(1, strLine.GetLength() - 2);
4960 m_changelists.insert_or_assign(changelistName, 0);
4962 else // this is git path
4963 m_pathToChangelist.insert_or_assign(strLine, changelistName);
4965 file.Close();
4967 catch (CFileException* pE)
4969 pE->Delete();
4970 CMessageBox::Show(GetSafeHwnd(), IDS_ERR_CHANGELIST_LOAD, IDS_APPNAME, MB_ICONERROR);
4974 void CGitStatusListCtrl::SaveChangelists()
4976 CString tgitChangelistPath;
4977 if (!GitAdminDir::GetWorktreeAdminDirPath(g_Git.m_CurrentDir, tgitChangelistPath))
4979 CMessageBox::Show(GetSafeHwnd(), IDS_ERR_CHANGELIST_SAVE, IDS_APPNAME, MB_ICONERROR);
4980 return;
4983 tgitChangelistPath += CHANGELIST_FILE_NAME;
4985 std::map<CString, std::set<CString>> changelistToPath;
4986 for (auto it = m_pathToChangelist.cbegin(); it != m_pathToChangelist.cend(); ++it)
4988 auto itChangelist = changelistToPath.find(it->second);
4989 if (itChangelist == changelistToPath.end())
4991 std::set<CString> paths;
4992 paths.insert(it->first);
4993 changelistToPath.insert(std::make_pair(it->second, paths));
4995 else
4996 itChangelist->second.insert(it->first);
5001 CStdioFile file(tgitChangelistPath, CFile::typeText | CFile::modeCreate | CFile::modeWrite | CFile::shareDenyWrite);
5002 for (auto itChangelist = changelistToPath.cbegin(); itChangelist != changelistToPath.cend(); ++itChangelist)
5004 if (itChangelist != changelistToPath.cbegin())
5005 file.WriteString(L"\n");
5007 file.WriteString(L"<" + itChangelist->first + L">\n");
5008 for (auto itPath = itChangelist->second.cbegin(); itPath != itChangelist->second.cend(); ++itPath)
5009 file.WriteString(*itPath + L"\n");
5012 file.Close();
5014 catch (CFileException* pE)
5016 pE->Delete();
5017 CMessageBox::Show(GetSafeHwnd(), IDS_ERR_CHANGELIST_SAVE, IDS_APPNAME, MB_ICONERROR);
5021 void CGitStatusListCtrl::PruneChangelists(const CTGitPathList* root)
5023 CAutoReadLock locker(m_guard);
5025 if (m_pathToChangelist.empty())
5026 return;
5028 CTGitPathList prefixList;
5029 if (root)
5031 if (!root->AreAllPathsDirectories())
5032 return;
5034 prefixList = *root;
5035 prefixList.SortByPathname();
5038 std::set<CString> unchecked;
5039 const int nListboxEntries = GetItemCount();
5040 for (int nItem = 0; nItem < nListboxEntries; ++nItem)
5042 auto pentry = GetListEntry(nItem);
5043 if (!pentry || (pentry->m_Checked && m_restorepaths.find(pentry->GetWinPathString()) == m_restorepaths.end()))
5044 continue;
5046 unchecked.emplace(pentry->GetGitPathString());
5049 auto it1 = unchecked.cbegin();
5050 auto it2 = m_pathToChangelist.begin();
5052 while (it1 != unchecked.cend() && it2 != m_pathToChangelist.end())
5054 if (*it1 > it2->first)
5056 if (prefixList.IsEmpty() || prefixList.IsAnyAncestorOf(it2->first))
5057 it2 = m_pathToChangelist.erase(it2);
5058 else
5059 ++it2;
5061 else if (it2->first > *it1)
5062 ++it1;
5063 else
5065 ++it1;
5066 ++it2;
5069 while (it2 != m_pathToChangelist.end())
5071 if (prefixList.IsEmpty() || prefixList.IsAnyAncestorOf(it2->first))
5072 it2 = m_pathToChangelist.erase(it2);
5073 else
5074 ++it2;
5078 void CGitStatusListCtrl::OnColumnVisibilityChanged(int column, bool visible)
5080 if (visible && column == m_ColumnManager.GetColumnByName(IDS_STATUSLIST_COLLFSLOCK))
5081 RefreshParent();
5084 void CGitStatusListCtrl::RefreshParent()
5086 SetRedraw(FALSE);
5087 auto pParent = GetLogicalParent();
5088 if (pParent && pParent->GetSafeHwnd())
5089 pParent->SendMessage(GITSLNM_NEEDSREFRESH);
5090 SetRedraw(TRUE);