Fixed typos
[TortoiseGit.git] / src / Git / GitStatusListCtrl.cpp
blob781f9661a2ac7abde5afcfe9fc810a523985d4fa
1 // TortoiseGit - a Windows shell extension for easy version control
3 // Copyright (C) 2008-2018 - TortoiseGit
4 // Copyright (C) 2003-2008, 2013-2015 - TortoiseSVN
6 // This program is free software; you can redistribute it and/or
7 // modify it under the terms of the GNU General Public License
8 // as published by the Free Software Foundation; either version 2
9 // of the License, or (at your option) any later version.
11 // This program is distributed in the hope that it will be useful,
12 // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 // GNU General Public License for more details.
16 // You should have received a copy of the GNU General Public License
17 // along with this program; if not, write to the Free Software Foundation,
18 // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
21 #include "stdafx.h"
22 #include "resource.h"
23 #include "../TortoiseShell/resource.h"
24 #include "GitStatusListCtrl.h"
25 #include "MessageBox.h"
26 #include "MyMemDC.h"
27 #include "UnicodeUtils.h"
28 #include "AppUtils.h"
29 #include "PathUtils.h"
30 #include "TempFile.h"
31 #include "StringUtils.h"
32 #include "LoglistUtils.h"
33 #include "Git.h"
34 #include "GitRev.h"
35 #include "GitDiff.h"
36 #include "GitProgressDlg.h"
37 #include "SysImageList.h"
38 #include "TGitPath.h"
39 #include "registry.h"
40 #include "InputDlg.h"
41 #include "GitAdminDir.h"
42 #include "GitDataObject.h"
43 #include "ProgressCommands/AddProgressCommand.h"
44 #include "IconMenu.h"
45 #include "FormatMessageWrapper.h"
46 #include "BrowseFolder.h"
47 #include "SysInfo.h"
49 const UINT CGitStatusListCtrl::GITSLNM_ITEMCOUNTCHANGED
50 = ::RegisterWindowMessage(L"GITSLNM_ITEMCOUNTCHANGED");
51 const UINT CGitStatusListCtrl::GITSLNM_NEEDSREFRESH
52 = ::RegisterWindowMessage(L"GITSLNM_NEEDSREFRESH");
53 const UINT CGitStatusListCtrl::GITSLNM_ADDFILE
54 = ::RegisterWindowMessage(L"GITSLNM_ADDFILE");
55 const UINT CGitStatusListCtrl::GITSLNM_CHECKCHANGED
56 = ::RegisterWindowMessage(L"GITSLNM_CHECKCHANGED");
57 const UINT CGitStatusListCtrl::GITSLNM_ITEMCHANGED
58 = ::RegisterWindowMessage(L"GITSLNM_ITEMCHANGED");
60 struct icompare
62 bool operator() (const std::wstring& lhs, const std::wstring& rhs) const
64 // no logical comparison here: we need this sorted strictly
65 return _wcsicmp(lhs.c_str(), rhs.c_str()) < 0;
69 class CIShellFolderHook : public IShellFolder
71 public:
72 CIShellFolderHook(LPSHELLFOLDER sf, const CTGitPathList& pathlist)
74 sf->AddRef();
75 m_iSF = sf;
76 // it seems the paths in the HDROP need to be sorted, otherwise
77 // it might not work properly or even crash.
78 // to get the items sorted, we just add them to a set
79 for (int i = 0; i < pathlist.GetCount(); ++i)
80 sortedpaths.insert((LPCTSTR)g_Git.CombinePath(pathlist[i].GetWinPath()));
83 ~CIShellFolderHook() { m_iSF->Release(); }
85 // IUnknown methods --------
86 virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, __RPC__deref_out void** ppvObject) override { return m_iSF->QueryInterface(riid, ppvObject); }
87 virtual ULONG STDMETHODCALLTYPE AddRef(void) override { return m_iSF->AddRef(); }
88 virtual ULONG STDMETHODCALLTYPE Release(void) override { return m_iSF->Release(); }
90 // IShellFolder methods ----
91 virtual HRESULT STDMETHODCALLTYPE GetUIObjectOf(HWND hwndOwner, UINT cidl, PCUITEMID_CHILD_ARRAY apidl, REFIID riid, UINT* rgfReserved, void** ppv) override;
93 virtual HRESULT STDMETHODCALLTYPE CompareIDs(LPARAM lParam, __RPC__in PCUIDLIST_RELATIVE pidl1, __RPC__in PCUIDLIST_RELATIVE pidl2) override { return m_iSF->CompareIDs(lParam, pidl1, pidl2); }
94 virtual HRESULT STDMETHODCALLTYPE GetDisplayNameOf(__RPC__in_opt PCUITEMID_CHILD pidl, SHGDNF uFlags, __RPC__out STRRET* pName) override { return m_iSF->GetDisplayNameOf(pidl, uFlags, pName); }
95 virtual HRESULT STDMETHODCALLTYPE CreateViewObject(__RPC__in_opt HWND hwndOwner, __RPC__in REFIID riid, __RPC__deref_out_opt void** ppv) override { return m_iSF->CreateViewObject(hwndOwner, riid, ppv); }
96 virtual HRESULT STDMETHODCALLTYPE EnumObjects(__RPC__in_opt HWND hwndOwner, SHCONTF grfFlags, __RPC__deref_out_opt IEnumIDList** ppenumIDList) override { return m_iSF->EnumObjects(hwndOwner, grfFlags, ppenumIDList); }
97 virtual HRESULT STDMETHODCALLTYPE BindToObject(__RPC__in PCUIDLIST_RELATIVE pidl, __RPC__in_opt IBindCtx* pbc, __RPC__in REFIID riid, __RPC__deref_out_opt void** ppv) override { return m_iSF->BindToObject(pidl, pbc, riid, ppv); }
98 virtual HRESULT STDMETHODCALLTYPE ParseDisplayName(__RPC__in_opt HWND hwnd, __RPC__in_opt IBindCtx* pbc, __RPC__in_string LPWSTR pszDisplayName, __reserved ULONG* pchEaten, __RPC__deref_out_opt PIDLIST_RELATIVE* ppidl, __RPC__inout_opt ULONG* pdwAttributes) override { return m_iSF->ParseDisplayName(hwnd, pbc, pszDisplayName, pchEaten, ppidl, pdwAttributes); }
99 virtual HRESULT STDMETHODCALLTYPE GetAttributesOf(UINT cidl, __RPC__in_ecount_full_opt(cidl) PCUITEMID_CHILD_ARRAY apidl, __RPC__inout SFGAOF* rgfInOut) override { return m_iSF->GetAttributesOf(cidl, apidl, rgfInOut); }
100 virtual HRESULT STDMETHODCALLTYPE BindToStorage(__RPC__in PCUIDLIST_RELATIVE pidl, __RPC__in_opt IBindCtx* pbc, __RPC__in REFIID riid, __RPC__deref_out_opt void** ppv) override { return m_iSF->BindToStorage(pidl, pbc, riid, ppv); }
101 virtual HRESULT STDMETHODCALLTYPE SetNameOf(__in_opt HWND hwnd, __in PCUITEMID_CHILD pidl, __in LPCWSTR pszName, __in SHGDNF uFlags, __deref_opt_out PITEMID_CHILD* ppidlOut) override { return m_iSF->SetNameOf(hwnd, pidl, pszName, uFlags, ppidlOut); }
103 protected:
104 LPSHELLFOLDER m_iSF;
105 std::set<std::wstring, icompare> sortedpaths;
108 HRESULT STDMETHODCALLTYPE CIShellFolderHook::GetUIObjectOf(HWND hwndOwner, UINT cidl, PCUITEMID_CHILD_ARRAY apidl, REFIID riid, UINT* rgfReserved, void** ppv)
110 if (InlineIsEqualGUID(riid, IID_IDataObject))
112 HRESULT hres = m_iSF->GetUIObjectOf(hwndOwner, cidl, apidl, IID_IDataObject, nullptr, ppv);
113 if (FAILED(hres))
114 return hres;
116 IDataObject * idata = (LPDATAOBJECT)(*ppv);
117 // the IDataObject returned here doesn't have a HDROP, so we create one ourselves and add it to the IDataObject
118 // the HDROP is necessary for most context menu handlers
119 int nLength = 0;
120 for (auto it = sortedpaths.cbegin(); it != sortedpaths.cend(); ++it)
122 nLength += (int)it->size();
123 nLength += 1; // '\0' separator
125 int nBufferSize = sizeof(DROPFILES) + ((nLength + 5)*sizeof(TCHAR));
126 auto pBuffer = std::make_unique<char[]>(nBufferSize);
127 SecureZeroMemory(pBuffer.get(), nBufferSize);
128 DROPFILES* df = (DROPFILES*)pBuffer.get();
129 df->pFiles = sizeof(DROPFILES);
130 df->fWide = 1;
131 TCHAR* pFilenames = (TCHAR*)((BYTE*)(pBuffer.get()) + sizeof(DROPFILES));
132 TCHAR* pCurrentFilename = pFilenames;
134 for (auto it = sortedpaths.cbegin(); it != sortedpaths.cend(); ++it)
136 wcscpy_s(pCurrentFilename, it->size() + 1, it->c_str());
137 pCurrentFilename += it->size();
138 *pCurrentFilename = '\0'; // separator between file names
139 pCurrentFilename++;
141 *pCurrentFilename = '\0'; // terminate array
142 pCurrentFilename++;
143 *pCurrentFilename = '\0'; // terminate array
144 STGMEDIUM medium = { 0 };
145 medium.tymed = TYMED_HGLOBAL;
146 medium.hGlobal = GlobalAlloc(GMEM_ZEROINIT | GMEM_MOVEABLE, nBufferSize + 20);
147 if (medium.hGlobal)
149 LPVOID pMem = ::GlobalLock(medium.hGlobal);
150 if (pMem)
152 memcpy(pMem, pBuffer.get(), nBufferSize);
153 GlobalUnlock(medium.hGlobal);
154 FORMATETC formatetc = { 0 };
155 formatetc.cfFormat = CF_HDROP;
156 formatetc.dwAspect = DVASPECT_CONTENT;
157 formatetc.lindex = -1;
158 formatetc.tymed = TYMED_HGLOBAL;
159 medium.pUnkForRelease = nullptr;
160 hres = idata->SetData(&formatetc, &medium, TRUE);
161 return hres;
164 return E_OUTOFMEMORY;
166 else
168 // just pass it on to the base object
169 return m_iSF->GetUIObjectOf(hwndOwner, cidl, apidl, riid, rgfReserved, ppv);
173 IContextMenu2 * g_IContext2 = nullptr;
174 IContextMenu3 * g_IContext3 = nullptr;
175 CIShellFolderHook * g_pFolderhook = nullptr;
176 IShellFolder * g_psfDesktopFolder = nullptr;
177 LPITEMIDLIST * g_pidlArray = nullptr;
178 int g_pidlArrayItems = 0;
180 #define SHELL_MIN_CMD 10000
181 #define SHELL_MAX_CMD 20000
183 HRESULT CALLBACK dfmCallback(IShellFolder * /*psf*/, HWND /*hwnd*/, IDataObject * /*pdtobj*/, UINT uMsg, WPARAM /*wParam*/, LPARAM /*lParam*/)
185 switch (uMsg)
187 case DFM_MERGECONTEXTMENU:
188 return S_OK;
189 case DFM_INVOKECOMMAND:
190 case DFM_INVOKECOMMANDEX:
191 case DFM_GETDEFSTATICID: // Required for Windows 7 to pick a default
192 return S_FALSE;
194 return E_NOTIMPL;
197 BEGIN_MESSAGE_MAP(CGitStatusListCtrl, CResizableColumnsListCtrl<CListCtrl>)
198 ON_NOTIFY(HDN_ITEMCLICKA, 0, OnHdnItemclick)
199 ON_NOTIFY(HDN_ITEMCLICKW, 0, OnHdnItemclick)
200 ON_NOTIFY_REFLECT_EX(LVN_ITEMCHANGED, OnLvnItemchanged)
201 ON_WM_CONTEXTMENU()
202 ON_NOTIFY_REFLECT(NM_DBLCLK, OnNMDblclk)
203 ON_NOTIFY_REFLECT(LVN_GETINFOTIP, OnLvnGetInfoTip)
204 ON_NOTIFY_REFLECT(NM_CUSTOMDRAW, OnNMCustomdraw)
205 ON_NOTIFY_REFLECT(LVN_GETDISPINFO, OnLvnGetdispinfo)
206 ON_WM_SETCURSOR()
207 ON_WM_GETDLGCODE()
208 ON_NOTIFY_REFLECT(NM_RETURN, OnNMReturn)
209 ON_WM_KEYDOWN()
210 ON_WM_PAINT()
211 ON_WM_SYSCOLORCHANGE()
212 ON_NOTIFY_REFLECT(LVN_BEGINDRAG, OnBeginDrag)
213 END_MESSAGE_MAP()
215 CGitStatusListCtrl::CGitStatusListCtrl() : CResizableColumnsListCtrl<CListCtrl>()
216 , m_pbCanceled(nullptr)
217 , m_pStatLabel(nullptr)
218 , m_pSelectButton(nullptr)
219 , m_pConfirmButton(nullptr)
220 , m_bBusy(false)
221 , m_bWaitCursor(false)
222 , m_bEmpty(false)
223 , m_bShowIgnores(false)
224 , m_bIgnoreRemoveOnly(false)
225 , m_bCheckChildrenWithParent(false)
226 , m_bUnversionedLast(true)
227 , m_bHasChangeLists(false)
228 , m_bHasCheckboxes(false)
229 , m_bCheckIfGroupsExist(true)
230 , m_bFileDropsEnabled(false)
231 , m_bOwnDrag(false)
232 , m_dwDefaultColumns(0)
233 , m_bAscending(false)
234 , m_nSortedColumn(-1)
235 , m_bHasExternalsFromDifferentRepos(false)
236 , m_amend(false)
237 , m_bDoNotAutoselectSubmodules(false)
238 , m_bNoAutoselectMissing(false)
239 , m_bHasWC(true)
240 , m_hwndLogicalParent(nullptr)
241 , m_bHasUnversionedItems(FALSE)
242 , m_nTargetCount(0)
243 , m_bHasExternals(false)
244 , m_bHasIgnoreGroup(false)
245 , m_nUnversioned(0)
246 , m_nNormal(0)
247 , m_nModified(0)
248 , m_nAdded(0)
249 , m_nDeleted(0)
250 , m_nConflicted(0)
251 , m_nTotal(0)
252 , m_nSelected(0)
253 , m_nRenamed(0)
254 , m_nShownUnversioned(0)
255 , m_nShownModified(0)
256 , m_nShownAdded(0)
257 , m_nShownDeleted(0)
258 , m_nShownConflicted(0)
259 , m_nShownFiles(0)
260 , m_nShownSubmodules(0)
261 , m_dwShow(0)
262 , m_bShowFolders(false)
263 , m_bUpdate(false)
264 , m_dwContextMenus(0)
265 , m_nIconFolder(0)
266 , m_nRestoreOvl(0)
267 , m_pContextMenu(nullptr)
268 , m_hShellMenu(nullptr)
269 , m_nBackgroundImageID(0)
270 , m_FileLoaded(0)
271 , m_bIsRevertTheirMy(false)
272 , m_nLineAdded(0)
273 , m_nLineDeleted(0)
274 , m_nBlockItemChangeHandler(0)
275 , m_uiFont(nullptr)
277 m_critSec.Init();
278 m_bNoAutoselectMissing = CRegDWORD(L"Software\\TortoiseGit\\AutoselectMissingFiles", FALSE) == TRUE;
280 NONCLIENTMETRICS metrics = { 0 };
281 metrics.cbSize = sizeof(NONCLIENTMETRICS);
282 SystemParametersInfo(SPI_GETNONCLIENTMETRICS, 0, &metrics, FALSE);
283 m_uiFont = CreateFontIndirect(&metrics.lfMessageFont);
286 CGitStatusListCtrl::~CGitStatusListCtrl()
288 if (m_uiFont)
289 DeleteObject(m_uiFont);
290 ClearStatusArray();
293 HWND CGitStatusListCtrl::GetParentHWND()
295 if (m_hwndLogicalParent)
296 return m_hwndLogicalParent->GetSafeHwnd();
297 auto owner = GetSafeOwner();
298 if (!owner)
299 return GetSafeHwnd();
300 return owner->GetSafeHwnd();
303 void CGitStatusListCtrl::ClearStatusArray()
305 #if 0
306 CAutoWriteLock locker(m_guard);
307 for (size_t i = 0; i < m_arStatusArray.size(); ++i)
309 delete m_arStatusArray[i];
311 m_arStatusArray.clear();
312 #endif
315 void CGitStatusListCtrl::Init(DWORD dwColumns, const CString& sColumnInfoContainer, unsigned __int64 dwContextMenus /* = GitSLC_POPALL */, bool bHasCheckboxes /* = true */, bool bHasWC /* = true */, DWORD allowedColumns /* = 0xffffffff */)
317 CAutoWriteLock locker(m_guard);
319 m_dwDefaultColumns = dwColumns | 1;
320 m_dwContextMenus = dwContextMenus;
321 m_bHasCheckboxes = bHasCheckboxes;
322 m_bHasWC = bHasWC;
323 m_bWaitCursor = true;
325 // set the extended style of the listcontrol
326 DWORD exStyle = LVS_EX_DOUBLEBUFFER | LVS_EX_INFOTIP | LVS_EX_SUBITEMIMAGES;
327 exStyle |= (bHasCheckboxes ? LVS_EX_CHECKBOXES : 0);
328 SetRedraw(false);
329 SetExtendedStyle(exStyle);
330 CResizableColumnsListCtrl::Init();
332 SetWindowTheme(m_hWnd, L"Explorer", nullptr);
334 m_nIconFolder = SYS_IMAGE_LIST().GetDirIconIndex();
335 m_nRestoreOvl = SYS_IMAGE_LIST().AddIcon((HICON)LoadImage(AfxGetResourceHandle(), MAKEINTRESOURCE(IDI_RESTOREOVL), IMAGE_ICON, 0, 0, LR_DEFAULTSIZE));
336 SYS_IMAGE_LIST().SetOverlayImage(m_nRestoreOvl, OVL_RESTORE);
337 SetImageList(&SYS_IMAGE_LIST(), LVSIL_SMALL);
339 // keep CSorter::operator() in sync!!
340 static UINT standardColumnNames[GITSLC_NUMCOLUMNS]
341 = { IDS_STATUSLIST_COLFILE
342 , IDS_STATUSLIST_COLFILENAME
343 , IDS_STATUSLIST_COLEXT
344 , IDS_STATUSLIST_COLSTATUS
345 , IDS_STATUSLIST_COLADD
346 , IDS_STATUSLIST_COLDEL
347 , IDS_STATUSLIST_COLLASTMODIFIED
348 , IDS_STATUSLIST_COLSIZE
351 m_ColumnManager.SetNames(standardColumnNames,GITSLC_NUMCOLUMNS);
352 m_ColumnManager.ReadSettings(m_dwDefaultColumns, 0xffffffff & ~(allowedColumns | m_dwDefaultColumns), sColumnInfoContainer, GITSLC_NUMCOLUMNS);
353 m_ColumnManager.SetRightAlign(4);
354 m_ColumnManager.SetRightAlign(5);
355 m_ColumnManager.SetRightAlign(7);
357 // enable file drops
358 if (!m_pDropTarget)
360 m_pDropTarget = std::make_unique<CGitStatusListCtrlDropTarget>(this);
361 RegisterDragDrop(m_hWnd, m_pDropTarget.get());
362 // create the supported formats:
363 FORMATETC ftetc = { 0 };
364 ftetc.dwAspect = DVASPECT_CONTENT;
365 ftetc.lindex = -1;
366 ftetc.tymed = TYMED_HGLOBAL;
367 ftetc.cfFormat = CF_HDROP;
368 m_pDropTarget->AddSuportedFormat(ftetc);
371 SetRedraw(true);
372 m_bWaitCursor = false;
375 bool CGitStatusListCtrl::SetBackgroundImage(UINT nID)
377 m_nBackgroundImageID = nID;
378 return CAppUtils::SetListCtrlBackgroundImage(GetSafeHwnd(), nID);
381 BOOL CGitStatusListCtrl::GetStatus ( const CTGitPathList* pathList
382 , bool bUpdate /* = FALSE */
383 , bool bShowIgnores /* = false */
384 , bool bShowUnRev /* = false */
385 , bool bShowLocalChangesIgnored /* = false */)
387 CAutoWriteLock locker(m_guard);
389 m_bEmpty = false;
390 m_bBusy = true;
391 m_bWaitCursor = true;
392 Invalidate();
394 m_bIsRevertTheirMy = g_Git.IsRebaseRunning() > 0;
396 int mask= CGitStatusListCtrl::FILELIST_MODIFY;
397 if(bShowIgnores)
398 mask|= CGitStatusListCtrl::FILELIST_IGNORE;
399 if(bShowUnRev)
400 mask|= CGitStatusListCtrl::FILELIST_UNVER;
401 if (bShowLocalChangesIgnored)
402 mask |= CGitStatusListCtrl::FILELIST_LOCALCHANGESIGNORED;
403 this->UpdateFileList(mask, bUpdate, pathList);
405 if (pathList && m_setDirectFiles.empty())
407 // remember files which are selected by users so that those can be preselected
408 for (int i = 0; i < pathList->GetCount(); ++i)
409 if (!(*pathList)[i].IsDirectory())
410 m_setDirectFiles.insert((*pathList)[i].GetGitPathString());
413 #if 0
414 int refetchcounter = 0;
415 BOOL bRet = TRUE;
416 Invalidate();
417 // force the cursor to change
418 POINT pt;
419 GetCursorPos(&pt);
420 SetCursorPos(pt.x, pt.y);
422 m_mapFilenameToChecked.clear();
423 //m_StatusUrlList.Clear();
424 bool bHasChangelists = (m_changelists.size() > 1 || (!m_changelists.empty() && !m_bHasIgnoreGroup));
425 m_changelists.clear();
426 for (size_t i=0; i < m_arStatusArray.size(); i++)
428 FileEntry * entry = m_arStatusArray[i];
429 if ( bHasChangelists && entry->checked)
431 // If change lists are present, remember all checked entries
432 CString path = entry->GetPath().GetGitPathString();
433 m_mapFilenameToChecked[path] = true;
435 if ( (entry->status==git_wc_status_unversioned || entry->status==git_wc_status_missing ) && entry->checked )
437 // The user manually selected an unversioned or missing file. We remember
438 // this so that the selection can be restored when refreshing.
439 CString path = entry->GetPath().GetGitPathString();
440 m_mapFilenameToChecked[path] = true;
442 else if ( entry->status > git_wc_status_normal && !entry->checked )
444 // The user manually deselected a versioned file. We remember
445 // this so that the deselection can be restored when refreshing.
446 CString path = entry->GetPath().GetGitPathString();
447 m_mapFilenameToChecked[path] = false;
451 // use a sorted path list to make sure we fetch the status of
452 // parent items first, *then* the child items (if any)
453 CTGitPathList sortedPathList = pathList;
454 sortedPathList.SortByPathname();
457 bRet = TRUE;
458 m_nTargetCount = 0;
459 m_bHasExternalsFromDifferentRepos = FALSE;
460 m_bHasExternals = FALSE;
461 m_bHasUnversionedItems = FALSE;
462 m_bHasChangeLists = false;
463 m_bShowIgnores = bShowIgnores;
464 m_nSortedColumn = 0;
465 m_bBlock = TRUE;
466 m_bBusy = true;
467 m_bEmpty = false;
468 Invalidate();
470 // first clear possible status data left from
471 // previous GetStatus() calls
472 ClearStatusArray();
474 m_StatusFileList = sortedPathList;
476 // Since Git_client_status() returns all files, even those in
477 // folders included with Git:externals we need to check if all
478 // files we get here belongs to the same repository.
479 // It is possible to commit changes in an external folder, as long
480 // as the folder belongs to the same repository (but another path),
481 // but it is not possible to commit all files if the externals are
482 // from a different repository.
484 // To check if all files belong to the same repository, we compare the
485 // UUID's - if they're identical then the files belong to the same
486 // repository and can be committed. But if they're different, then
487 // tell the user to commit all changes in the external folders
488 // first and exit.
489 CStringA sUUID; // holds the repo UUID
490 CTGitPathList arExtPaths; // list of Git:external paths
492 GitConfig config;
494 m_sURL.Empty();
496 m_nTargetCount = sortedPathList.GetCount();
498 GitStatus status(m_pbCanceled);
499 if(m_nTargetCount > 1 && sortedPathList.AreAllPathsFilesInOneDirectory())
501 // This is a special case, where we've been given a list of files
502 // all from one folder
503 // We handle them by setting a status filter, then requesting the Git status of
504 // all the files in the directory, filtering for the ones we're interested in
505 status.SetFilter(sortedPathList);
507 // if all selected entries are files, we don't do a recursive status
508 // fetching. But if only one is a directory, we have to recurse.
509 git_depth_t depth = git_depth_files;
510 // We have set a filter. If all selected items were files or we fetch
511 // the status not recursively, then we have to include
512 // ignored items too - the user has selected them
513 bool bShowIgnoresRight = true;
514 for (int fcindex=0; fcindex<sortedPathList.GetCount(); ++fcindex)
516 if (sortedPathList[fcindex].IsDirectory())
518 depth = git_depth_infinity;
519 bShowIgnoresRight = false;
520 break;
523 if(!FetchStatusForSingleTarget(config, status, sortedPathList.GetCommonDirectory(), bUpdate, sUUID, arExtPaths, true, depth, bShowIgnoresRight))
524 bRet = FALSE;
526 else
528 for(int nTarget = 0; nTarget < m_nTargetCount; nTarget++)
530 // check whether the path we want the status for is already fetched due to status-fetching
531 // of a parent path.
532 // this check is only done for file paths, because folder paths could be included already
533 // but not recursively
534 if (sortedPathList[nTarget].IsDirectory() || !GetListEntry(sortedPathList[nTarget]))
536 if(!FetchStatusForSingleTarget(config, status, sortedPathList[nTarget], bUpdate, sUUID,
537 arExtPaths, false, git_depth_infinity, bShowIgnores))
539 bRet = FALSE;
545 // remove the 'helper' files of conflicted items from the list.
546 // otherwise they would appear as unversioned files.
547 for (INT_PTR cind = 0; cind < m_ConflictFileList.GetCount(); ++cind)
549 for (size_t i=0; i < m_arStatusArray.size(); i++)
551 if (m_arStatusArray[i]->GetPath().IsEquivalentTo(m_ConflictFileList[cind]))
553 delete m_arStatusArray[i];
554 m_arStatusArray.erase(m_arStatusArray.cbegin() + i);
555 break;
559 refetchcounter++;
560 } while(!BuildStatistics() && (refetchcounter < 2) && (*m_pbCanceled==false));
562 m_bBlock = FALSE;
563 m_bBusy = false;
564 GetCursorPos(&pt);
565 SetCursorPos(pt.x, pt.y);
566 return bRet;
567 #endif
568 m_bBusy = false;
569 m_bWaitCursor = false;
570 BuildStatistics();
571 return TRUE;
574 // Get the show-flags bitmap value which corresponds to a particular Git status
575 DWORD CGitStatusListCtrl::GetShowFlagsFromGitStatus(git_wc_status_kind status)
577 switch (status)
579 case git_wc_status_none:
580 case git_wc_status_unversioned:
581 return GITSLC_SHOWUNVERSIONED;
582 case git_wc_status_ignored:
583 if (!m_bShowIgnores)
584 return GITSLC_SHOWDIRECTS;
585 return GITSLC_SHOWDIRECTS|GITSLC_SHOWIGNORED;
586 case git_wc_status_normal:
587 return GITSLC_SHOWNORMAL;
588 case git_wc_status_added:
589 return GITSLC_SHOWADDED;
590 case git_wc_status_deleted:
591 return GITSLC_SHOWREMOVED;
592 case git_wc_status_modified:
593 return GITSLC_SHOWMODIFIED;
594 case git_wc_status_conflicted:
595 return GITSLC_SHOWCONFLICTED;
596 default:
597 // we should NEVER get here!
598 ASSERT(FALSE);
599 break;
601 return 0;
604 void CGitStatusListCtrl::Show(unsigned int dwShow, unsigned int dwCheck /*=0*/, bool /*bShowFolders*/ /* = true */,BOOL UpdateStatusList,bool UseStoredCheckStatus)
606 m_bWaitCursor = true;
607 m_bBusy = true;
608 m_bEmpty = false;
609 Invalidate();
611 WORD langID = (WORD)CRegStdDWORD(L"Software\\TortoiseGit\\LanguageID", GetUserDefaultLangID());
612 SetRedraw(FALSE);
614 CAutoWriteLock locker(m_guard);
615 DeleteAllItems();
616 m_nSelected = 0;
618 m_nShownUnversioned = 0;
619 m_nShownModified = 0;
620 m_nShownAdded = 0;
621 m_nShownDeleted = 0;
622 m_nShownConflicted = 0;
623 m_nShownFiles = 0;
624 m_nShownSubmodules = 0;
626 m_dwShow = dwShow;
628 if (UpdateStatusList)
630 m_arStatusArray.clear();
631 for (int i = 0; i < m_StatusFileList.GetCount(); ++i)
632 m_arStatusArray.push_back(&m_StatusFileList[i]);
634 for (int i = 0; i < m_UnRevFileList.GetCount(); ++i)
635 m_arStatusArray.push_back(&m_UnRevFileList[i]);
637 for (int i = 0; i < m_IgnoreFileList.GetCount(); ++i)
638 m_arStatusArray.push_back(&m_IgnoreFileList[i]);
640 PrepareGroups();
641 m_arListArray.clear();
642 m_arListArray.reserve(m_arStatusArray.size());
643 if (m_nSortedColumn >= 0)
645 CSorter predicate(&m_ColumnManager, m_nSortedColumn, m_bAscending);
646 std::stable_sort(m_arStatusArray.begin(), m_arStatusArray.end(), predicate);
649 int index = 0;
650 for (size_t i = 0; i < m_arStatusArray.size(); ++i)
652 //set default checkbox status
653 auto entry = const_cast<CTGitPath*>(m_arStatusArray[i]);
654 CString path = entry->GetGitPathString();
655 if (!m_mapFilenameToChecked.empty() && m_mapFilenameToChecked.find(path) != m_mapFilenameToChecked.end())
656 entry->m_Checked = m_mapFilenameToChecked[path];
657 else if (!UseStoredCheckStatus)
659 bool autoSelectSubmodules = !(entry->IsDirectory() && m_bDoNotAutoselectSubmodules);
660 if (((entry->m_Action & dwCheck) && !(m_bNoAutoselectMissing && entry->m_Action & CTGitPath::LOGACTIONS_MISSING) || dwShow & GITSLC_SHOWDIRECTFILES && m_setDirectFiles.find(path) != m_setDirectFiles.end()) && autoSelectSubmodules)
661 entry->m_Checked = true;
662 else
663 entry->m_Checked = false;
664 m_mapFilenameToChecked[path] = entry->m_Checked;
667 if (entry->m_Action & dwShow)
669 AddEntry(i, entry, langID, index);
670 index++;
675 AdjustColumnWidths();
677 SetRedraw(TRUE);
678 GetStatisticsString();
680 CHeaderCtrl * pHeader = GetHeaderCtrl();
681 HDITEM HeaderItem = {0};
682 HeaderItem.mask = HDI_FORMAT;
683 for (int i=0; i<pHeader->GetItemCount(); ++i)
685 pHeader->GetItem(i, &HeaderItem);
686 HeaderItem.fmt &= ~(HDF_SORTDOWN | HDF_SORTUP);
687 pHeader->SetItem(i, &HeaderItem);
689 if (m_nSortedColumn >= 0)
691 pHeader->GetItem(m_nSortedColumn, &HeaderItem);
692 HeaderItem.fmt |= (m_bAscending ? HDF_SORTUP : HDF_SORTDOWN);
693 pHeader->SetItem(m_nSortedColumn, &HeaderItem);
696 RestoreScrollPos();
698 m_bWaitCursor = false;
699 m_bBusy = false;
700 m_bEmpty = (GetItemCount() == 0);
701 Invalidate();
703 this->BuildStatistics();
705 #if 0
707 m_bShowFolders = bShowFolders;
709 int nTopIndex = GetTopIndex();
710 POSITION posSelectedEntry = GetFirstSelectedItemPosition();
711 int nSelectedEntry = 0;
712 if (posSelectedEntry)
713 nSelectedEntry = GetNextSelectedItem(posSelectedEntry);
714 SetRedraw(FALSE);
715 DeleteAllItems();
717 PrepareGroups();
719 m_arListArray.clear();
721 m_arListArray.reserve(m_arStatusArray.size());
722 SetItemCount (static_cast<int>(m_arStatusArray.size()));
724 int listIndex = 0;
725 for (size_t i=0; i < m_arStatusArray.size(); ++i)
727 FileEntry * entry = m_arStatusArray[i];
728 if ((entry->inexternal) && (!(dwShow & SVNSLC_SHOWINEXTERNALS)))
729 continue;
730 if ((entry->differentrepo || entry->isNested) && (! (dwShow & SVNSLC_SHOWEXTERNALFROMDIFFERENTREPO)))
731 continue;
732 if (entry->IsFolder() && (!bShowFolders))
733 continue; // don't show folders if they're not wanted.
735 #if 0
736 git_wc_status_kind status = GitStatus::GetMoreImportant(entry->status, entry->remotestatus);
737 DWORD showFlags = GetShowFlagsFromGitStatus(status);
738 if (entry->switched)
739 showFlags |= SVNSLC_SHOWSWITCHED;
740 if (!entry->changelist.IsEmpty())
741 showFlags |= SVNSLC_SHOWINCHANGELIST;
742 #endif
743 bool bAllowCheck = ((entry->changelist.Compare(GITSLC_IGNORECHANGELIST) != 0)
744 && (m_bCheckIfGroupsExist || (m_changelists.empty() || (m_changelists.size() == 1 && m_bHasIgnoreGroup))));
746 // status_ignored is a special case - we must have the 'direct' flag set to add a status_ignored item
747 #if 0
748 if (status != Git_wc_status_ignored || (entry->direct) || (dwShow & GitSLC_SHOWIGNORED))
750 if ((!entry->IsFolder()) && (status == Git_wc_status_deleted) && (dwShow & SVNSLC_SHOWREMOVEDANDPRESENT))
752 if (PathFileExists(entry->GetPath().GetWinPath()))
754 m_arListArray.push_back(i);
755 if ((dwCheck & SVNSLC_SHOWREMOVEDANDPRESENT)||((dwCheck & SVNSLC_SHOWDIRECTS)&&(entry->direct)))
757 if (bAllowCheck)
758 entry->checked = true;
760 AddEntry(entry, langID, listIndex++);
763 else if ((dwShow & showFlags)||((dwShow & SVNSLC_SHOWDIRECTFILES)&&(entry->direct)&&(!entry->IsFolder())))
765 m_arListArray.push_back(i);
766 if ((dwCheck & showFlags)||((dwCheck & SVNSLC_SHOWDIRECTS)&&(entry->direct)))
768 if (bAllowCheck)
769 entry->checked = true;
771 AddEntry(entry, langID, listIndex++);
773 else if ((dwShow & showFlags)||((dwShow & SVNSLC_SHOWDIRECTFOLDER)&&(entry->direct)&&entry->IsFolder()))
775 m_arListArray.push_back(i);
776 if ((dwCheck & showFlags)||((dwCheck & SVNSLC_SHOWDIRECTS)&&(entry->direct)))
778 if (bAllowCheck)
779 entry->checked = true;
781 AddEntry(entry, langID, listIndex++);
784 #endif
787 SetItemCount(listIndex);
789 m_ColumnManager.UpdateRelevance (m_arStatusArray, m_arListArray);
791 AdjustColumnWidths();
793 SetRedraw(TRUE);
794 GetStatisticsString();
796 CHeaderCtrl * pHeader = GetHeaderCtrl();
797 HDITEM HeaderItem = {0};
798 HeaderItem.mask = HDI_FORMAT;
799 for (int i=0; i<pHeader->GetItemCount(); ++i)
801 pHeader->GetItem(i, &HeaderItem);
802 HeaderItem.fmt &= ~(HDF_SORTDOWN | HDF_SORTUP);
803 pHeader->SetItem(i, &HeaderItem);
805 if (m_nSortedColumn)
807 pHeader->GetItem(m_nSortedColumn, &HeaderItem);
808 HeaderItem.fmt |= (m_bAscending ? HDF_SORTUP : HDF_SORTDOWN);
809 pHeader->SetItem(m_nSortedColumn, &HeaderItem);
812 if (nSelectedEntry)
814 SetItemState(nSelectedEntry, LVIS_SELECTED, LVIS_SELECTED);
815 EnsureVisible(nSelectedEntry, false);
817 else
819 // Restore the item at the top of the list.
820 for (int i=0;GetTopIndex() != nTopIndex;i++)
822 if ( !EnsureVisible(nTopIndex+i,false) )
823 break;
827 m_bEmpty = (GetItemCount() == 0);
828 Invalidate();
829 #endif
832 void CGitStatusListCtrl::StoreScrollPos()
834 m_sScrollPos.enabled = true;
835 m_sScrollPos.nTopIndex = GetTopIndex();
836 m_sScrollPos.selMark = GetSelectionMark();
837 POSITION posSelectedEntry = GetFirstSelectedItemPosition();
838 m_sScrollPos.nSelectedEntry = 0;
839 if (posSelectedEntry)
840 m_sScrollPos.nSelectedEntry = GetNextSelectedItem(posSelectedEntry);
843 void CGitStatusListCtrl::RestoreScrollPos()
845 if (!m_sScrollPos.enabled || CRegDWORD(L"Software\\TortoiseGit\\RememberFileListPosition", TRUE) != TRUE)
846 return;
848 if (m_sScrollPos.nSelectedEntry)
850 SetItemState(m_sScrollPos.nSelectedEntry, LVIS_SELECTED, LVIS_SELECTED);
851 EnsureVisible(m_sScrollPos.nSelectedEntry, false);
853 else
855 // Restore the item at the top of the list.
856 for (int i = 0; GetTopIndex() != m_sScrollPos.nTopIndex; ++i)
858 if (!EnsureVisible(m_sScrollPos.nTopIndex + i, false))
859 break;
862 if (m_sScrollPos.selMark >= 0)
864 SetSelectionMark(m_sScrollPos.selMark);
865 SetItemState(m_sScrollPos.selMark, LVIS_FOCUSED, LVIS_FOCUSED);
867 m_sScrollPos.enabled = false;
870 int CGitStatusListCtrl::GetColumnIndex(int mask)
872 for (int i = 0; i < 32; ++i)
873 if(mask&0x1)
874 return i;
875 else
876 mask=mask>>1;
877 return -1;
880 CString CGitStatusListCtrl::GetCellText(int listIndex, int column)
882 static CString from(MAKEINTRESOURCE(IDS_STATUSLIST_FROM));
883 static bool abbreviateRenamings(((DWORD)CRegDWORD(L"Software\\TortoiseGit\\AbbreviateRenamings", FALSE)) == TRUE);
884 static bool relativeTimes = (CRegDWORD(L"Software\\TortoiseGit\\RelativeTimes", FALSE) != FALSE);
885 static const CString empty;
887 CAutoReadLock locker(m_guard);
888 const auto* entry = GetListEntry(listIndex);
889 if (!entry)
890 return empty;
892 switch (column)
894 case 0: // relative path
895 if (!(entry->m_Action & (CTGitPath::LOGACTIONS_REPLACED | CTGitPath::LOGACTIONS_COPY) && !entry->GetGitOldPathString().IsEmpty()))
896 return entry->GetGitPathString();
898 if (!abbreviateRenamings)
900 CString entryname = entry->GetGitPathString();
901 entryname += L' ';
902 // relative path
903 entryname.AppendFormat(from, (LPCTSTR)entry->GetGitOldPathString());
904 return entryname;
907 return entry->GetAbbreviatedRename();
909 case 1: // GITSLC_COLFILENAME
910 return entry->GetFileOrDirectoryName();
912 case 2: // GITSLC_COLEXT
913 return entry->GetFileExtension();
915 case 3: // GITSLC_COLSTATUS
916 return entry->GetActionName();
918 case 4: // GITSLC_COLADD
919 return entry->m_StatAdd;
921 case 5: // GITSLC_COLDEL
922 return entry->m_StatDel;
924 case 6: // GITSLC_COLMODIFICATIONDATE
925 if (!(entry->m_Action & CTGitPath::LOGACTIONS_DELETED) && m_ColumnManager.IsRelevant(GetColumnIndex(GITSLC_COLMODIFICATIONDATE)))
927 CString modificationDate;
928 __int64 filetime = entry->GetLastWriteTime();
929 if (filetime)
931 FILETIME* f = (FILETIME*)(__int64*)&filetime;
932 modificationDate = CLoglistUtils::FormatDateAndTime(CTime(CGit::filetime_to_time_t(f)), DATE_SHORTDATE, true, relativeTimes);
934 return modificationDate;
936 return empty;
938 case 7: // GITSLC_COLSIZE
939 if (!(entry->IsDirectory() || !m_ColumnManager.IsRelevant(GetColumnIndex(GITSLC_COLSIZE))))
941 TCHAR buf[100] = { 0 };
942 StrFormatByteSize64(entry->GetFileSize(), buf, 100);
943 return buf;
945 return empty;
947 #if 0
948 default: // user-defined properties
949 if (column < m_ColumnManager.GetColumnCount())
951 assert(m_ColumnManager.IsUserProp(column));
953 const CString& name = m_ColumnManager.GetName(column);
954 auto propEntry = m_PropertyMap.find(entry->GetPath());
955 if (propEntry != m_PropertyMap.end())
957 if (propEntry->second.HasProperty(name))
959 const CString& propVal = propEntry->second[name];
960 return propVal.IsEmpty()
961 ? m_sNoPropValueText
962 : propVal;
966 #endif
968 return empty;
971 void CGitStatusListCtrl::AddEntry(size_t arStatusArrayIndex, CTGitPath * GitPath, WORD /*langID*/, int listIndex)
973 CAutoWriteLock locker(m_guard);
974 ScopedInDecrement blocker(m_nBlockItemChangeHandler);
975 CString path = GitPath->GetGitPathString();
977 // Load the icons *now* so the icons are cached when showing them later in the
978 // WM_PAINT handler.
979 // Problem is that (at least on Win10), loading the icons in the WM_PAINT
980 // handler triggers an OLE operation, which should not happen in WM_PAINT at all
981 // (see ..\VC\atlmfc\src\mfc\olemsgf.cpp, COleMessageFilter::OnMessagePending() for details about this)
982 // By loading the icons here, they get cached and the OLE operation won't happen
983 // later in the WM_PAINT handler.
984 // This solves the 'hang' which happens in the commit dialog if images are
985 // shown in the file list.
986 int icon_idx = 0;
987 if (GitPath->IsDirectory())
989 icon_idx = m_nIconFolder;
990 m_nShownSubmodules++;
992 else
994 icon_idx = SYS_IMAGE_LIST().GetPathIconIndex(*GitPath);
995 m_nShownFiles++;
997 switch (GitPath->m_Action)
999 case CTGitPath::LOGACTIONS_ADDED:
1000 case CTGitPath::LOGACTIONS_COPY:
1001 m_nShownAdded++;
1002 break;
1003 case CTGitPath::LOGACTIONS_DELETED:
1004 case CTGitPath::LOGACTIONS_MISSING:
1005 case CTGitPath::LOGACTIONS_DELETED | CTGitPath::LOGACTIONS_MISSING:
1006 m_nShownDeleted++;
1007 break;
1008 case CTGitPath::LOGACTIONS_REPLACED:
1009 case CTGitPath::LOGACTIONS_MODIFIED:
1010 case CTGitPath::LOGACTIONS_MERGED:
1011 m_nShownModified++;
1012 break;
1013 case CTGitPath::LOGACTIONS_UNMERGED:
1014 case CTGitPath::LOGACTIONS_UNMERGED | CTGitPath::LOGACTIONS_ADDED:
1015 case CTGitPath::LOGACTIONS_UNMERGED | CTGitPath::LOGACTIONS_MODIFIED:
1016 m_nShownConflicted++;
1017 break;
1018 case CTGitPath::LOGACTIONS_UNVER:
1019 m_nShownUnversioned++;
1020 break;
1021 default:
1022 m_nShownUnversioned++;
1023 break;
1026 LVITEM lvItem = { 0 };
1027 lvItem.iItem = listIndex;
1028 lvItem.lParam = (LPARAM)GitPath;
1029 lvItem.mask = LVIF_TEXT | LVIF_IMAGE | LVIF_STATE | LVIF_PARAM;
1030 lvItem.pszText = LPSTR_TEXTCALLBACK;
1031 lvItem.stateMask = LVIS_OVERLAYMASK;
1032 if (m_restorepaths.find(GitPath->GetWinPathString()) != m_restorepaths.end())
1033 lvItem.state = INDEXTOOVERLAYMASK(OVL_RESTORE);
1034 lvItem.iImage = icon_idx;
1035 InsertItem(&lvItem);
1037 m_arListArray.push_back(arStatusArrayIndex);
1039 SetCheck(listIndex, GitPath->m_Checked);
1040 if (GitPath->m_Checked)
1041 m_nSelected++;
1043 if ((GitPath->m_Action & CTGitPath::LOGACTIONS_SKIPWORKTREE) || (GitPath->m_Action & CTGitPath::LOGACTIONS_ASSUMEVALID))
1044 SetItemGroup(listIndex, 3);
1045 else if (GitPath->m_Action & CTGitPath::LOGACTIONS_IGNORE)
1046 SetItemGroup(listIndex, 2);
1047 else if( GitPath->m_Action & CTGitPath::LOGACTIONS_UNVER)
1048 SetItemGroup(listIndex,1);
1049 else
1050 SetItemGroup(listIndex, GitPath->m_ParentNo&(PARENT_MASK|MERGE_MASK));
1053 bool CGitStatusListCtrl::SetItemGroup(int item, int groupindex)
1055 CAutoWriteLock locker(m_guard);
1056 // if (!(m_dwContextMenus & SVNSLC_POPCHANGELISTS))
1057 // return false;
1058 if (groupindex < 0)
1059 return false;
1060 LVITEM i = {0};
1061 i.mask = LVIF_GROUPID;
1062 i.iItem = item;
1063 i.iSubItem = 0;
1064 i.iGroupId = groupindex;
1066 return !!SetItem(&i);
1069 void CGitStatusListCtrl::OnHdnItemclick(NMHDR *pNMHDR, LRESULT *pResult)
1071 LPNMHEADER phdr = reinterpret_cast<LPNMHEADER>(pNMHDR);
1072 *pResult = 0;
1074 CAutoReadWeakLock lock(m_guard);
1075 if (!lock.IsAcquired())
1076 return;
1078 if (m_arStatusArray.empty())
1079 return;
1081 if (m_nSortedColumn == phdr->iItem)
1082 m_bAscending = !m_bAscending;
1083 else
1084 m_bAscending = TRUE;
1085 m_nSortedColumn = phdr->iItem;
1086 Show(m_dwShow, 0, m_bShowFolders,false,true);
1089 BOOL CGitStatusListCtrl::OnLvnItemchanged(NMHDR *pNMHDR, LRESULT *pResult)
1091 LPNMLISTVIEW pNMLV = reinterpret_cast<LPNMLISTVIEW>(pNMHDR);
1092 *pResult = 0;
1093 CWnd* pParent = GetLogicalParent();
1094 if (pParent && pParent->GetSafeHwnd())
1095 pParent->SendMessage(GITSLNM_ITEMCHANGED, pNMLV->iItem);
1097 if (m_nBlockItemChangeHandler)
1098 return FALSE;
1100 if ((pNMLV->uNewState==0)||(pNMLV->uNewState & LVIS_SELECTED)||(pNMLV->uNewState & LVIS_FOCUSED))
1101 return FALSE;
1103 CAutoWriteWeakLock writeLock(m_guard);
1104 if (!writeLock.IsAcquired())
1106 NotifyCheck();
1107 return FALSE;
1110 bool bSelected = !!(ListView_GetItemState(m_hWnd, pNMLV->iItem, LVIS_SELECTED) & LVIS_SELECTED);
1111 int nListItems = GetItemCount();
1113 // was the item checked?
1115 if (GetCheck(pNMLV->iItem))
1118 ScopedInDecrement blocker(m_nBlockItemChangeHandler);
1119 CheckEntry(pNMLV->iItem, nListItems);
1121 if (bSelected)
1123 ScopedInDecrement blocker(m_nBlockItemChangeHandler);
1124 POSITION pos = GetFirstSelectedItemPosition();
1125 int index;
1126 while ((index = GetNextSelectedItem(pos)) >= 0)
1128 if (index != pNMLV->iItem)
1129 CheckEntry(index, nListItems);
1133 else
1136 ScopedInDecrement blocker(m_nBlockItemChangeHandler);
1137 UncheckEntry(pNMLV->iItem, nListItems);
1139 if (bSelected)
1141 ScopedInDecrement blocker(m_nBlockItemChangeHandler);
1142 POSITION pos = GetFirstSelectedItemPosition();
1143 int index;
1144 while ((index = GetNextSelectedItem(pos)) >= 0)
1146 if (index != pNMLV->iItem)
1147 UncheckEntry(index, nListItems);
1152 GetStatisticsString();
1153 NotifyCheck();
1155 return FALSE;
1158 void CGitStatusListCtrl::CheckEntry(int index, int /*nListItems*/)
1160 CAutoWriteLock locker(m_guard);
1161 auto path = GetListEntry(index);
1162 if (!path)
1163 return;
1164 m_mapFilenameToChecked[path->GetGitPathString()] = true;
1165 SetCheck(index, TRUE);
1166 // if an unversioned item was checked, then we need to check if
1167 // the parent folders are unversioned too. If the parent folders actually
1168 // are unversioned, then check those too.
1169 #if 0
1170 if (entry->status == git_wc_status_unversioned)
1172 // we need to check the parent folder too
1173 const CTGitPath& folderpath = entry->path;
1174 for (int i=0; i< nListItems; ++i)
1176 FileEntry * testEntry = GetListEntry(i);
1177 ASSERT(testEntry);
1178 if (!testEntry)
1179 continue;
1180 if (!testEntry->checked)
1182 if (testEntry->path.IsAncestorOf(folderpath) && (!testEntry->path.IsEquivalentTo(folderpath)))
1184 SetEntryCheck(testEntry,i,true);
1185 m_nSelected++;
1190 bool bShift = !!(GetAsyncKeyState(VK_SHIFT) & 0x8000);
1191 if ( (entry->status == git_wc_status_deleted) || (m_bCheckChildrenWithParent) || (bShift) )
1193 // if a deleted folder gets checked, we have to check all
1194 // children of that folder too.
1195 if (entry->path.IsDirectory())
1196 SetCheckOnAllDescendentsOf(entry, true);
1198 // if a deleted file or folder gets checked, we have to
1199 // check all parents of this item too.
1200 for (int i=0; i<nListItems; ++i)
1202 FileEntry * testEntry = GetListEntry(i);
1203 ASSERT(testEntry);
1204 if (!testEntry)
1205 continue;
1206 if (!testEntry->checked)
1208 if (testEntry->path.IsAncestorOf(entry->path) && (!testEntry->path.IsEquivalentTo(entry->path)))
1210 if ((testEntry->status == git_wc_status_deleted)||(m_bCheckChildrenWithParent))
1212 SetEntryCheck(testEntry,i,true);
1213 m_nSelected++;
1214 // now we need to check all children of this parent folder
1215 SetCheckOnAllDescendentsOf(testEntry, true);
1221 #endif
1222 if ( !path->m_Checked )
1224 path->m_Checked = TRUE;
1225 m_nSelected++;
1229 void CGitStatusListCtrl::UncheckEntry(int index, int /*nListItems*/)
1231 CAutoWriteLock locker(m_guard);
1232 auto path = GetListEntry(index);
1233 if (!path)
1234 return;
1235 SetCheck(index, FALSE);
1236 m_mapFilenameToChecked[path->GetGitPathString()] = false;
1237 // item was unchecked
1238 #if 0
1239 if (entry->path.IsDirectory())
1241 // disable all files within an unselected folder, except when unchecking a folder with property changes
1242 bool bShift = !!(GetAsyncKeyState(VK_SHIFT) & 0x8000);
1243 if ( (entry->status != git_wc_status_modified) || (bShift) )
1245 SetCheckOnAllDescendentsOf(entry, false);
1248 else if (entry->status == git_wc_status_deleted)
1250 // a "deleted" file was unchecked, so uncheck all parent folders
1251 // and all children of those parents
1252 for (int i=0; i<nListItems; i++)
1254 FileEntry * testEntry = GetListEntry(i);
1255 ASSERT(testEntry);
1256 if (!testEntry)
1257 continue;
1258 if (testEntry->checked)
1260 if (testEntry->path.IsAncestorOf(entry->path))
1262 if (testEntry->status == git_wc_status_deleted)
1264 SetEntryCheck(testEntry,i,false);
1265 m_nSelected--;
1267 SetCheckOnAllDescendentsOf(testEntry, false);
1273 #endif
1274 if ( path->m_Checked )
1276 path->m_Checked = FALSE;
1277 m_nSelected--;
1280 void CGitStatusListCtrl::BuildStatistics()
1282 CAutoReadLock locker(m_guard);
1284 // now gather some statistics
1285 m_nUnversioned = 0;
1286 m_nNormal = 0;
1287 m_nModified = 0;
1288 m_nAdded = 0;
1289 m_nDeleted = 0;
1290 m_nConflicted = 0;
1291 m_nTotal = 0;
1292 m_nSelected = 0;
1293 m_nLineAdded = 0;
1294 m_nLineDeleted = 0;
1295 m_nRenamed = 0;
1297 for (size_t i = 0; i < m_arStatusArray.size(); ++i)
1299 int status = m_arStatusArray[i]->m_Action;
1301 m_nLineAdded += _wtol(m_arStatusArray[i]->m_StatAdd);
1302 m_nLineDeleted += _wtol(m_arStatusArray[i]->m_StatDel);
1304 if(status&(CTGitPath::LOGACTIONS_ADDED|CTGitPath::LOGACTIONS_COPY))
1305 m_nAdded++;
1307 if(status&CTGitPath::LOGACTIONS_DELETED)
1308 m_nDeleted++;
1310 if(status&(CTGitPath::LOGACTIONS_REPLACED|CTGitPath::LOGACTIONS_MODIFIED))
1311 m_nModified++;
1313 if(status&CTGitPath::LOGACTIONS_UNMERGED)
1314 m_nConflicted++;
1316 if(status&(CTGitPath::LOGACTIONS_IGNORE|CTGitPath::LOGACTIONS_UNVER))
1317 m_nUnversioned++;
1319 if(status&(CTGitPath::LOGACTIONS_REPLACED))
1320 m_nRenamed++;
1322 if (m_arStatusArray[i]->m_Checked)
1323 m_nSelected++;
1327 int CGitStatusListCtrl::GetGroupFromPoint(POINT * ppt)
1329 // the point must be relative to the upper left corner of the control
1331 if (!ppt)
1332 return -1;
1333 if (!IsGroupViewEnabled())
1334 return -1;
1336 POINT pt = *ppt;
1337 pt.x = 10;
1338 UINT flags = 0;
1339 int nItem = -1;
1340 RECT rc;
1341 GetWindowRect(&rc);
1342 while (((flags & LVHT_BELOW) == 0)&&(pt.y < rc.bottom))
1344 nItem = HitTest(pt, &flags);
1345 if ((flags & LVHT_ONITEM)||(flags & LVHT_EX_GROUP_HEADER))
1347 // the first item below the point
1349 // check if the point is too much right (i.e. if the point
1350 // is farther to the right than the width of the item)
1351 RECT r;
1352 GetItemRect(nItem, &r, LVIR_LABEL);
1353 if (ppt->x > r.right)
1354 return -1;
1356 LVITEM lv = {0};
1357 lv.mask = LVIF_GROUPID;
1358 lv.iItem = nItem;
1359 GetItem(&lv);
1360 int groupID = lv.iGroupId;
1361 // now we search upwards and check if the item above this one
1362 // belongs to another group. If it belongs to the same group,
1363 // we're not over a group header
1364 while (pt.y >= 0)
1366 pt.y -= 2;
1367 nItem = HitTest(pt, &flags);
1368 if ((flags & LVHT_ONITEM)&&(nItem >= 0))
1370 // the first item below the point
1371 LVITEM lv2 = {0};
1372 lv2.mask = LVIF_GROUPID;
1373 lv2.iItem = nItem;
1374 GetItem(&lv2);
1375 if (lv2.iGroupId != groupID)
1376 return groupID;
1377 else
1378 return -1;
1381 if (pt.y < 0)
1382 return groupID;
1383 return -1;
1385 pt.y += 2;
1387 return -1;
1390 void CGitStatusListCtrl::OnContextMenuGroup(CWnd * /*pWnd*/, CPoint point)
1392 POINT clientpoint = point;
1393 ScreenToClient(&clientpoint);
1394 if ((IsGroupViewEnabled())&&(GetGroupFromPoint(&clientpoint) >= 0))
1396 CAutoReadWeakLock readLock(m_guard);
1397 if (!readLock.IsAcquired())
1398 return;
1400 CMenu popup;
1401 if (popup.CreatePopupMenu())
1403 CString temp;
1404 temp.LoadString(IDS_STATUSLIST_CHECKGROUP);
1405 popup.AppendMenu(MF_STRING | MF_ENABLED, IDGITLC_CHECKGROUP, temp);
1406 temp.LoadString(IDS_STATUSLIST_UNCHECKGROUP);
1407 popup.AppendMenu(MF_STRING | MF_ENABLED, IDGITLC_UNCHECKGROUP, temp);
1408 int cmd = popup.TrackPopupMenu(TPM_RETURNCMD | TPM_LEFTALIGN | TPM_NONOTIFY, point.x, point.y, this);
1409 bool bCheck = false;
1410 switch (cmd)
1412 case IDGITLC_CHECKGROUP:
1413 bCheck = true;
1414 // fall through here
1415 case IDGITLC_UNCHECKGROUP:
1417 int group = GetGroupFromPoint(&clientpoint);
1418 // go through all items and check/uncheck those assigned to the group
1419 // but block the OnLvnItemChanged handler
1420 for (int i=0; i<GetItemCount(); ++i)
1422 LVITEM lv = { 0 };
1423 lv.mask = LVIF_GROUPID;
1424 lv.iItem = i;
1425 GetItem(&lv);
1427 if (lv.iGroupId == group)
1429 auto entry = GetListEntry(i);
1430 if (entry)
1432 bool bOldCheck = entry->m_Checked;
1433 SetEntryCheck(entry, i, bCheck);
1434 if (bCheck != bOldCheck)
1436 if (bCheck)
1437 m_nSelected++;
1438 else
1439 m_nSelected--;
1445 GetStatisticsString();
1446 NotifyCheck();
1448 break;
1454 void CGitStatusListCtrl::OnContextMenuList(CWnd * pWnd, CPoint point)
1456 bool bShift = !!(GetAsyncKeyState(VK_SHIFT) & 0x8000);
1458 CAutoWriteWeakLock writeLock(m_guard);
1459 if (!writeLock.IsAcquired())
1460 return;
1462 auto selectedCount = GetSelectedCount();
1463 int selIndex = GetSelectionMark();
1464 int selSubitem = -1;
1465 if (selectedCount > 0)
1467 CPoint pt = point;
1468 ScreenToClient(&pt);
1469 LVHITTESTINFO hittest = { 0 };
1470 hittest.flags = LVHT_ONITEM;
1471 hittest.pt = pt;
1472 if (this->SubItemHitTest(&hittest) >= 0)
1473 selSubitem = hittest.iSubItem;
1475 if ((point.x == -1) && (point.y == -1))
1477 CRect rect;
1478 GetItemRect(selIndex, &rect, LVIR_LABEL);
1479 ClientToScreen(&rect);
1480 point = rect.CenterPoint();
1482 if (selectedCount == 0 && m_bHasCheckboxes)
1484 // nothing selected could mean the context menu is requested for
1485 // a group header
1486 OnContextMenuGroup(pWnd, point);
1488 else if (selIndex >= 0)
1490 auto filepath = GetListEntry(selIndex);
1491 if (!filepath)
1492 return;
1494 //const CTGitPath& filepath = entry->path;
1495 int wcStatus = filepath->m_Action;
1496 // entry is selected, now show the popup menu
1497 CIconMenu popup;
1498 CMenu changelistSubMenu;
1499 CMenu ignoreSubMenu;
1500 CIconMenu clipSubMenu;
1501 CMenu shellMenu;
1502 if (popup.CreatePopupMenu())
1504 //Add Menu for version controlled file
1506 if (selectedCount > 0 && (wcStatus & CTGitPath::LOGACTIONS_UNMERGED))
1508 if (selectedCount == 1 && (m_dwContextMenus & GITSLC_POPCONFLICT))
1510 popup.AppendMenuIcon(IDGITLC_EDITCONFLICT, IDS_MENUCONFLICT, IDI_CONFLICT);
1511 popup.SetDefaultItem(IDGITLC_EDITCONFLICT, FALSE);
1513 if (m_dwContextMenus & GITSLC_POPRESOLVE)
1515 popup.AppendMenuIcon(IDGITLC_RESOLVECONFLICT, IDS_STATUSLIST_CONTEXT_RESOLVED, IDI_RESOLVE);
1516 CString tmp, mineTitle, theirsTitle;
1517 CAppUtils::GetConflictTitles(nullptr, mineTitle, theirsTitle, m_bIsRevertTheirMy);
1518 tmp.Format(IDS_SVNPROGRESS_MENURESOLVEUSING, (LPCTSTR)theirsTitle);
1519 if (m_bIsRevertTheirMy)
1521 popup.AppendMenuIcon(IDGITLC_RESOLVEMINE, tmp, IDI_RESOLVE);
1522 tmp.Format(IDS_SVNPROGRESS_MENURESOLVEUSING, (LPCTSTR)mineTitle);
1523 popup.AppendMenuIcon(IDGITLC_RESOLVETHEIRS, tmp, IDI_RESOLVE);
1525 else
1527 popup.AppendMenuIcon(IDGITLC_RESOLVETHEIRS, tmp, IDI_RESOLVE);
1528 tmp.Format(IDS_SVNPROGRESS_MENURESOLVEUSING, (LPCTSTR)mineTitle);
1529 popup.AppendMenuIcon(IDGITLC_RESOLVEMINE, tmp, IDI_RESOLVE);
1532 if ((m_dwContextMenus & GITSLC_POPCONFLICT)||(m_dwContextMenus & GITSLC_POPRESOLVE))
1533 popup.AppendMenu(MF_SEPARATOR);
1536 if (selectedCount > 0)
1538 if (wcStatus & (CTGitPath::LOGACTIONS_UNVER | CTGitPath::LOGACTIONS_IGNORE))
1540 if (m_dwContextMenus & GITSLC_POPADD)
1541 popup.AppendMenuIcon(IDGITLC_ADD, IDS_STATUSLIST_CONTEXT_ADD, IDI_ADD);
1542 if (m_dwContextMenus & GITSLC_POPCOMMIT)
1543 popup.AppendMenuIcon(IDGITLC_COMMIT, IDS_STATUSLIST_CONTEXT_COMMIT, IDI_COMMIT);
1547 if (!(wcStatus & (CTGitPath::LOGACTIONS_UNVER | CTGitPath::LOGACTIONS_IGNORE)) && !((wcStatus & CTGitPath::LOGACTIONS_MISSING) && wcStatus != (CTGitPath::LOGACTIONS_MISSING | CTGitPath::LOGACTIONS_DELETED) && wcStatus != (CTGitPath::LOGACTIONS_MISSING | CTGitPath::LOGACTIONS_DELETED | CTGitPath::LOGACTIONS_MODIFIED)) && selectedCount > 0)
1549 bool bEntryAdded = false;
1550 if (m_dwContextMenus & GITSLC_POPCOMPAREWITHBASE)
1552 if(filepath->m_ParentNo & MERGE_MASK)
1553 popup.AppendMenuIcon(IDGITLC_COMPARE, IDS_TREE_DIFF, IDI_DIFF);
1554 else
1555 popup.AppendMenuIcon(IDGITLC_COMPARE, IDS_LOG_COMPAREWITHBASE, IDI_DIFF);
1557 if (!(wcStatus & (CTGitPath::LOGACTIONS_UNMERGED)) || selectedCount != 1)
1558 popup.SetDefaultItem(IDGITLC_COMPARE, FALSE);
1559 bEntryAdded = true;
1562 if (!g_Git.IsInitRepos() && (m_dwContextMenus & GITSLC_POPGNUDIFF))
1564 popup.AppendMenuIcon(IDGITLC_GNUDIFF1, IDS_LOG_POPUP_GNUDIFF, IDI_DIFF);
1565 bEntryAdded = true;
1568 if ((m_dwContextMenus & this->GetContextMenuBit(IDGITLC_COMPAREWC)) && m_bHasWC)
1570 if ((!m_CurrentVersion.IsEmpty()) && m_CurrentVersion != GIT_REV_ZERO)
1572 popup.AppendMenuIcon(IDGITLC_COMPAREWC, IDS_LOG_POPUP_COMPARE, IDI_DIFF);
1573 bEntryAdded = true;
1577 if (bEntryAdded)
1578 popup.AppendMenu(MF_SEPARATOR);
1581 if (!m_Rev1.IsEmpty() && !m_Rev2.IsEmpty())
1583 if (m_dwContextMenus & this->GetContextMenuBit(IDGITLC_COMPARETWOREVISIONS))
1585 popup.AppendMenuIcon(IDGITLC_COMPARETWOREVISIONS, IDS_LOG_POPUP_COMPARETWO, IDI_DIFF);
1586 popup.SetDefaultItem(IDGITLC_COMPARETWOREVISIONS, FALSE);
1588 if (m_dwContextMenus & this->GetContextMenuBit(IDGITLC_GNUDIFF2REVISIONS))
1589 popup.AppendMenuIcon(IDGITLC_GNUDIFF2REVISIONS, IDS_LOG_POPUP_GNUDIFF, IDI_DIFF);
1592 //Select Multi item
1593 if (selectedCount > 0)
1595 if (selectedCount == 2 && (m_dwContextMenus & GITSLC_POPCOMPARETWOFILES))
1597 POSITION pos = GetFirstSelectedItemPosition();
1598 int index = GetNextSelectedItem(pos);
1599 if (index >= 0)
1601 auto entry2 = GetListEntry(index);
1602 bool firstEntryExistsAndIsFile = entry2 && !entry2->IsDirectory();
1603 index = GetNextSelectedItem(pos);
1604 if (index >= 0)
1606 entry2 = GetListEntry(index);
1607 if (firstEntryExistsAndIsFile && entry2 && !entry2->IsDirectory())
1608 popup.AppendMenuIcon(IDGITLC_COMPARETWOFILES, IDS_STATUSLIST_CONTEXT_COMPARETWOFILES, IDI_DIFF);
1614 if (selectedCount > 0 && (!(wcStatus & (CTGitPath::LOGACTIONS_UNVER | CTGitPath::LOGACTIONS_IGNORE))) && m_bHasWC)
1616 if ((m_dwContextMenus & GITSLC_POPCOMMIT) && (this->m_CurrentVersion.IsEmpty() || this->m_CurrentVersion == GIT_REV_ZERO) && !(wcStatus & (CTGitPath::LOGACTIONS_SKIPWORKTREE | CTGitPath::LOGACTIONS_ASSUMEVALID)))
1617 popup.AppendMenuIcon(IDGITLC_COMMIT, IDS_STATUSLIST_CONTEXT_COMMIT, IDI_COMMIT);
1619 if ((m_dwContextMenus & GITSLC_POPREVERT) && (this->m_CurrentVersion.IsEmpty() || this->m_CurrentVersion == GIT_REV_ZERO))
1620 popup.AppendMenuIcon(IDGITLC_REVERT, IDS_MENUREVERT, IDI_REVERT);
1622 if ((m_dwContextMenus & GITSLC_POPSKIPWORKTREE) && (this->m_CurrentVersion.IsEmpty() || this->m_CurrentVersion == GIT_REV_ZERO) && !(wcStatus & (CTGitPath::LOGACTIONS_ADDED | CTGitPath::LOGACTIONS_UNMERGED | CTGitPath::LOGACTIONS_SKIPWORKTREE)))
1623 popup.AppendMenuIcon(IDGITLC_SKIPWORKTREE, IDS_STATUSLIST_SKIPWORKTREE);
1625 if ((m_dwContextMenus & GITSLC_POPASSUMEVALID) && (this->m_CurrentVersion.IsEmpty() || this->m_CurrentVersion == GIT_REV_ZERO) && !(wcStatus & (CTGitPath::LOGACTIONS_ADDED | CTGitPath::LOGACTIONS_DELETED | CTGitPath::LOGACTIONS_UNMERGED | CTGitPath::LOGACTIONS_ASSUMEVALID)))
1626 popup.AppendMenuIcon(IDGITLC_ASSUMEVALID, IDS_MENUASSUMEVALID);
1628 if ((m_dwContextMenus & GITSLC_POPUNSETIGNORELOCALCHANGES) && (this->m_CurrentVersion.IsEmpty() || this->m_CurrentVersion == GIT_REV_ZERO) && (wcStatus & (CTGitPath::LOGACTIONS_SKIPWORKTREE | CTGitPath::LOGACTIONS_ASSUMEVALID)))
1629 popup.AppendMenuIcon(IDGITLC_UNSETIGNORELOCALCHANGES, IDS_STATUSLIST_UNSETIGNORELOCALCHANGES);
1631 if (m_dwContextMenus & GITSLC_POPRESTORE && !filepath->IsDirectory())
1633 if (m_restorepaths.find(filepath->GetWinPathString()) == m_restorepaths.end())
1634 popup.AppendMenuIcon(IDGITLC_CREATERESTORE, IDS_MENUCREATERESTORE, IDI_RESTORE);
1635 else
1636 popup.AppendMenuIcon(IDGITLC_RESTOREPATH, IDS_MENURESTORE, IDI_RESTORE);
1639 if ((m_dwContextMenus & GetContextMenuBit(IDGITLC_REVERTTOREV)) && ( !this->m_CurrentVersion.IsEmpty() )
1640 && this->m_CurrentVersion != GIT_REV_ZERO && !(wcStatus & CTGitPath::LOGACTIONS_DELETED))
1642 popup.AppendMenuIcon(IDGITLC_REVERTTOREV, IDS_LOG_POPUP_REVERTTOREV, IDI_REVERT);
1645 if ((m_dwContextMenus & GetContextMenuBit(IDGITLC_REVERTTOPARENT)) && ( !this->m_CurrentVersion.IsEmpty() )
1646 && this->m_CurrentVersion != GIT_REV_ZERO && !(wcStatus & CTGitPath::LOGACTIONS_ADDED))
1648 popup.AppendMenuIcon(IDGITLC_REVERTTOPARENT, IDS_LOG_POPUP_REVERTTOPARENT, IDI_REVERT);
1652 if (selectedCount == 1 && !(wcStatus & (CTGitPath::LOGACTIONS_UNVER | CTGitPath::LOGACTIONS_IGNORE)))
1654 if (m_dwContextMenus & GITSLC_POPSHOWLOG)
1655 popup.AppendMenuIcon(IDGITLC_LOG, IDS_REPOBROWSE_SHOWLOG, IDI_LOG);
1656 if (m_dwContextMenus & GITSLC_POPSHOWLOGSUBMODULE && filepath->IsDirectory())
1657 popup.AppendMenuIcon(IDGITLC_LOGSUBMODULE, IDS_LOG_SUBMODULE, IDI_LOG);
1658 if (m_dwContextMenus & GITSLC_POPSHOWLOGOLDNAME && (wcStatus & (CTGitPath::LOGACTIONS_REPLACED|CTGitPath::LOGACTIONS_COPY) && !filepath->GetGitOldPathString().IsEmpty()))
1659 popup.AppendMenuIcon(IDGITLC_LOGOLDNAME, IDS_STATUSLIST_SHOWLOGOLDNAME, IDI_LOG);
1660 if ((m_dwContextMenus & GITSLC_POPBLAME) && !filepath->IsDirectory() && !(wcStatus & CTGitPath::LOGACTIONS_DELETED) && !((wcStatus & CTGitPath::LOGACTIONS_ADDED) && (m_CurrentVersion.IsEmpty() || m_CurrentVersion == GIT_REV_ZERO)) && m_bHasWC)
1661 popup.AppendMenuIcon(IDGITLC_BLAME, IDS_MENUBLAME, IDI_BLAME);
1664 if (selectedCount > 0)
1666 if ((m_dwContextMenus & GetContextMenuBit(IDGITLC_EXPORT)) && !(wcStatus & (CTGitPath::LOGACTIONS_DELETED | CTGitPath::LOGACTIONS_MISSING)))
1667 popup.AppendMenuIcon(IDGITLC_EXPORT, IDS_LOG_POPUP_EXPORT, IDI_EXPORT);
1670 if (selectedCount == 1)
1672 if (m_dwContextMenus & this->GetContextMenuBit(IDGITLC_SAVEAS) && ! filepath->IsDirectory() && !(wcStatus & CTGitPath::LOGACTIONS_DELETED))
1673 popup.AppendMenuIcon(IDGITLC_SAVEAS, IDS_LOG_POPUP_SAVE, IDI_SAVEAS);
1675 if (m_dwContextMenus & GITSLC_POPOPEN && !filepath->IsDirectory() && !(wcStatus & (CTGitPath::LOGACTIONS_DELETED | CTGitPath::LOGACTIONS_MISSING)))
1677 popup.AppendMenuIcon(IDGITLC_VIEWREV, IDS_LOG_POPUP_VIEWREV);
1678 popup.AppendMenuIcon(IDGITLC_OPEN, IDS_REPOBROWSE_OPEN, IDI_OPEN);
1679 popup.AppendMenuIcon(IDGITLC_OPENWITH, IDS_LOG_POPUP_OPENWITH, IDI_OPEN);
1682 if (m_dwContextMenus & GITSLC_POPEXPLORE && !(wcStatus & CTGitPath::LOGACTIONS_DELETED) && m_bHasWC)
1683 popup.AppendMenuIcon(IDGITLC_EXPLORE, IDS_STATUSLIST_CONTEXT_EXPLORE, IDI_EXPLORER);
1685 if (m_dwContextMenus & GITSLC_POPPREPAREDIFF && !(wcStatus & CTGitPath::LOGACTIONS_DELETED))
1687 popup.AppendMenu(MF_SEPARATOR);
1688 popup.AppendMenuIcon(IDGITLC_PREPAREDIFF, IDS_PREPAREDIFF, IDI_DIFF);
1689 if (!m_sMarkForDiffFilename.IsEmpty())
1691 CString diffWith;
1692 if (filepath->GetGitPathString() == m_sMarkForDiffFilename)
1693 diffWith = m_sMarkForDiffVersion;
1694 else
1696 PathCompactPathEx(CStrBuf(diffWith, 2 * GIT_HASH_SIZE), m_sMarkForDiffFilename, 2 * GIT_HASH_SIZE, 0);
1697 diffWith += L':' + m_sMarkForDiffVersion.Left(g_Git.GetShortHASHLength());
1699 CString menuEntry;
1700 menuEntry.Format(IDS_MENUDIFFNOW, (LPCTSTR)diffWith);
1701 popup.AppendMenuIcon(IDGITLC_PREPAREDIFF_COMPARE, menuEntry, IDI_DIFF);
1705 if (selectedCount > 0)
1707 // if (((wcStatus == git_wc_status_unversioned)||(wcStatus == git_wc_status_ignored))&&(m_dwContextMenus & SVNSLC_POPDELETE))
1708 // popup.AppendMenuIcon(IDSVNLC_DELETE, IDS_MENUREMOVE, IDI_DELETE);
1709 // 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))
1710 // {
1711 // if (bShift)
1712 // popup.AppendMenuIcon(IDGitLC_REMOVE, IDS_MENUREMOVEKEEP, IDI_DELETE);
1713 // else
1714 // popup.AppendMenuIcon(IDGitLC_REMOVE, IDS_MENUREMOVE, IDI_DELETE);
1715 // }
1716 if ((wcStatus & (CTGitPath::LOGACTIONS_UNVER | CTGitPath::LOGACTIONS_IGNORE | CTGitPath::LOGACTIONS_MISSING))/*||(wcStatus == git_wc_status_deleted)*/)
1718 if (m_dwContextMenus & GITSLC_POPDELETE)
1719 popup.AppendMenuIcon(IDGITLC_DELETE, IDS_MENUREMOVE, IDI_DELETE);
1721 if ( (wcStatus & CTGitPath::LOGACTIONS_UNVER || wcStatus & CTGitPath::LOGACTIONS_DELETED) )
1723 if (m_dwContextMenus & GITSLC_POPIGNORE)
1725 CTGitPathList ignorelist;
1726 FillListOfSelectedItemPaths(ignorelist);
1727 //check if all selected entries have the same extension
1728 bool bSameExt = true;
1729 CString sExt;
1730 for (int i=0; i<ignorelist.GetCount(); ++i)
1732 if (sExt.IsEmpty() && (i==0))
1733 sExt = ignorelist[i].GetFileExtension();
1734 else if (sExt.CompareNoCase(ignorelist[i].GetFileExtension())!=0)
1735 bSameExt = false;
1737 if (bSameExt)
1739 if (ignoreSubMenu.CreateMenu())
1741 CString ignorepath;
1742 if (ignorelist.GetCount()==1)
1743 ignorepath = ignorelist[0].GetFileOrDirectoryName();
1744 else
1745 ignorepath.Format(IDS_MENUIGNOREMULTIPLE, ignorelist.GetCount());
1746 ignoreSubMenu.AppendMenu(MF_STRING | MF_ENABLED, IDGITLC_IGNORE, ignorepath);
1747 ignorepath = L'*' + sExt;
1748 ignoreSubMenu.AppendMenu(MF_STRING | MF_ENABLED, IDGITLC_IGNOREMASK, ignorepath);
1749 if (ignorelist.GetCount() == 1 && !ignorelist[0].GetContainingDirectory().GetGitPathString().IsEmpty())
1750 ignoreSubMenu.AppendMenu(MF_STRING | MF_ENABLED, IDGITLC_IGNOREFOLDER, ignorelist[0].GetContainingDirectory().GetGitPathString());
1751 CString temp;
1752 temp.LoadString(IDS_MENUIGNORE);
1753 popup.InsertMenu((UINT)-1, MF_BYPOSITION | MF_POPUP, (UINT_PTR)ignoreSubMenu.m_hMenu, temp);
1756 else
1758 CString temp;
1759 if (ignorelist.GetCount()==1)
1760 temp.LoadString(IDS_MENUIGNORE);
1761 else
1762 temp.Format(IDS_MENUIGNOREMULTIPLE, ignorelist.GetCount());
1763 popup.AppendMenuIcon(IDGITLC_IGNORE, temp, IDI_IGNORE);
1764 temp.Format(IDS_MENUIGNOREMULTIPLEMASK, ignorelist.GetCount());
1765 popup.AppendMenuIcon(IDGITLC_IGNOREMASK, temp, IDI_IGNORE);
1773 if (selectedCount > 0)
1775 popup.AppendMenu(MF_SEPARATOR);
1777 if (clipSubMenu.CreatePopupMenu())
1779 CString temp;
1780 clipSubMenu.AppendMenuIcon(IDGITLC_COPYFULL, IDS_STATUSLIST_CONTEXT_COPYFULLPATHS, IDI_COPYCLIP);
1781 clipSubMenu.AppendMenuIcon(IDGITLC_COPYRELPATHS, IDS_STATUSLIST_CONTEXT_COPYRELPATHS, IDI_COPYCLIP);
1782 clipSubMenu.AppendMenuIcon(IDGITLC_COPYFILENAMES, IDS_STATUSLIST_CONTEXT_COPYFILENAMES, IDI_COPYCLIP);
1783 clipSubMenu.AppendMenuIcon(IDGITLC_COPYEXT, IDS_STATUSLIST_CONTEXT_COPYEXT, IDI_COPYCLIP);
1784 if (selSubitem >= 0)
1786 temp.Format(IDS_STATUSLIST_CONTEXT_COPYCOL, (LPCWSTR)m_ColumnManager.GetName(selSubitem));
1787 clipSubMenu.AppendMenuIcon(IDGITLC_COPYCOL, temp, IDI_COPYCLIP);
1789 temp.LoadString(IDS_LOG_POPUP_COPYTOCLIPBOARD);
1790 popup.InsertMenu((UINT)-1, MF_BYPOSITION | MF_POPUP, (UINT_PTR)clipSubMenu.m_hMenu, temp);
1793 #if 0
1794 if ((m_dwContextMenus & SVNSLC_POPCHANGELISTS))
1795 &&(wcStatus != git_wc_status_unversioned)&&(wcStatus != git_wc_status_none))
1797 popup.AppendMenu(MF_SEPARATOR);
1798 // changelist commands
1799 size_t numChangelists = GetNumberOfChangelistsInSelection();
1800 if (numChangelists > 0)
1801 popup.AppendMenuIcon(IDSVNLC_REMOVEFROMCS, IDS_STATUSLIST_CONTEXT_REMOVEFROMCS);
1802 if ((!entry->IsFolder())&&(changelistSubMenu.CreateMenu()))
1804 CString temp;
1805 temp.LoadString(IDS_STATUSLIST_CONTEXT_CREATECS);
1806 changelistSubMenu.AppendMenu(MF_STRING | MF_ENABLED, IDSVNLC_CREATECS, temp);
1808 if (entry->changelist.Compare(SVNSLC_IGNORECHANGELIST))
1810 changelistSubMenu.AppendMenu(MF_SEPARATOR);
1811 changelistSubMenu.AppendMenu(MF_STRING | MF_ENABLED, IDSVNLC_CREATEIGNORECS, SVNSLC_IGNORECHANGELIST);
1814 if (!m_changelists.empty())
1816 // find the changelist names
1817 bool bNeedSeparator = true;
1818 int cmdID = IDSVNLC_MOVETOCS;
1819 for (auto it = m_changelists.cbegin(); it != m_changelists.cend(); ++it)
1821 if ((entry->changelist.Compare(it->first))&&(it->first.Compare(SVNSLC_IGNORECHANGELIST)))
1823 if (bNeedSeparator)
1825 changelistSubMenu.AppendMenu(MF_SEPARATOR);
1826 bNeedSeparator = false;
1828 changelistSubMenu.AppendMenu(MF_STRING | MF_ENABLED, cmdID, it->first);
1829 cmdID++;
1833 temp.LoadString(IDS_STATUSLIST_CONTEXT_MOVETOCS);
1834 popup.AppendMenu(MF_POPUP|MF_STRING, (UINT_PTR)changelistSubMenu.GetSafeHmenu(), temp);
1837 #endif
1840 m_hShellMenu = nullptr;
1841 if (selectedCount > 0 && !(wcStatus & (CTGitPath::LOGACTIONS_DELETED | CTGitPath::LOGACTIONS_MISSING)) && m_bHasWC && (m_CurrentVersion.IsEmpty() || m_CurrentVersion == GIT_REV_ZERO) && shellMenu.CreatePopupMenu())
1843 // insert the shell context menu
1844 popup.AppendMenu(MF_SEPARATOR);
1845 popup.InsertMenu((UINT)-1, MF_BYPOSITION | MF_POPUP, (UINT_PTR)shellMenu.m_hMenu, CString(MAKEINTRESOURCE(IDS_STATUSLIST_CONTEXT_SHELL)));
1846 m_hShellMenu = shellMenu.m_hMenu;
1849 int cmd = popup.TrackPopupMenu(TPM_RETURNCMD | TPM_LEFTALIGN | TPM_RIGHTBUTTON, point.x, point.y, this);
1850 g_IContext2 = nullptr;
1851 g_IContext3 = nullptr;
1852 if (m_pContextMenu)
1854 if (cmd >= SHELL_MIN_CMD && cmd <= SHELL_MAX_CMD) // see if returned idCommand belongs to shell menu entries)
1856 CMINVOKECOMMANDINFOEX cmi = { 0 };
1857 cmi.cbSize = sizeof(CMINVOKECOMMANDINFOEX);
1858 cmi.fMask = CMIC_MASK_UNICODE | CMIC_MASK_PTINVOKE;
1859 if (GetKeyState(VK_CONTROL) < 0)
1860 cmi.fMask |= CMIC_MASK_CONTROL_DOWN;
1861 if (bShift)
1862 cmi.fMask |= CMIC_MASK_SHIFT_DOWN;
1863 cmi.hwnd = m_hWnd;
1864 cmi.lpVerb = MAKEINTRESOURCEA(cmd - SHELL_MIN_CMD);
1865 cmi.lpVerbW = MAKEINTRESOURCEW(cmd - SHELL_MIN_CMD);
1866 cmi.nShow = SW_SHOWNORMAL;
1867 cmi.ptInvoke = point;
1869 m_pContextMenu->InvokeCommand((LPCMINVOKECOMMANDINFO)&cmi);
1871 cmd = 0;
1873 m_pContextMenu->Release();
1874 m_pContextMenu = nullptr;
1876 if (g_pFolderhook)
1878 delete g_pFolderhook;
1879 g_pFolderhook = nullptr;
1881 if (g_psfDesktopFolder)
1883 g_psfDesktopFolder->Release();
1884 g_psfDesktopFolder = nullptr;
1886 for (int i = 0; i < g_pidlArrayItems; i++)
1888 if (g_pidlArray[i])
1889 CoTaskMemFree(g_pidlArray[i]);
1891 if (g_pidlArray)
1892 CoTaskMemFree(g_pidlArray);
1893 g_pidlArray = nullptr;
1894 g_pidlArrayItems = 0;
1896 m_bWaitCursor = true;
1897 bShift = !!(GetAsyncKeyState(VK_SHIFT) & 0x8000);
1898 //int iItemCountBeforeMenuCmd = GetItemCount();
1899 //bool bForce = false;
1900 switch (cmd)
1902 case IDGITLC_VIEWREV:
1903 OpenFile(filepath, ALTERNATIVEEDITOR);
1904 break;
1906 case IDGITLC_OPEN:
1907 OpenFile(filepath,OPEN);
1908 break;
1910 case IDGITLC_OPENWITH:
1911 OpenFile(filepath,OPEN_WITH);
1912 break;
1914 case IDGITLC_EXPLORE:
1915 CAppUtils::ExploreTo(GetSafeHwnd(), g_Git.CombinePath(filepath));
1916 break;
1918 case IDGITLC_PREPAREDIFF:
1919 m_sMarkForDiffFilename = filepath->GetGitPathString();
1920 m_sMarkForDiffVersion = m_CurrentVersion;
1921 break;
1923 case IDGITLC_PREPAREDIFF_COMPARE:
1925 CTGitPath savedFile(m_sMarkForDiffFilename);
1926 CGitDiff::Diff(filepath, &savedFile, m_CurrentVersion, m_sMarkForDiffVersion, false, false, 0, bShift);
1928 break;
1930 case IDGITLC_CREATERESTORE:
1932 POSITION pos = GetFirstSelectedItemPosition();
1933 while (pos)
1935 int index = GetNextSelectedItem(pos);
1936 auto entry2 = GetListEntry(index);
1937 if (!entry2 || entry2->IsDirectory())
1938 continue;
1939 if (m_restorepaths.find(entry2->GetWinPathString()) != m_restorepaths.end())
1940 continue;
1941 CTGitPath tempFile = CTempFiles::Instance().GetTempFilePath(false);
1942 // delete the temp file: the temp file has the FILE_ATTRIBUTE_TEMPORARY flag set
1943 // and copying the real file over it would leave that temp flag.
1944 DeleteFile(tempFile.GetWinPath());
1945 if (CopyFile(g_Git.CombinePath(entry2), tempFile.GetWinPath(), FALSE))
1947 m_restorepaths[entry2->GetWinPathString()] = tempFile.GetWinPathString();
1948 SetItemState(index, INDEXTOOVERLAYMASK(OVL_RESTORE), LVIS_OVERLAYMASK);
1951 Invalidate();
1953 break;
1955 case IDGITLC_RESTOREPATH:
1957 if (CMessageBox::Show(GetParentHWND(), IDS_STATUSLIST_RESTOREPATH, IDS_APPNAME, 2, IDI_QUESTION, IDS_RESTOREBUTTON, IDS_ABORTBUTTON) == 2)
1958 break;
1959 POSITION pos = GetFirstSelectedItemPosition();
1960 while (pos)
1962 int index = GetNextSelectedItem(pos);
1963 auto entry2 = GetListEntry(index);
1964 if (!entry2)
1965 continue;
1966 if (m_restorepaths.find(entry2->GetWinPathString()) == m_restorepaths.end())
1967 continue;
1968 if (CopyFile(m_restorepaths[entry2->GetWinPathString()], g_Git.CombinePath(entry2), FALSE))
1970 m_restorepaths.erase(entry2->GetWinPathString());
1971 SetItemState(index, 0, LVIS_OVERLAYMASK);
1974 Invalidate();
1976 break;
1978 // Compare current version and work copy.
1979 case IDGITLC_COMPAREWC:
1981 if (!CheckMultipleDiffs())
1982 break;
1983 POSITION pos = GetFirstSelectedItemPosition();
1984 while ( pos )
1986 int index = GetNextSelectedItem(pos);
1987 StartDiffWC(index);
1990 break;
1992 // Compare with base version. when current version is zero, compare workcopy and HEAD.
1993 case IDGITLC_COMPARE:
1995 if (!CheckMultipleDiffs())
1996 break;
1997 POSITION pos = GetFirstSelectedItemPosition();
1998 while ( pos )
2000 int index = GetNextSelectedItem(pos);
2001 StartDiff(index);
2004 break;
2006 case IDGITLC_COMPARETWOREVISIONS:
2008 if (!CheckMultipleDiffs())
2009 break;
2010 POSITION pos = GetFirstSelectedItemPosition();
2011 while ( pos )
2013 int index = GetNextSelectedItem(pos);
2014 StartDiffTwo(index);
2017 break;
2019 case IDGITLC_COMPARETWOFILES:
2021 POSITION pos = GetFirstSelectedItemPosition();
2022 if (pos)
2024 auto firstfilepath = GetListEntry(GetNextSelectedItem(pos));
2025 if (!firstfilepath)
2026 break;
2028 auto secondfilepath = GetListEntry(GetNextSelectedItem(pos));
2029 if (!secondfilepath)
2030 break;
2032 CString sCmd;
2033 if (m_CurrentVersion.IsEmpty() || m_CurrentVersion == GIT_REV_ZERO)
2034 sCmd.Format(L"/command:diff /path:\"%s\" /endrev:%s /path2:\"%s\" /startrev:%s /hwnd:%p", firstfilepath->GetWinPath(), firstfilepath->Exists() ? GIT_REV_ZERO : L"HEAD", secondfilepath->GetWinPath(), secondfilepath->Exists() ? GIT_REV_ZERO : L"HEAD", (void*)m_hWnd);
2035 else
2036 sCmd.Format(L"/command:diff /path:\"%s\" /startrev:%s /path2:\"%s\" /endrev:%s /hwnd:%p", firstfilepath->GetWinPath(), firstfilepath->m_Action & CTGitPath::LOGACTIONS_DELETED ? (LPCTSTR)(m_CurrentVersion + L"~1") : (LPCTSTR)m_CurrentVersion, secondfilepath->GetWinPath(), secondfilepath->m_Action & CTGitPath::LOGACTIONS_DELETED ? (LPCTSTR)(m_CurrentVersion + L"~1") : (LPCTSTR)m_CurrentVersion, (void*)m_hWnd);
2037 if (bShift)
2038 sCmd += L" /alternative";
2039 CAppUtils::RunTortoiseGitProc(sCmd);
2042 break;
2044 case IDGITLC_GNUDIFF1:
2046 if (!CheckMultipleDiffs())
2047 break;
2048 POSITION pos = GetFirstSelectedItemPosition();
2049 while (pos)
2051 auto selectedFilepath = GetListEntry(GetNextSelectedItem(pos));
2052 if (m_CurrentVersion.IsEmpty() || m_CurrentVersion == GIT_REV_ZERO)
2054 CString fromwhere;
2055 if (m_amend)
2056 fromwhere = L"~1";
2057 CAppUtils::StartShowUnifiedDiff(m_hWnd, *selectedFilepath, GitRev::GetHead() + fromwhere, *selectedFilepath, GitRev::GetWorkingCopy(), bShift);
2059 else
2061 if ((selectedFilepath->m_ParentNo & (PARENT_MASK | MERGE_MASK)) == 0)
2062 CAppUtils::StartShowUnifiedDiff(m_hWnd, *selectedFilepath, m_CurrentVersion + L"~1", *selectedFilepath, m_CurrentVersion, bShift);
2063 else
2065 CString str;
2066 if (!(selectedFilepath->m_ParentNo & MERGE_MASK))
2067 str.Format(L"%s^%d", (LPCTSTR)m_CurrentVersion, (selectedFilepath->m_ParentNo & PARENT_MASK) + 1);
2069 CAppUtils::StartShowUnifiedDiff(m_hWnd, *selectedFilepath, str, *selectedFilepath, m_CurrentVersion, bShift, false, false, false, !!(selectedFilepath->m_ParentNo & MERGE_MASK));
2074 break;
2076 case IDGITLC_GNUDIFF2REVISIONS:
2078 if (!CheckMultipleDiffs())
2079 break;
2080 POSITION pos = GetFirstSelectedItemPosition();
2081 while (pos)
2083 auto entry = GetListEntry(GetNextSelectedItem(pos));
2084 CAppUtils::StartShowUnifiedDiff(m_hWnd, *entry, m_Rev2, *entry, m_Rev1, bShift);
2087 break;
2089 case IDGITLC_ADD:
2091 CTGitPathList paths;
2092 FillListOfSelectedItemPaths(paths, true);
2094 CGitProgressDlg progDlg;
2095 AddProgressCommand addCommand;
2096 progDlg.SetCommand(&addCommand);
2097 addCommand.SetShowCommitButtonAfterAdd((m_dwContextMenus & GITSLC_POPCOMMIT) != 0);
2098 addCommand.SetPathList(paths);
2099 progDlg.SetItemCount(paths.GetCount());
2100 progDlg.DoModal();
2102 // reset unchecked status
2103 POSITION pos = GetFirstSelectedItemPosition();
2104 int index;
2105 while ((index = GetNextSelectedItem(pos)) >= 0)
2106 m_mapFilenameToChecked.erase(GetListEntry(index)->GetGitPathString());
2108 if (GetLogicalParent() && GetLogicalParent()->GetSafeHwnd())
2109 GetLogicalParent()->SendMessage(GITSLNM_NEEDSREFRESH);
2111 SetRedraw(TRUE);
2113 break;
2115 case IDGITLC_DELETE:
2116 DeleteSelectedFiles();
2117 break;
2119 case IDGITLC_BLAME:
2121 CAppUtils::LaunchTortoiseBlame(g_Git.CombinePath(filepath), m_CurrentVersion);
2123 break;
2125 case IDGITLC_LOG:
2126 case IDGITLC_LOGSUBMODULE:
2128 CString sCmd;
2129 sCmd.Format(L"/command:log /path:\"%s\"", (LPCTSTR)g_Git.CombinePath(filepath));
2130 if (cmd == IDGITLC_LOG && filepath->IsDirectory())
2131 sCmd += L" /submodule";
2132 if (!m_sDisplayedBranch.IsEmpty())
2133 sCmd += L" /range:\"" + m_sDisplayedBranch + L'"';
2134 CAppUtils::RunTortoiseGitProc(sCmd, false, !(cmd == IDGITLC_LOGSUBMODULE));
2136 break;
2138 case IDGITLC_LOGOLDNAME:
2140 CTGitPath oldName(filepath->GetGitOldPathString());
2141 CString sCmd;
2142 sCmd.Format(L"/command:log /path:\"%s\"", (LPCTSTR)g_Git.CombinePath(oldName));
2143 if (!m_sDisplayedBranch.IsEmpty())
2144 sCmd += L" /range:\"" + m_sDisplayedBranch + L'"';
2145 CAppUtils::RunTortoiseGitProc(sCmd);
2147 break;
2149 case IDGITLC_EDITCONFLICT:
2151 if (CAppUtils::ConflictEdit(GetParentHWND(), *filepath, bShift, m_bIsRevertTheirMy, GetLogicalParent() ? GetLogicalParent()->GetSafeHwnd() : nullptr))
2153 CString conflictedFile = g_Git.CombinePath(filepath);
2154 if (!PathFileExists(conflictedFile) && GetLogicalParent() && GetLogicalParent()->GetSafeHwnd())
2156 GetLogicalParent()->SendMessage(GITSLNM_NEEDSREFRESH);
2157 break;
2159 StoreScrollPos();
2160 Show(m_dwShow, 0, m_bShowFolders, 0, true);
2163 break;
2165 case IDGITLC_RESOLVETHEIRS: //follow up
2166 case IDGITLC_RESOLVEMINE: //follow up
2167 case IDGITLC_RESOLVECONFLICT:
2169 if (CMessageBox::Show(GetParentHWND(), IDS_PROC_RESOLVE, IDS_APPNAME, MB_ICONQUESTION | MB_YESNO) == IDYES)
2171 bool needsFullRefresh = false;
2172 POSITION pos = GetFirstSelectedItemPosition();
2173 while (pos != 0)
2175 int index;
2176 index = GetNextSelectedItem(pos);
2177 auto fentry = GetListEntry(index);
2178 if (!fentry)
2179 continue;
2181 CAppUtils::resolve_with resolveWith = CAppUtils::RESOLVE_WITH_CURRENT;
2182 if (((!this->m_bIsRevertTheirMy) && cmd == IDGITLC_RESOLVETHEIRS) || ((this->m_bIsRevertTheirMy) && cmd == IDGITLC_RESOLVEMINE))
2183 resolveWith = CAppUtils::RESOLVE_WITH_THEIRS;
2184 else if (((!this->m_bIsRevertTheirMy) && cmd == IDGITLC_RESOLVEMINE) || ((this->m_bIsRevertTheirMy) && cmd == IDGITLC_RESOLVETHEIRS))
2185 resolveWith = CAppUtils::RESOLVE_WITH_MINE;
2186 if (CAppUtils::ResolveConflict(GetParentHWND(), *fentry, resolveWith) == 0 && fentry->m_Action & CTGitPath::LOGACTIONS_UNMERGED)
2187 needsFullRefresh = true;
2189 if (needsFullRefresh && CRegDWORD(L"Software\\TortoiseGit\\RefreshFileListAfterResolvingConflict", TRUE) == TRUE)
2191 CWnd* pParent = GetLogicalParent();
2192 if (pParent && pParent->GetSafeHwnd())
2193 pParent->SendMessage(GITSLNM_NEEDSREFRESH);
2194 SetRedraw(TRUE);
2195 break;
2197 StoreScrollPos();
2198 Show(m_dwShow, 0, m_bShowFolders,0,true);
2201 break;
2203 case IDGITLC_IGNORE:
2205 CTGitPathList ignorelist;
2206 //std::vector<CString> toremove;
2207 FillListOfSelectedItemPaths(ignorelist, true);
2209 if (!CAppUtils::IgnoreFile(GetParentHWND(), ignorelist, false))
2210 break;
2212 SetRedraw(FALSE);
2213 CWnd* pParent = GetLogicalParent();
2214 if (pParent && pParent->GetSafeHwnd())
2216 pParent->SendMessage(GITSLNM_NEEDSREFRESH);
2218 SetRedraw(TRUE);
2220 break;
2222 case IDGITLC_IGNOREMASK:
2224 CString common;
2225 CString ext=filepath->GetFileExtension();
2226 CTGitPathList ignorelist;
2227 FillListOfSelectedItemPaths(ignorelist, true);
2229 if (!CAppUtils::IgnoreFile(GetParentHWND(), ignorelist, true))
2230 break;
2232 SetRedraw(FALSE);
2233 CWnd* pParent = GetLogicalParent();
2234 if (pParent && pParent->GetSafeHwnd())
2236 pParent->SendMessage(GITSLNM_NEEDSREFRESH);
2239 SetRedraw(TRUE);
2241 break;
2243 case IDGITLC_IGNOREFOLDER:
2245 CTGitPathList ignorelist;
2246 ignorelist.AddPath(filepath->GetContainingDirectory());
2248 if (!CAppUtils::IgnoreFile(GetParentHWND(), ignorelist, false))
2249 break;
2251 SetRedraw(FALSE);
2252 CWnd *pParent = GetLogicalParent();
2253 if (pParent && pParent->GetSafeHwnd())
2254 pParent->SendMessage(GITSLNM_NEEDSREFRESH);
2256 SetRedraw(TRUE);
2258 break;
2259 case IDGITLC_COMMIT:
2261 CTGitPathList targetList;
2262 FillListOfSelectedItemPaths(targetList);
2263 CTGitPath tempFile = CTempFiles::Instance().GetTempFilePath(false);
2264 VERIFY(targetList.WriteToFile(tempFile.GetWinPathString()));
2265 CString commandline = L"/command:commit /pathfile:\"";
2266 commandline += tempFile.GetWinPathString();
2267 commandline += L'"';
2268 commandline += L" /deletepathfile";
2269 CAppUtils::RunTortoiseGitProc(commandline);
2271 break;
2272 case IDGITLC_REVERT:
2274 // If at least one item is not in the status "added"
2275 // we ask for a confirmation
2276 BOOL bConfirm = FALSE;
2277 POSITION pos = GetFirstSelectedItemPosition();
2278 int index;
2279 while ((index = GetNextSelectedItem(pos)) >= 0)
2281 auto fentry = GetListEntry(index);
2282 if(fentry && fentry->m_Action &CTGitPath::LOGACTIONS_MODIFIED && !fentry->IsDirectory())
2284 bConfirm = TRUE;
2285 break;
2289 CString str;
2290 str.Format(IDS_PROC_WARNREVERT, selectedCount);
2292 if (!bConfirm || MessageBox(str, L"TortoiseGit", MB_YESNO | MB_ICONQUESTION) == IDYES)
2294 CTGitPathList targetList;
2295 FillListOfSelectedItemPaths(targetList);
2297 // make sure that the list is reverse sorted, so that
2298 // children are removed before any parents
2299 targetList.SortByPathname(true);
2301 // put all reverted files in the trashbin, except the ones with 'added'
2302 // status because they are not restored by the revert.
2303 CTGitPathList delList;
2304 POSITION pos2 = GetFirstSelectedItemPosition();
2305 int index2;
2306 while ((index2 = GetNextSelectedItem(pos2)) >= 0)
2308 auto entry = GetListEntry(index2);
2309 if (entry&&(!(entry->m_Action& CTGitPath::LOGACTIONS_ADDED))
2310 && (!(entry->m_Action& CTGitPath::LOGACTIONS_REPLACED)) && !entry->IsDirectory())
2312 CTGitPath fullpath;
2313 fullpath.SetFromWin(g_Git.CombinePath(entry));
2314 delList.AddPath(fullpath);
2317 if (DWORD(CRegDWORD(L"Software\\TortoiseGit\\RevertWithRecycleBin", TRUE)))
2318 delList.DeleteAllFiles(true);
2320 CString revertToCommit = L"HEAD";
2321 if (m_amend)
2322 revertToCommit = L"HEAD~1";
2323 CString err;
2324 if (g_Git.Revert(revertToCommit, targetList, err))
2325 MessageBox(L"Revert failed:\n" + err, L"TortoiseGit", MB_ICONERROR);
2326 else
2328 bool updateStatusList = false;
2329 for (int i = 0 ; i < targetList.GetCount(); ++i)
2331 int nListboxEntries = GetItemCount();
2332 for (int nItem=0; nItem<nListboxEntries; ++nItem)
2334 auto path = GetListEntry(nItem);
2335 if (path->GetGitPathString()==targetList[i].GetGitPathString() && !path->IsDirectory())
2337 if(path->m_Action & CTGitPath::LOGACTIONS_ADDED)
2339 path->m_Action = CTGitPath::LOGACTIONS_UNVER;
2340 SetEntryCheck(path,nItem,false);
2341 updateStatusList = true;
2342 #if 0 // revert an added file and some entry will be cloned (part 1/2)
2343 SetItemGroup(nItem,1);
2344 this->m_StatusFileList.RemoveItem(*path);
2345 this->m_UnRevFileList.AddPath(*path);
2346 //this->m_IgnoreFileList.RemoveItem(*path);
2347 #endif
2349 else
2351 if (GetCheck(nItem))
2352 m_nSelected--;
2353 RemoveListEntry(nItem);
2355 break;
2357 else if (path->GetGitPathString()==targetList[i].GetGitPathString() && path->IsDirectory() && path->IsWCRoot())
2359 CString sCmd;
2360 sCmd.Format(L"/command:revert /path:\"%s\"", (LPCTSTR)path->GetGitPathString());
2361 CCommonAppUtils::RunTortoiseGitProc(sCmd);
2365 SetRedraw(TRUE);
2366 #if 0 // revert an added file and some entry will be cloned (part 2/2)
2367 Show(m_dwShow, 0, m_bShowFolders,updateStatusList,true);
2368 NotifyCheck();
2369 #else
2370 if (updateStatusList && GetLogicalParent() && GetLogicalParent()->GetSafeHwnd())
2371 GetLogicalParent()->SendMessage(GITSLNM_NEEDSREFRESH);
2372 #endif
2376 break;
2378 case IDGITLC_ASSUMEVALID:
2379 SetGitIndexFlagsForSelectedFiles(IDS_PROC_MARK_ASSUMEVALID, BST_CHECKED, BST_INDETERMINATE);
2380 break;
2381 case IDGITLC_SKIPWORKTREE:
2382 SetGitIndexFlagsForSelectedFiles(IDS_PROC_MARK_SKIPWORKTREE, BST_INDETERMINATE, BST_CHECKED);
2383 break;
2384 case IDGITLC_UNSETIGNORELOCALCHANGES:
2385 SetGitIndexFlagsForSelectedFiles(IDS_PROC_UNSET_IGNORELOCALCHANGES, BST_UNCHECKED, BST_UNCHECKED);
2386 break;
2387 case IDGITLC_COPYFULL:
2388 case IDGITLC_COPYRELPATHS:
2389 case IDGITLC_COPYFILENAMES:
2390 CopySelectedEntriesToClipboard(GITSLC_COLFILENAME, cmd);
2391 break;
2392 case IDGITLC_COPYEXT:
2393 CopySelectedEntriesToClipboard((DWORD)-1, 0);
2394 break;
2395 case IDGITLC_COPYCOL:
2396 CopySelectedEntriesToClipboard((DWORD)1 << selSubitem, 0);
2397 break;
2398 case IDGITLC_EXPORT:
2399 FilesExport();
2400 break;
2401 case IDGITLC_SAVEAS:
2402 FileSaveAs(filepath);
2403 break;
2405 case IDGITLC_REVERTTOREV:
2406 RevertSelectedItemToVersion();
2407 break;
2408 case IDGITLC_REVERTTOPARENT:
2409 RevertSelectedItemToVersion(true);
2410 break;
2411 #if 0
2412 case IDSVNLC_COMMIT:
2414 CTSVNPathList targetList;
2415 FillListOfSelectedItemPaths(targetList);
2416 CTSVNPath tempFile = CTempFiles::Instance().GetTempFilePath(false);
2417 VERIFY(targetList.WriteToFile(tempFile.GetWinPathString()));
2418 CString commandline = CPathUtils::GetAppDirectory();
2419 commandline += L"TortoiseGitProc.exe /command:commit /pathfile:\"";
2420 commandline += tempFile.GetWinPathString();
2421 commandline += L'"';
2422 commandline += L" /deletepathfile";
2423 CAppUtils::LaunchApplication(commandline, nullptr, false);
2425 break;
2426 case IDSVNLC_CREATEIGNORECS:
2427 CreateChangeList(SVNSLC_IGNORECHANGELIST);
2428 break;
2429 case IDSVNLC_CREATECS:
2431 CCreateChangelistDlg dlg;
2432 if (dlg.DoModal() == IDOK)
2433 CreateChangeList(dlg.m_sName);
2435 break;
2436 default:
2438 if (cmd < IDSVNLC_MOVETOCS)
2439 break;
2440 CTSVNPathList changelistItems;
2441 FillListOfSelectedItemPaths(changelistItems);
2443 // find the changelist name
2444 CString sChangelist;
2445 int cmdID = IDSVNLC_MOVETOCS;
2446 SetRedraw(FALSE);
2447 for (auto it = m_changelists.cbegin(); it != m_changelists.cend(); ++it)
2449 if ((it->first.Compare(SVNSLC_IGNORECHANGELIST))&&(entry->changelist.Compare(it->first)))
2451 if (cmd == cmdID)
2452 sChangelist = it->first;
2453 cmdID++;
2456 if (!sChangelist.IsEmpty())
2458 SVN git;
2459 if (git.AddToChangeList(changelistItems, sChangelist, git_depth_empty))
2461 // The changelists were moved, but we now need to run through the selected items again
2462 // and update their changelist
2463 POSITION pos = GetFirstSelectedItemPosition();
2464 int index;
2465 while ((index = GetNextSelectedItem(pos)) >= 0)
2467 FileEntry * e = GetListEntry(index);
2468 e->changelist = sChangelist;
2469 if (!e->IsFolder())
2471 if (m_changelists.find(e->changelist)!=m_changelists.end())
2472 SetItemGroup(index, m_changelists[e->changelist]);
2473 else
2474 SetItemGroup(index, 0);
2478 else
2479 MessageBox(git.GetLastErrorMessage(), L"TortoiseGit", MB_ICONERROR);
2481 SetRedraw(TRUE);
2483 break;
2484 #endif
2486 } // switch (cmd)
2487 m_bWaitCursor = false;
2488 GetStatisticsString();
2489 //int iItemCountAfterMenuCmd = GetItemCount();
2490 //if (iItemCountAfterMenuCmd != iItemCountBeforeMenuCmd)
2492 // CWnd* pParent = GetParent();
2493 // if (pParent && pParent->GetSafeHwnd())
2494 // {
2495 // pParent->SendMessage(SVNSLNM_ITEMCOUNTCHANGED);
2496 // }
2498 } // if (popup.CreatePopupMenu())
2499 } // if (selIndex >= 0)
2502 void CGitStatusListCtrl::SetGitIndexFlagsForSelectedFiles(UINT message, BOOL assumevalid, BOOL skipworktree)
2504 if (CMessageBox::Show(GetParentHWND(), message, IDS_APPNAME, MB_YESNO | MB_DEFBUTTON2 | MB_ICONQUESTION) != IDYES)
2505 return;
2507 CAutoReadLock locker(m_guard);
2509 CAutoRepository repository(g_Git.GetGitRepository());
2510 if (!repository)
2512 MessageBox(g_Git.GetLibGit2LastErr(), L"TortoiseGit", MB_ICONERROR);
2513 return;
2516 CAutoIndex gitindex;
2517 if (git_repository_index(gitindex.GetPointer(), repository))
2519 MessageBox(g_Git.GetLibGit2LastErr(), L"TortoiseGit", MB_ICONERROR);
2520 return;
2523 POSITION pos = GetFirstSelectedItemPosition();
2524 int index = -1;
2525 while ((index = GetNextSelectedItem(pos)) >= 0)
2527 auto path = GetListEntry(index);
2528 if (path == nullptr)
2529 continue;
2531 size_t idx;
2532 if (!git_index_find(&idx, gitindex, CUnicodeUtils::GetMulti(path->GetGitPathString(), CP_UTF8)))
2534 git_index_entry *e = const_cast<git_index_entry *>(git_index_get_byindex(gitindex, idx)); // HACK
2535 if (assumevalid == BST_UNCHECKED)
2536 e->flags &= ~GIT_IDXENTRY_VALID;
2537 else if (assumevalid == BST_CHECKED)
2538 e->flags |= GIT_IDXENTRY_VALID;
2539 if (skipworktree == BST_UNCHECKED)
2540 e->flags_extended &= ~GIT_IDXENTRY_SKIP_WORKTREE;
2541 else if (skipworktree == BST_CHECKED)
2542 e->flags_extended |= GIT_IDXENTRY_SKIP_WORKTREE;
2543 git_index_add(gitindex, e);
2545 else
2546 MessageBox(g_Git.GetLibGit2LastErr(), L"TortoiseGit", MB_ICONERROR);
2549 if (git_index_write(gitindex))
2551 MessageBox(g_Git.GetLibGit2LastErr(), L"TortoiseGit", MB_ICONERROR);
2552 return;
2555 if (nullptr != GetLogicalParent() && nullptr != GetLogicalParent()->GetSafeHwnd())
2556 GetLogicalParent()->SendMessage(GITSLNM_NEEDSREFRESH);
2558 SetRedraw(TRUE);
2561 void CGitStatusListCtrl::OnContextMenu(CWnd* pWnd, CPoint point)
2563 __super::OnContextMenu(pWnd, point);
2564 if (pWnd == this)
2565 OnContextMenuList(pWnd, point);
2568 void CGitStatusListCtrl::OnNMDblclk(NMHDR *pNMHDR, LRESULT *pResult)
2570 LPNMLISTVIEW pNMLV = reinterpret_cast<LPNMLISTVIEW>(pNMHDR);
2571 *pResult = 0;
2573 CAutoReadWeakLock readLock(m_guard);
2574 if (!readLock.IsAcquired())
2575 return;
2577 if (pNMLV->iItem < 0)
2578 return;
2580 auto file = GetListEntry(pNMLV->iItem);
2582 if (file->m_Action & (CTGitPath::LOGACTIONS_UNVER | CTGitPath::LOGACTIONS_IGNORE)) {
2583 StartDiffWC(pNMLV->iItem);
2584 return;
2586 if( file->m_Action&CTGitPath::LOGACTIONS_UNMERGED )
2588 if (CAppUtils::ConflictEdit(GetParentHWND(), *file, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000), m_bIsRevertTheirMy, GetLogicalParent() ? GetLogicalParent()->GetSafeHwnd() : nullptr))
2590 CString conflictedFile = g_Git.CombinePath(file);
2591 if (!PathFileExists(conflictedFile) && GetLogicalParent() && GetLogicalParent()->GetSafeHwnd())
2593 GetLogicalParent()->SendMessage(GITSLNM_NEEDSREFRESH);
2594 return;
2596 StoreScrollPos();
2597 Show(m_dwShow, 0, m_bShowFolders, 0, true);
2600 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))
2601 return;
2602 else
2604 if (!m_Rev1.IsEmpty() && !m_Rev2.IsEmpty())
2605 StartDiffTwo(pNMLV->iItem);
2606 else
2607 StartDiff(pNMLV->iItem);
2610 void CGitStatusListCtrl::StartDiffTwo(int fileindex)
2612 if(fileindex<0)
2613 return;
2615 CAutoReadLock locker(m_guard);
2616 auto ptr = GetListEntry(fileindex);
2617 if (!ptr)
2618 return;
2619 CTGitPath file1 = *ptr;
2621 if (file1.m_Action & CTGitPath::LOGACTIONS_ADDED)
2622 CGitDiff::DiffNull(&file1, m_Rev1, true, 0, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000));
2623 else if (file1.m_Action & CTGitPath::LOGACTIONS_DELETED)
2624 CGitDiff::DiffNull(&file1, m_Rev2, false, 0, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000));
2625 else
2626 CGitDiff::Diff(&file1, &file1, m_Rev1, m_Rev2, false, false, 0, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000));
2629 void CGitStatusListCtrl::StartDiffWC(int fileindex)
2631 if(fileindex<0)
2632 return;
2634 CAutoReadLock locker(m_guard);
2635 if (m_CurrentVersion.IsEmpty())
2636 m_CurrentVersion == GIT_REV_ZERO;
2638 auto ptr = GetListEntry(fileindex);
2639 if (!ptr)
2640 return;
2641 CTGitPath file1 = *ptr;
2642 file1.m_Action = 0; // reset action, so that diff is not started as added/deleted file; see issue #1757
2644 CGitDiff::Diff(&file1, &file1, GIT_REV_ZERO, m_CurrentVersion, false, false, 0, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000));
2647 void CGitStatusListCtrl::StartDiff(int fileindex)
2649 if(fileindex<0)
2650 return;
2652 CAutoReadLock locker(m_guard);
2653 auto ptr = GetListEntry(fileindex);
2654 if (!ptr)
2655 return;
2656 CTGitPath file1 = *ptr;
2657 CTGitPath file2;
2658 if(file1.m_Action & (CTGitPath::LOGACTIONS_REPLACED|CTGitPath::LOGACTIONS_COPY))
2659 file2.SetFromGit(file1.GetGitOldPathString());
2660 else
2661 file2=file1;
2663 if(this->m_CurrentVersion.IsEmpty() || m_CurrentVersion== GIT_REV_ZERO)
2665 CString fromwhere;
2666 if(m_amend && (file1.m_Action & CTGitPath::LOGACTIONS_ADDED) == 0)
2667 fromwhere = L"~1";
2668 if( g_Git.IsInitRepos())
2669 CGitDiff::DiffNull(GetListEntry(fileindex),
2670 GIT_REV_ZERO, true, 0, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000));
2671 else if( file1.m_Action&CTGitPath::LOGACTIONS_ADDED )
2672 CGitDiff::DiffNull(GetListEntry(fileindex),
2673 m_CurrentVersion + fromwhere, true, 0, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000));
2674 else if( file1.m_Action&CTGitPath::LOGACTIONS_DELETED )
2675 CGitDiff::DiffNull(GetListEntry(fileindex),
2676 GitRev::GetHead() + fromwhere, false, 0, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000));
2677 else
2678 CGitDiff::Diff(&file1,&file2,
2679 CString(GIT_REV_ZERO),
2680 GitRev::GetHead() + fromwhere, false, false, 0, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000));
2682 else
2684 CGitHash hash;
2685 CString fromwhere = m_CurrentVersion + L"~1";
2686 if(m_amend)
2687 fromwhere = m_CurrentVersion + L"~2";
2688 bool revfail = !!g_Git.GetHash(hash, fromwhere);
2689 if (revfail || (file1.m_Action & file1.LOGACTIONS_ADDED))
2690 CGitDiff::DiffNull(&file1, m_CurrentVersion, true, 0, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000));
2691 else if (file1.m_Action & file1.LOGACTIONS_DELETED)
2693 if (file1.m_ParentNo > 0)
2694 fromwhere.Format(L"%s^%d", (LPCTSTR)m_CurrentVersion, file1.m_ParentNo + 1);
2696 CGitDiff::DiffNull(&file1, fromwhere, false, 0, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000));
2698 else
2700 if( file1.m_ParentNo & MERGE_MASK)
2702 CTGitPath base, theirs, mine, merge;
2704 CString temppath;
2705 GetTempPath(temppath);
2706 temppath.TrimRight(L'\\');
2708 mine.SetFromGit(temppath + L'\\' + file1.GetFileOrDirectoryName() + L".LOCAL" + file1.GetFileExtension());
2709 theirs.SetFromGit(temppath + L'\\' + file1.GetFileOrDirectoryName() + L".REMOTE" + file1.GetFileExtension());
2710 base.SetFromGit(temppath + L'\\' + file1.GetFileOrDirectoryName() + L".BASE" + file1.GetFileExtension());
2712 CFile tempfile;
2713 //create a empty file, incase stage is not three
2714 tempfile.Open(mine.GetWinPathString(),CFile::modeCreate|CFile::modeReadWrite);
2715 tempfile.Close();
2716 tempfile.Open(theirs.GetWinPathString(),CFile::modeCreate|CFile::modeReadWrite);
2717 tempfile.Close();
2718 tempfile.Open(base.GetWinPathString(),CFile::modeCreate|CFile::modeReadWrite);
2719 tempfile.Close();
2721 merge.SetFromGit(temppath + L'\\' + file1.GetFileOrDirectoryName() + L".Merged" + file1.GetFileExtension());
2723 int parent1=-1, parent2 =-1;
2724 for (size_t i = 0; i < m_arStatusArray.size(); ++i)
2726 if(m_arStatusArray[i]->GetGitPathString() == file1.GetGitPathString())
2728 if(m_arStatusArray[i]->m_ParentNo & MERGE_MASK)
2731 else
2733 if(parent1<0)
2734 parent1 = m_arStatusArray[i]->m_ParentNo & PARENT_MASK;
2735 else if (parent2 <0)
2736 parent2 = m_arStatusArray[i]->m_ParentNo & PARENT_MASK;
2741 if(g_Git.GetOneFile(m_CurrentVersion, file1, (CString&)merge.GetWinPathString()))
2742 CMessageBox::Show(GetParentHWND(), IDS_STATUSLIST_FAILEDGETMERGEFILE, IDS_APPNAME, MB_OK | MB_ICONERROR);
2744 if(parent1>=0)
2746 CString str;
2747 str.Format(L"%s^%d", (LPCTSTR)this->m_CurrentVersion, parent1 + 1);
2749 if(g_Git.GetOneFile(str, file1, (CString&)mine.GetWinPathString()))
2750 CMessageBox::Show(GetParentHWND(), IDS_STATUSLIST_FAILEDGETMERGEFILE, IDS_APPNAME, MB_OK | MB_ICONERROR);
2753 if(parent2>=0)
2755 CString str;
2756 str.Format(L"%s^%d", (LPCTSTR)this->m_CurrentVersion, parent2 + 1);
2758 if(g_Git.GetOneFile(str, file1, (CString&)theirs.GetWinPathString()))
2759 CMessageBox::Show(GetParentHWND(), IDS_STATUSLIST_FAILEDGETMERGEFILE, IDS_APPNAME, MB_OK | MB_ICONERROR);
2762 if(parent1>=0 && parent2>=0)
2764 CString cmd, output;
2765 cmd.Format(L"git.exe merge-base %s^%d %s^%d", (LPCTSTR)this->m_CurrentVersion, parent1 + 1,
2766 (LPCTSTR)m_CurrentVersion, parent2 + 1);
2768 if (!g_Git.Run(cmd, &output, nullptr, CP_UTF8))
2770 if (g_Git.GetOneFile(output.Left(2 * GIT_HASH_SIZE), file1, (CString&)base.GetWinPathString()))
2771 CMessageBox::Show(GetParentHWND(), IDS_STATUSLIST_FAILEDGETBASEFILE, IDS_APPNAME, MB_OK | MB_ICONERROR);
2774 CAppUtils::StartExtMerge(!!(GetAsyncKeyState(VK_SHIFT) & 0x8000), base, theirs, mine, merge, L"BASE", L"REMOTE", L"LOCAL");
2776 else
2778 CString str;
2779 if( (file1.m_ParentNo&PARENT_MASK) == 0)
2780 str = L"~1";
2781 else
2782 str.Format(L"^%d", (file1.m_ParentNo & PARENT_MASK) + 1);
2783 CGitDiff::Diff(&file1,&file2,
2784 m_CurrentVersion,
2785 m_CurrentVersion + str, false, false, 0, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000));
2791 CString CGitStatusListCtrl::GetStatisticsString(bool simple)
2793 CString sNormal = CString(MAKEINTRESOURCE(IDS_STATUSNORMAL));
2794 CString sAdded = CString(MAKEINTRESOURCE(IDS_STATUSADDED));
2795 CString sDeleted = CString(MAKEINTRESOURCE(IDS_STATUSDELETED));
2796 CString sModified = CString(MAKEINTRESOURCE(IDS_STATUSMODIFIED));
2797 CString sConflicted = CString(MAKEINTRESOURCE(IDS_STATUSCONFLICTED));
2798 CString sUnversioned = CString(MAKEINTRESOURCE(IDS_STATUSUNVERSIONED));
2799 CString sRenamed = CString(MAKEINTRESOURCE(IDS_STATUSREPLACED));
2800 CString sToolTip;
2801 if(simple)
2803 sToolTip.Format(IDS_STATUSLIST_STATUSLINE1,
2804 this->m_nLineAdded,this->m_nLineDeleted,
2805 (LPCTSTR)sModified, m_nModified,
2806 (LPCTSTR)sAdded, m_nAdded,
2807 (LPCTSTR)sDeleted, m_nDeleted,
2808 (LPCTSTR)sRenamed, m_nRenamed
2811 else
2813 sToolTip.Format(IDS_STATUSLIST_STATUSLINE2,
2814 this->m_nLineAdded,this->m_nLineDeleted,
2815 (LPCTSTR)sNormal, m_nNormal,
2816 (LPCTSTR)sUnversioned, m_nUnversioned,
2817 (LPCTSTR)sModified, m_nModified,
2818 (LPCTSTR)sAdded, m_nAdded,
2819 (LPCTSTR)sDeleted, m_nDeleted,
2820 (LPCTSTR)sConflicted, m_nConflicted
2823 CString sStats;
2824 sStats.Format(IDS_COMMITDLG_STATISTICSFORMAT, m_nSelected, GetItemCount());
2825 if (m_pStatLabel)
2826 m_pStatLabel->SetWindowText(sStats);
2828 if (m_pSelectButton)
2830 if (m_nSelected == 0)
2831 m_pSelectButton->SetCheck(BST_UNCHECKED);
2832 else if (m_nSelected != GetItemCount())
2833 m_pSelectButton->SetCheck(BST_INDETERMINATE);
2834 else
2835 m_pSelectButton->SetCheck(BST_CHECKED);
2838 if (m_pConfirmButton)
2839 m_pConfirmButton->EnableWindow(m_nSelected>0);
2841 return sToolTip;
2844 CString CGitStatusListCtrl::GetCommonDirectory(bool bStrict)
2846 CAutoReadLock locker(m_guard);
2847 if (!bStrict)
2849 // not strict means that the selected folder has priority
2850 if (!m_StatusFileList.GetCommonDirectory().IsEmpty())
2851 return m_StatusFileList.GetCommonDirectory().GetWinPath();
2854 CTGitPathList list;
2855 int nListItems = GetItemCount();
2856 for (int i=0; i<nListItems; ++i)
2858 auto* entry = GetListEntry(i);
2859 if (entry->IsEmpty())
2860 continue;
2861 list.AddPath(*entry);
2863 return list.GetCommonRoot().GetWinPath();
2866 void CGitStatusListCtrl::SelectAll(bool bSelect, bool /*bIncludeNoCommits*/)
2868 CWaitCursor waitCursor;
2869 // block here so the LVN_ITEMCHANGED messages
2870 // get ignored
2871 ScopedInDecrement blocker(m_nBlockItemChangeHandler);
2874 CAutoWriteLock locker(m_guard);
2875 SetRedraw(FALSE);
2877 int nListItems = GetItemCount();
2878 if (bSelect)
2879 m_nSelected = nListItems;
2880 else
2881 m_nSelected = 0;
2883 for (int i=0; i<nListItems; ++i)
2885 auto path = GetListEntry(i);
2886 if (!path)
2887 continue;
2888 //if ((bIncludeNoCommits)||(entry->GetChangeList().Compare(SVNSLC_IGNORECHANGELIST)))
2889 SetEntryCheck(path,i,bSelect);
2892 SetRedraw(TRUE);
2893 GetStatisticsString();
2894 NotifyCheck();
2897 void CGitStatusListCtrl::Check(DWORD dwCheck, bool check)
2899 CWaitCursor waitCursor;
2900 // block here so the LVN_ITEMCHANGED messages
2901 // get ignored
2903 CAutoWriteLock locker(m_guard);
2904 SetRedraw(FALSE);
2905 ScopedInDecrement blocker(m_nBlockItemChangeHandler);
2907 int nListItems = GetItemCount();
2909 for (int i = 0; i < nListItems; ++i)
2911 auto entry = GetListEntry(i);
2912 if (!entry)
2913 continue;
2915 DWORD showFlags = entry->m_Action;
2916 if (entry->IsDirectory())
2917 showFlags |= GITSLC_SHOWSUBMODULES;
2918 else
2919 showFlags |= GITSLC_SHOWFILES;
2921 if (check && (showFlags & dwCheck) && !GetCheck(i) && !(entry->IsDirectory() && m_bDoNotAutoselectSubmodules && !(dwCheck & GITSLC_SHOWSUBMODULES)))
2923 SetEntryCheck(entry, i, true);
2924 m_nSelected++;
2926 else if (!check && (showFlags & dwCheck) && GetCheck(i))
2928 SetEntryCheck(entry, i, false);
2929 m_nSelected--;
2933 SetRedraw(TRUE);
2934 GetStatisticsString();
2935 NotifyCheck();
2938 void CGitStatusListCtrl::OnLvnGetInfoTip(NMHDR *pNMHDR, LRESULT *pResult)
2940 LPNMLVGETINFOTIP pGetInfoTip = reinterpret_cast<LPNMLVGETINFOTIP>(pNMHDR);
2941 *pResult = 0;
2942 if (CRegDWORD(L"Software\\TortoiseGit\\ShowListFullPathTooltip", TRUE) != TRUE)
2943 return;
2945 CAutoReadWeakLock readLock(m_guard);
2946 if (!readLock.IsAcquired())
2947 return;
2949 auto entry = GetListEntry(pGetInfoTip->iItem);
2951 if (entry)
2952 if (pGetInfoTip->cchTextMax > entry->GetGitPathString().GetLength() + g_Git.m_CurrentDir.GetLength())
2954 CString str = g_Git.CombinePath(entry->GetWinPathString());
2955 wcsncpy_s(pGetInfoTip->pszText, pGetInfoTip->cchTextMax, str.GetBuffer(), pGetInfoTip->cchTextMax - 1);
2959 void CGitStatusListCtrl::OnNMCustomdraw(NMHDR *pNMHDR, LRESULT *pResult)
2961 NMLVCUSTOMDRAW* pLVCD = reinterpret_cast<NMLVCUSTOMDRAW*>( pNMHDR );
2963 // Take the default processing unless we set this to something else below.
2964 *pResult = CDRF_DODEFAULT;
2966 // First thing - check the draw stage. If it's the control's prepaint
2967 // stage, then tell Windows we want messages for every item.
2969 switch (pLVCD->nmcd.dwDrawStage)
2971 case CDDS_PREPAINT:
2972 *pResult = CDRF_NOTIFYITEMDRAW;
2973 break;
2974 case CDDS_ITEMPREPAINT:
2976 // This is the prepaint stage for an item. Here's where we set the
2977 // item's text color. Our return value will tell Windows to draw the
2978 // item itself, but it will use the new color we set here.
2980 // Tell Windows to paint the control itself.
2981 *pResult = CDRF_DODEFAULT;
2982 CAutoReadWeakLock readLock(m_guard, 0);
2983 if (!readLock.IsAcquired())
2984 return;
2986 COLORREF crText = GetSysColor(COLOR_WINDOWTEXT);
2988 if (m_arStatusArray.size() > (DWORD_PTR)pLVCD->nmcd.dwItemSpec)
2990 auto entry = GetListEntry((int)pLVCD->nmcd.dwItemSpec);
2991 if (!entry)
2992 return;
2994 // coloring
2995 // ========
2996 // black : unversioned, normal
2997 // purple : added
2998 // blue : modified
2999 // brown : missing, deleted, replaced
3000 // green : merged (or potential merges)
3001 // red : conflicts or sure conflicts
3002 if(entry->m_Action & CTGitPath::LOGACTIONS_GRAY)
3003 crText = RGB(128,128,128);
3004 else if(entry->m_Action & CTGitPath::LOGACTIONS_UNMERGED)
3005 crText = m_Colors.GetColor(CColors::Conflict);
3006 else if(entry->m_Action & (CTGitPath::LOGACTIONS_MODIFIED))
3007 crText = m_Colors.GetColor(CColors::Modified);
3008 else if(entry->m_Action & (CTGitPath::LOGACTIONS_ADDED|CTGitPath::LOGACTIONS_COPY))
3009 crText = m_Colors.GetColor(CColors::Added);
3010 else if(entry->m_Action & CTGitPath::LOGACTIONS_DELETED)
3011 crText = m_Colors.GetColor(CColors::Deleted);
3012 else if(entry->m_Action & CTGitPath::LOGACTIONS_REPLACED)
3013 crText = m_Colors.GetColor(CColors::Renamed);
3014 else if(entry->m_Action & CTGitPath::LOGACTIONS_MERGED)
3015 crText = m_Colors.GetColor(CColors::Merged);
3016 else
3017 crText = GetSysColor(COLOR_WINDOWTEXT);
3018 // Store the color back in the NMLVCUSTOMDRAW struct.
3019 pLVCD->clrText = crText;
3022 break;
3026 void CGitStatusListCtrl::OnLvnGetdispinfo(NMHDR* pNMHDR, LRESULT* pResult)
3028 auto pDispInfo = reinterpret_cast<NMLVDISPINFO*>(pNMHDR);
3029 *pResult = 0;
3031 // Create a pointer to the item
3032 LV_ITEM* pItem = &(pDispInfo)->item;
3034 CAutoReadWeakLock readLock(m_guard, 0);
3035 if (readLock.IsAcquired())
3037 if (pItem->mask & LVIF_TEXT)
3039 CString text = GetCellText(pItem->iItem, pItem->iSubItem);
3040 lstrcpyn(pItem->pszText, text, pItem->cchTextMax - 1);
3043 else
3044 pItem->mask = 0;
3047 BOOL CGitStatusListCtrl::OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message)
3049 if (pWnd != this)
3050 return CListCtrl::OnSetCursor(pWnd, nHitTest, message);
3051 if (!m_bWaitCursor && !m_bBusy)
3053 HCURSOR hCur = LoadCursor(nullptr, IDC_ARROW);
3054 SetCursor(hCur);
3055 return CListCtrl::OnSetCursor(pWnd, nHitTest, message);
3057 HCURSOR hCur = LoadCursor(nullptr, IDC_WAIT);
3058 SetCursor(hCur);
3059 return TRUE;
3062 void CGitStatusListCtrl::RemoveListEntry(int index)
3064 CAutoWriteLock locker(m_guard);
3065 DeleteItem(index);
3067 m_arStatusArray.erase(m_arStatusArray.cbegin() + index);
3069 #if 0
3070 delete m_arStatusArray[m_arListArray[index]];
3071 m_arStatusArray.erase(m_arStatusArray.begin()+m_arListArray[index]);
3072 m_arListArray.erase(m_arListArray.begin()+index);
3073 for (int i=index; i< (int)m_arListArray.size(); ++i)
3075 m_arListArray[i]--;
3077 #endif
3080 ///< Set a checkbox on an entry in the listbox
3081 // NEVER, EVER call SetCheck directly, because you'll end-up with the checkboxes and the 'checked' flag getting out of sync
3082 void CGitStatusListCtrl::SetEntryCheck(CTGitPath* pEntry, int listboxIndex, bool bCheck)
3084 CAutoWriteLock locker(m_guard);
3085 pEntry->m_Checked = bCheck;
3086 m_mapFilenameToChecked[pEntry->GetGitPathString()] = bCheck;
3087 SetCheck(listboxIndex, bCheck);
3090 void CGitStatusListCtrl::ResetChecked(const CTGitPath& entry)
3092 CAutoWriteLock locker(m_guard);
3093 CTGitPath adjustedEntry;
3094 if (g_Git.m_CurrentDir[g_Git.m_CurrentDir.GetLength() - 1] == L'\\')
3095 adjustedEntry.SetFromWin(entry.GetWinPathString().Right(entry.GetWinPathString().GetLength() - g_Git.m_CurrentDir.GetLength()));
3096 else
3097 adjustedEntry.SetFromWin(entry.GetWinPathString().Right(entry.GetWinPathString().GetLength() - g_Git.m_CurrentDir.GetLength() - 1));
3098 if (entry.IsDirectory())
3100 STRING_VECTOR toDelete;
3101 for (auto it = m_mapFilenameToChecked.begin(); it != m_mapFilenameToChecked.end(); ++it)
3103 if (adjustedEntry.IsAncestorOf(it->first))
3104 toDelete.emplace_back(it->first);
3106 for (const auto& file : toDelete)
3107 m_mapFilenameToChecked.erase(file);
3108 return;
3110 m_mapFilenameToChecked.erase(adjustedEntry.GetGitPathString());
3113 #if 0
3114 void CGitStatusListCtrl::SetCheckOnAllDescendentsOf(const FileEntry* parentEntry, bool bCheck)
3116 CAutoWriteLock locker(m_guard);
3117 int nListItems = GetItemCount();
3118 for (int j=0; j< nListItems ; ++j)
3120 FileEntry * childEntry = GetListEntry(j);
3121 ASSERT(childEntry);
3122 if (!childEntry || childEntry == parentEntry)
3123 continue;
3124 if (childEntry->checked != bCheck)
3126 if (parentEntry->path.IsAncestorOf(childEntry->path))
3128 SetEntryCheck(childEntry,j,bCheck);
3129 if(bCheck)
3130 m_nSelected++;
3131 else
3132 m_nSelected--;
3137 #endif
3139 void CGitStatusListCtrl::WriteCheckedNamesToPathList(CTGitPathList& pathList)
3141 pathList.Clear();
3142 CAutoReadLock locker(m_guard);
3143 int nListItems = GetItemCount();
3144 for (int i = 0; i< nListItems; ++i)
3146 auto entry = GetListEntry(i);
3147 if (entry->m_Checked)
3148 pathList.AddPath(*entry);
3150 pathList.SortByPathname();
3154 /// Build a path list of all the selected items in the list (NOTE - SELECTED, not CHECKED)
3155 void CGitStatusListCtrl::FillListOfSelectedItemPaths(CTGitPathList& pathList, bool /*bNoIgnored*/)
3157 pathList.Clear();
3159 CAutoReadLock locker(m_guard);
3160 POSITION pos = GetFirstSelectedItemPosition();
3161 int index;
3162 while ((index = GetNextSelectedItem(pos)) >= 0)
3164 auto entry = GetListEntry(index);
3165 //if ((bNoIgnored)&&(entry->status == git_wc_status_ignored))
3166 // continue;
3167 pathList.AddPath(*entry);
3171 UINT CGitStatusListCtrl::OnGetDlgCode()
3173 // we want to process the return key and not have that one
3174 // routed to the default pushbutton
3175 return CListCtrl::OnGetDlgCode() | DLGC_WANTALLKEYS;
3178 void CGitStatusListCtrl::OnNMReturn(NMHDR * /*pNMHDR*/, LRESULT *pResult)
3180 *pResult = 0;
3181 CAutoReadWeakLock readLock(m_guard);
3182 if (!readLock.IsAcquired())
3183 return;
3184 if (!CheckMultipleDiffs())
3185 return;
3186 bool needsRefresh = false;
3187 bool resolvedTreeConfict = false;
3188 POSITION pos = GetFirstSelectedItemPosition();
3189 while ( pos )
3191 int index = GetNextSelectedItem(pos);
3192 if (index < 0)
3193 return;
3194 auto file = GetListEntry(index);
3195 if (file == nullptr)
3196 return;
3197 if (file->m_Action & (CTGitPath::LOGACTIONS_UNVER | CTGitPath::LOGACTIONS_IGNORE))
3198 StartDiffWC(index);
3199 else if ((file->m_Action & CTGitPath::LOGACTIONS_UNMERGED))
3201 if (CAppUtils::ConflictEdit(GetParentHWND(), *file, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000), m_bIsRevertTheirMy, GetLogicalParent() ? GetLogicalParent()->GetSafeHwnd() : nullptr))
3203 CString conflictedFile = g_Git.CombinePath(file);
3204 needsRefresh = needsRefresh || !PathFileExists(conflictedFile);
3205 resolvedTreeConfict = resolvedTreeConfict || (file->m_Action & CTGitPath::LOGACTIONS_UNMERGED) == 0;
3208 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))
3209 continue;
3210 else
3212 if (!m_Rev1.IsEmpty() && !m_Rev2.IsEmpty())
3213 StartDiffTwo(index);
3214 else
3215 StartDiff(index);
3218 if (needsRefresh && GetLogicalParent() && GetLogicalParent()->GetSafeHwnd())
3219 GetLogicalParent()->SendMessage(GITSLNM_NEEDSREFRESH);
3220 else if (resolvedTreeConfict)
3222 StoreScrollPos();
3223 Show(m_dwShow, 0, m_bShowFolders, 0, true);
3227 void CGitStatusListCtrl::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags)
3229 // Since we catch all keystrokes (to have the enter key processed here instead
3230 // of routed to the default pushbutton) we have to make sure that other
3231 // keys like Tab and Esc still do what they're supposed to do
3232 // Tab = change focus to next/previous control
3233 // Esc = quit the dialog
3234 switch (nChar)
3236 case (VK_TAB):
3238 ::PostMessage(GetLogicalParent()->GetSafeHwnd(), WM_NEXTDLGCTL, GetKeyState(VK_SHIFT)&0x8000, 0);
3239 return;
3241 break;
3242 case (VK_ESCAPE):
3244 ::SendMessage(GetLogicalParent()->GetSafeHwnd(), WM_CLOSE, 0, 0);
3246 break;
3249 CListCtrl::OnKeyDown(nChar, nRepCnt, nFlags);
3252 void CGitStatusListCtrl::PreSubclassWindow()
3254 __super::PreSubclassWindow();
3255 EnableToolTips(TRUE);
3256 SetWindowTheme(GetSafeHwnd(), L"Explorer", nullptr);
3259 void CGitStatusListCtrl::OnPaint()
3261 LRESULT defres = Default();
3262 if ((m_bBusy) || (m_bEmpty))
3264 CString str;
3265 if (m_bBusy)
3267 if (m_sBusy.IsEmpty())
3268 str.LoadString(IDS_STATUSLIST_BUSYMSG);
3269 else
3270 str = m_sBusy;
3272 else
3274 if (m_sEmpty.IsEmpty())
3275 str.LoadString(IDS_STATUSLIST_EMPTYMSG);
3276 else
3277 str = m_sEmpty;
3279 COLORREF clrText = ::GetSysColor(COLOR_WINDOWTEXT);
3280 COLORREF clrTextBk;
3281 if (IsWindowEnabled())
3282 clrTextBk = ::GetSysColor(COLOR_WINDOW);
3283 else
3284 clrTextBk = ::GetSysColor(COLOR_3DFACE);
3286 CRect rc;
3287 GetClientRect(&rc);
3288 CHeaderCtrl* pHC = GetHeaderCtrl();
3289 if (pHC)
3291 CRect rcH;
3292 pHC->GetItemRect(0, &rcH);
3293 rc.top += rcH.bottom;
3295 CDC* pDC = GetDC();
3297 CMyMemDC memDC(pDC, &rc);
3299 memDC.SetTextColor(clrText);
3300 memDC.SetBkColor(clrTextBk);
3301 memDC.BitBlt(rc.left, rc.top, rc.Width(), rc.Height(), pDC, rc.left, rc.top, SRCCOPY);
3302 rc.top += 10;
3303 CGdiObject* oldfont = memDC.SelectObject(CGdiObject::FromHandle(m_uiFont));
3304 memDC.DrawText(str, rc, DT_CENTER | DT_VCENTER |
3305 DT_WORDBREAK | DT_NOPREFIX | DT_NOCLIP);
3306 memDC.SelectObject(oldfont);
3308 ReleaseDC(pDC);
3310 if (defres)
3312 // the Default() call did not process the WM_PAINT message!
3313 // Validate the update region ourselves to avoid
3314 // an endless loop repainting
3315 CRect rc;
3316 GetUpdateRect(&rc, FALSE);
3317 if (!rc.IsRectEmpty())
3318 ValidateRect(rc);
3322 void CGitStatusListCtrl::OnBeginDrag(NMHDR* pNMHDR, LRESULT* pResult)
3324 LPNMLISTVIEW pNMLV = reinterpret_cast<LPNMLISTVIEW>(pNMHDR);
3326 CAutoReadLock locker(m_guard);
3328 CTGitPathList pathList;
3329 FillListOfSelectedItemPaths(pathList);
3330 if (pathList.IsEmpty())
3331 return;
3333 auto pdsrc = std::make_unique<CIDropSource>();
3334 if (!pdsrc)
3335 return;
3336 pdsrc->AddRef();
3338 GitDataObject* pdobj = new GitDataObject(pathList, m_Rev2.IsEmpty() ? m_CurrentVersion : m_Rev2);
3339 if (!pdobj)
3340 return;
3341 pdobj->AddRef();
3343 CDragSourceHelper dragsrchelper;
3345 SetRedraw(false);
3346 dragsrchelper.InitializeFromWindow(m_hWnd, pNMLV->ptAction, pdobj);
3347 SetRedraw(true);
3348 //dragsrchelper.InitializeFromBitmap()
3349 pdsrc->m_pIDataObj = pdobj;
3350 pdsrc->m_pIDataObj->AddRef();
3352 // Initiate the Drag & Drop
3353 DWORD dwEffect;
3354 m_bOwnDrag = true;
3355 ::DoDragDrop(pdobj, pdsrc.get(), DROPEFFECT_MOVE | DROPEFFECT_COPY, &dwEffect);
3356 m_bOwnDrag = false;
3357 pdsrc->Release();
3358 pdsrc.release();
3359 pdobj->Release();
3361 *pResult = 0;
3364 bool CGitStatusListCtrl::EnableFileDrop()
3366 m_bFileDropsEnabled = true;
3367 return true;
3370 bool CGitStatusListCtrl::HasPath(const CTGitPath& path)
3372 CAutoReadLock locker(m_guard);
3373 CTGitPath adjustedEntry;
3374 if (g_Git.m_CurrentDir[g_Git.m_CurrentDir.GetLength() - 1] == L'\\')
3375 adjustedEntry.SetFromWin(path.GetWinPathString().Right(path.GetWinPathString().GetLength() - g_Git.m_CurrentDir.GetLength()));
3376 else
3377 adjustedEntry.SetFromWin(path.GetWinPathString().Right(path.GetWinPathString().GetLength() - g_Git.m_CurrentDir.GetLength() - 1));
3378 for (size_t i=0; i < m_arStatusArray.size(); ++i)
3380 if (m_arStatusArray[i]->IsEquivalentTo(adjustedEntry))
3381 return true;
3384 return false;
3387 BOOL CGitStatusListCtrl::PreTranslateMessage(MSG* pMsg)
3389 if (pMsg->message == WM_KEYDOWN)
3391 switch (pMsg->wParam)
3393 case 'A':
3395 if (GetAsyncKeyState(VK_CONTROL)&0x8000)
3397 // select all entries
3398 for (int i=0; i<GetItemCount(); ++i)
3399 SetItemState(i, LVIS_SELECTED, LVIS_SELECTED);
3400 return TRUE;
3403 break;
3404 case 'C':
3405 case VK_INSERT:
3407 if (GetAsyncKeyState(VK_CONTROL)&0x8000)
3409 // copy all selected paths to the clipboard
3410 if (GetAsyncKeyState(VK_SHIFT)&0x8000)
3411 CopySelectedEntriesToClipboard(GITSLC_COLFILENAME | GITSLC_COLSTATUS, IDGITLC_COPYRELPATHS);
3412 else
3413 CopySelectedEntriesToClipboard(GITSLC_COLFILENAME, IDGITLC_COPYRELPATHS);
3414 return TRUE;
3417 break;
3418 case VK_DELETE:
3420 if ((GetSelectedCount() > 0) && (m_dwContextMenus & GITSLC_POPDELETE))
3422 CAutoReadLock locker(m_guard);
3423 auto filepath = GetListEntry(GetSelectionMark());
3424 if (filepath != nullptr && (filepath->m_Action & (CTGitPath::LOGACTIONS_UNVER | CTGitPath::LOGACTIONS_IGNORE)))
3425 DeleteSelectedFiles();
3428 break;
3432 return __super::PreTranslateMessage(pMsg);
3435 bool CGitStatusListCtrl::CopySelectedEntriesToClipboard(DWORD dwCols, int cmd)
3437 if (GetSelectedCount() == 0)
3438 return false;
3440 CString sClipboard;
3442 bool bMultipleColumnSelected = ((dwCols & dwCols - 1) != 0); // multiple columns are selected (clear least signifient bit and check for zero)
3444 #define ADDTOCLIPBOARDSTRING(x) sClipboard += (sClipboard.IsEmpty() || (sClipboard.Right(1)==L"\n")) ? (x) : ('\t' + x)
3445 #define ADDNEWLINETOCLIPBOARDSTRING() sClipboard += (sClipboard.IsEmpty()) ? L"" : L"\r\n"
3447 // first add the column titles as the first line
3448 DWORD selection = 0;
3449 int count = m_ColumnManager.GetColumnCount();
3450 for (int column = 0; column < count; ++column)
3452 if ((dwCols == -1 && m_ColumnManager.IsVisible(column)) || (column < GITSLC_NUMCOLUMNS && (dwCols & (1 << column))))
3454 if (bMultipleColumnSelected)
3455 ADDTOCLIPBOARDSTRING(m_ColumnManager.GetName(column));
3457 selection |= 1 << column;
3461 if (bMultipleColumnSelected)
3462 ADDNEWLINETOCLIPBOARDSTRING();
3464 // maybe clear first line when only one column is selected (btw by select not by dwCols) is simpler(not faster) way
3465 // but why no title on single column output ?
3466 // if (selection & selection-1) == 0 ) sClipboard = "";
3468 CAutoReadLock locker(m_guard);
3470 POSITION pos = GetFirstSelectedItemPosition();
3471 while (pos)
3473 int index = GetNextSelectedItem(pos);
3474 // we selected only cols we want, so not other then select test needed
3475 for (int column = 0; column < count; ++column)
3477 if (cmd && (GITSLC_COLFILENAME & (1 << column)))
3479 auto* entry = GetListEntry(index);
3480 if (entry)
3482 CString sPath;
3483 switch (cmd)
3485 case IDGITLC_COPYFULL:
3486 sPath = g_Git.CombinePath(entry);
3487 break;
3488 case IDGITLC_COPYRELPATHS:
3489 sPath = entry->GetGitPathString();
3490 break;
3491 case IDGITLC_COPYFILENAMES:
3492 sPath = entry->GetFileOrDirectoryName();
3493 break;
3495 ADDTOCLIPBOARDSTRING(sPath);
3498 else if (selection & (1 << column))
3499 ADDTOCLIPBOARDSTRING(GetCellText(index, column));
3502 ADDNEWLINETOCLIPBOARDSTRING();
3505 return CStringUtils::WriteAsciiStringToClipboard(sClipboard);
3508 size_t CGitStatusListCtrl::GetNumberOfChangelistsInSelection()
3510 #if 0
3511 CAutoReadLock locker(m_guard);
3512 std::set<CString> changelists;
3513 POSITION pos = GetFirstSelectedItemPosition();
3514 int index;
3515 while ((index = GetNextSelectedItem(pos)) >= 0)
3517 FileEntry * entry = GetListEntry(index);
3518 if (!entry->changelist.IsEmpty())
3519 changelists.insert(entry->changelist);
3521 return changelists.size();
3522 #endif
3523 return 0;
3526 bool CGitStatusListCtrl::PrepareGroups(bool bForce /* = false */)
3528 CAutoWriteLock locker(m_guard);
3529 bool bHasGroups=false;
3530 int max =0;
3532 for (size_t i = 0; i < m_arStatusArray.size(); ++i)
3534 int ParentNo = m_arStatusArray[i]->m_ParentNo&PARENT_MASK;
3535 if( ParentNo > max)
3536 max=m_arStatusArray[i]->m_ParentNo&PARENT_MASK;
3539 if (((m_dwShow & GITSLC_SHOWUNVERSIONED) && !m_UnRevFileList.IsEmpty()) ||
3540 ((m_dwShow & GITSLC_SHOWIGNORED) && !m_IgnoreFileList.IsEmpty()) ||
3541 (m_dwShow & (GITSLC_SHOWASSUMEVALID | GITSLC_SHOWSKIPWORKTREE) && !m_LocalChangesIgnoredFileList.IsEmpty()) ||
3542 max>0 || bForce)
3544 bHasGroups = true;
3547 RemoveAllGroups();
3548 EnableGroupView(bHasGroups);
3550 TCHAR groupname[1024] = { 0 };
3551 int groupindex = 0;
3553 if(bHasGroups)
3555 LVGROUP grp = {0};
3556 grp.cbSize = sizeof(LVGROUP);
3557 grp.mask = LVGF_ALIGN | LVGF_GROUPID | LVGF_HEADER;
3558 groupindex=0;
3560 //if(m_UnRevFileList.GetCount()>0)
3561 if(max >0)
3563 wcsncpy_s(groupname, 1024, (LPCTSTR)CString(MAKEINTRESOURCE(IDS_STATUSLIST_GROUP_MERGEDFILES)), 1023);
3564 grp.pszHeader = groupname;
3565 grp.iGroupId = MERGE_MASK;
3566 grp.uAlign = LVGA_HEADER_LEFT;
3567 InsertGroup(0, &grp);
3569 CAutoRepository repository(g_Git.GetGitRepository());
3570 if (!repository)
3571 MessageBox(CGit::GetLibGit2LastErr(L"Could not open repository."), L"TortoiseGit", MB_OK | MB_ICONERROR);
3572 for (int i = 0; i <= max; ++i)
3574 CString str;
3575 str.Format(IDS_STATUSLIST_GROUP_DIFFWITHPARENT, i+1);
3576 if (repository)
3578 CString rev;
3579 rev.Format(L"%s^%d", (LPCTSTR)m_CurrentVersion, i + 1);
3580 CGitHash hash;
3581 if (!CGit::GetHash(repository, hash, rev))
3582 str += L": " + hash.ToString().Left(g_Git.GetShortHASHLength());
3584 grp.pszHeader = str.GetBuffer();
3585 str.ReleaseBuffer();
3586 grp.iGroupId = i;
3587 grp.uAlign = LVGA_HEADER_LEFT;
3588 InsertGroup(i+1, &grp);
3591 else
3593 wcsncpy_s(groupname, 1024, (LPCTSTR)CString(MAKEINTRESOURCE(IDS_STATUSLIST_GROUP_MODIFIEDFILES)), 1023);
3594 grp.pszHeader = groupname;
3595 grp.iGroupId = groupindex;
3596 grp.uAlign = LVGA_HEADER_LEFT;
3597 InsertGroup(groupindex++, &grp);
3600 wcsncpy_s(groupname, 1024, (LPCTSTR)CString(MAKEINTRESOURCE(IDS_STATUSLIST_GROUP_NOTVERSIONEDFILES)), 1023);
3601 grp.pszHeader = groupname;
3602 grp.iGroupId = groupindex;
3603 grp.uAlign = LVGA_HEADER_LEFT;
3604 InsertGroup(groupindex++, &grp);
3607 //if(m_IgnoreFileList.GetCount()>0)
3609 wcsncpy_s(groupname, 1024, (LPCTSTR)CString(MAKEINTRESOURCE(IDS_STATUSLIST_GROUP_IGNOREDFILES)), 1023);
3610 grp.pszHeader = groupname;
3611 grp.iGroupId = groupindex;
3612 grp.uAlign = LVGA_HEADER_LEFT;
3613 InsertGroup(groupindex++, &grp);
3617 wcsncpy_s(groupname, 1024, (LPCTSTR)CString(MAKEINTRESOURCE(IDS_STATUSLIST_GROUP_IGNORELOCALCHANGES)), 1023);
3618 grp.pszHeader = groupname;
3619 grp.iGroupId = groupindex;
3620 grp.uAlign = LVGA_HEADER_LEFT;
3621 InsertGroup(groupindex++, &grp);
3626 #if 0
3627 m_bHasIgnoreGroup = false;
3629 // now add the items which don't belong to a group
3630 LVGROUP grp = {0};
3631 grp.cbSize = sizeof(LVGROUP);
3632 grp.mask = LVGF_ALIGN | LVGF_GROUPID | LVGF_HEADER;
3633 CString sUnassignedName(MAKEINTRESOURCE(IDS_STATUSLIST_UNASSIGNED_CHANGESET));
3634 wcsncpy_s(groupname, 1024, (LPCTSTR)sUnassignedName, 1023);
3635 grp.pszHeader = groupname;
3636 grp.iGroupId = groupindex;
3637 grp.uAlign = LVGA_HEADER_LEFT;
3638 InsertGroup(groupindex++, &grp);
3640 for (auto it = m_changelists.cbegin(); it != m_changelists.cend(); ++it)
3642 if (it->first.Compare(SVNSLC_IGNORECHANGELIST)!=0)
3644 LVGROUP grp = {0};
3645 grp.cbSize = sizeof(LVGROUP);
3646 grp.mask = LVGF_ALIGN | LVGF_GROUPID | LVGF_HEADER;
3647 wcsncpy_s(groupname, 1024, it->first, 1023);
3648 grp.pszHeader = groupname;
3649 grp.iGroupId = groupindex;
3650 grp.uAlign = LVGA_HEADER_LEFT;
3651 it->second = InsertGroup(groupindex++, &grp);
3653 else
3654 m_bHasIgnoreGroup = true;
3657 if (m_bHasIgnoreGroup)
3659 // and now add the group 'ignore-on-commit'
3660 std::map<CString,int>::iterator it = m_changelists.find(SVNSLC_IGNORECHANGELIST);
3661 if (it != m_changelists.end())
3663 grp.cbSize = sizeof(LVGROUP);
3664 grp.mask = LVGF_ALIGN | LVGF_GROUPID | LVGF_HEADER;
3665 wcsncpy_s(groupname, 1024, SVNSLC_IGNORECHANGELIST, 1023);
3666 grp.pszHeader = groupname;
3667 grp.iGroupId = groupindex;
3668 grp.uAlign = LVGA_HEADER_LEFT;
3669 it->second = InsertGroup(groupindex, &grp);
3672 #endif
3673 return bHasGroups;
3676 void CGitStatusListCtrl::NotifyCheck()
3678 CWnd* pParent = GetLogicalParent();
3679 if (pParent && pParent->GetSafeHwnd())
3681 pParent->SendMessage(GITSLNM_CHECKCHANGED, m_nSelected);
3685 int CGitStatusListCtrl::UpdateFileList(const CTGitPathList* list)
3687 CAutoWriteLock locker(m_guard);
3688 m_CurrentVersion = GIT_REV_ZERO;
3690 g_Git.GetWorkingTreeChanges(m_StatusFileList, m_amend, list);
3692 BOOL bDeleteChecked = FALSE;
3693 int deleteFromIndex = 0;
3694 bool needsRefresh = false;
3695 for (int i = 0; i < m_StatusFileList.GetCount(); ++i)
3697 auto gitpatch = const_cast<CTGitPath*>(&m_StatusFileList[i]);
3698 gitpatch->m_Checked = TRUE;
3700 if ((gitpatch->m_Action & (CTGitPath::LOGACTIONS_ADDED | CTGitPath::LOGACTIONS_REPLACED | CTGitPath::LOGACTIONS_MODIFIED)) && !gitpatch->Exists())
3702 if (!bDeleteChecked)
3704 CString message;
3705 message.Format(IDS_ASK_REMOVE_FROM_INDEX, gitpatch->GetWinPath());
3706 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);
3708 if (deleteFromIndex == 1)
3710 CString err;
3711 if (g_Git.Run(L"git.exe checkout -- \"" + gitpatch->GetWinPathString() + L'"', &err, CP_UTF8))
3712 MessageBox(L"Restoring from index failed:\n" + err, L"TortoiseGit", MB_ICONERROR);
3713 else
3714 needsRefresh = true;
3716 else if (deleteFromIndex == 2)
3718 CString err;
3719 if (g_Git.Run(L"git.exe rm -f --cache -- \"" + gitpatch->GetWinPathString() + L'"', &err, CP_UTF8))
3720 MessageBox(L"Removing from index failed:\n" + err, L"TortoiseGit", MB_ICONERROR);
3721 else
3722 needsRefresh = true;
3726 m_arStatusArray.push_back(&m_StatusFileList[i]);
3729 if (needsRefresh)
3730 MessageBox(L"Due to changes to the index, please refresh the dialog (e.g., by pressing F5).", L"TortoiseGit", MB_ICONINFORMATION);
3732 return 0;
3735 int CGitStatusListCtrl::UpdateWithGitPathList(CTGitPathList &list)
3737 CAutoWriteLock locker(m_guard);
3738 m_arStatusArray.clear();
3739 for (int i = 0; i < list.GetCount(); ++i)
3741 auto gitpath = const_cast<CTGitPath*>(&list[i]);
3743 if(gitpath ->m_Action & CTGitPath::LOGACTIONS_HIDE)
3744 continue;
3746 gitpath->m_Checked = TRUE;
3747 m_arStatusArray.push_back(&list[i]);
3749 return 0;
3752 int CGitStatusListCtrl::UpdateUnRevFileList(CTGitPathList &list)
3754 CAutoWriteLock locker(m_guard);
3755 m_UnRevFileList = list;
3756 for (int i = 0; i < m_UnRevFileList.GetCount(); ++i)
3758 auto gitpatch = const_cast<CTGitPath*>(&m_UnRevFileList[i]);
3759 gitpatch->m_Checked = FALSE;
3760 m_arStatusArray.push_back(&m_UnRevFileList[i]);
3762 return 0;
3765 int CGitStatusListCtrl::UpdateUnRevFileList(const CTGitPathList* List)
3767 CAutoWriteLock locker(m_guard);
3768 CString err;
3769 if (m_UnRevFileList.FillUnRev(CTGitPath::LOGACTIONS_UNVER, List, &err))
3771 MessageBox(L"Failed to get UnRev file list\n" + err, L"TortoiseGit", MB_OK | MB_ICONERROR);
3772 return -1;
3775 if (m_StatusFileList.m_Action & CTGitPath::LOGACTIONS_DELETED)
3777 int unrev = 0;
3778 int status = 0;
3779 while (unrev < m_UnRevFileList.GetCount() && status < m_StatusFileList.GetCount())
3781 auto cmp = CTGitPath::Compare(m_UnRevFileList[unrev], m_StatusFileList[status]);
3782 if (cmp < 1)
3784 ++unrev;
3785 continue;
3787 if (cmp == 1)
3788 m_UnRevFileList.RemovePath(m_StatusFileList[status]);
3789 ++status;
3793 for (int i = 0; i < m_UnRevFileList.GetCount(); ++i)
3795 auto gitpatch = const_cast<CTGitPath*>(&m_UnRevFileList[i]);
3796 gitpatch->m_Checked = FALSE;
3797 m_arStatusArray.push_back(&m_UnRevFileList[i]);
3799 return 0;
3802 int CGitStatusListCtrl::UpdateIgnoreFileList(const CTGitPathList* List)
3804 CAutoWriteLock locker(m_guard);
3805 CString err;
3806 if (m_IgnoreFileList.FillUnRev(CTGitPath::LOGACTIONS_IGNORE, List, &err))
3808 MessageBox(L"Failed to get Ignore file list\n" + err, L"TortoiseGit", MB_OK | MB_ICONERROR);
3809 return -1;
3812 for (int i = 0; i < m_IgnoreFileList.GetCount(); ++i)
3814 auto gitpatch = const_cast<CTGitPath*>(&m_IgnoreFileList[i]);
3815 gitpatch->m_Checked = FALSE;
3816 m_arStatusArray.push_back(&m_IgnoreFileList[i]);
3818 return 0;
3821 int CGitStatusListCtrl::UpdateLocalChangesIgnoredFileList(const CTGitPathList* list)
3823 CAutoWriteLock locker(m_guard);
3824 m_LocalChangesIgnoredFileList.FillBasedOnIndexFlags(GIT_IDXENTRY_VALID, GIT_IDXENTRY_SKIP_WORKTREE, list);
3825 for (int i = 0; i < m_LocalChangesIgnoredFileList.GetCount(); ++i)
3827 auto gitpatch = const_cast<CTGitPath*>(&m_LocalChangesIgnoredFileList[i]);
3828 gitpatch->m_Checked = FALSE;
3829 m_arStatusArray.push_back(&m_LocalChangesIgnoredFileList[i]);
3831 return 0;
3834 int CGitStatusListCtrl::UpdateFileList(int mask, bool once, const CTGitPathList* pList)
3836 CAutoWriteLock locker(m_guard);
3837 auto List = (pList && pList->GetCount() >= 1 && !(*pList)[0].GetWinPathString().IsEmpty()) ? pList : nullptr;
3838 if(mask&CGitStatusListCtrl::FILELIST_MODIFY)
3840 if(once || (!(m_FileLoaded&CGitStatusListCtrl::FILELIST_MODIFY)))
3842 UpdateFileList(List);
3843 m_FileLoaded|=CGitStatusListCtrl::FILELIST_MODIFY;
3846 if (mask & CGitStatusListCtrl::FILELIST_UNVER || mask & CGitStatusListCtrl::FILELIST_IGNORE)
3848 if(once || (!(m_FileLoaded&CGitStatusListCtrl::FILELIST_UNVER)))
3850 UpdateUnRevFileList(List);
3851 m_FileLoaded|=CGitStatusListCtrl::FILELIST_UNVER;
3853 if(mask&CGitStatusListCtrl::FILELIST_IGNORE && (once || (!(m_FileLoaded&CGitStatusListCtrl::FILELIST_IGNORE))))
3855 UpdateIgnoreFileList(List);
3856 m_FileLoaded |= CGitStatusListCtrl::FILELIST_IGNORE;
3859 if (mask & CGitStatusListCtrl::FILELIST_LOCALCHANGESIGNORED && (once || (!(m_FileLoaded & CGitStatusListCtrl::FILELIST_LOCALCHANGESIGNORED))))
3861 UpdateLocalChangesIgnoredFileList(List);
3862 m_FileLoaded |= CGitStatusListCtrl::FILELIST_LOCALCHANGESIGNORED;
3864 return 0;
3867 void CGitStatusListCtrl::Clear()
3869 CAutoWriteLock locker(m_guard);
3870 m_FileLoaded=0;
3871 this->DeleteAllItems();
3872 this->m_arListArray.clear();
3873 this->m_arStatusArray.clear();
3874 this->m_changelists.clear();
3877 bool CGitStatusListCtrl::CheckMultipleDiffs()
3879 UINT selCount = GetSelectedCount();
3880 if (selCount > max(3, (DWORD)CRegDWORD(L"Software\\TortoiseGit\\NumDiffWarning", 10)))
3882 CString message;
3883 message.Format(IDS_STATUSLIST_WARN_MAXDIFF, selCount);
3884 return MessageBox(message, L"TortoiseGit", MB_YESNO | MB_ICONQUESTION) == IDYES;
3886 return true;
3889 //////////////////////////////////////////////////////////////////////////
3890 bool CGitStatusListCtrlDropTarget::OnDrop(FORMATETC* pFmtEtc, STGMEDIUM& medium, DWORD* /*pdwEffect*/, POINTL pt)
3892 if (pFmtEtc->cfFormat == CF_HDROP && medium.tymed == TYMED_HGLOBAL)
3894 HDROP hDrop = (HDROP)GlobalLock(medium.hGlobal);
3895 if (hDrop)
3897 TCHAR szFileName[MAX_PATH] = {0};
3899 UINT cFiles = DragQueryFile(hDrop, 0xFFFFFFFF, nullptr, 0);
3901 POINT clientpoint;
3902 clientpoint.x = pt.x;
3903 clientpoint.y = pt.y;
3904 ScreenToClient(m_hTargetWnd, &clientpoint);
3905 if ((m_pGitStatusListCtrl->IsGroupViewEnabled()) && (m_pGitStatusListCtrl->GetGroupFromPoint(&clientpoint) >= 0))
3907 #if 0
3908 CTGitPathList changelistItems;
3909 for (UINT i = 0; i < cFiles; ++i)
3911 if (DragQueryFile(hDrop, i, szFileName, _countof(szFileName)))
3912 changelistItems.AddPath(CTGitPath(szFileName));
3914 // find the changelist name
3915 CString sChangelist;
3916 LONG_PTR nGroup = m_pGitStatusListCtrl->GetGroupFromPoint(&clientpoint);
3917 for (std::map<CString, int>::iterator it = m_pGitStatusListCtrl->m_changelists.begin(); it != m_pGitStatusListCtrl->m_changelists.end(); ++it)
3918 if (it->second == nGroup)
3919 sChangelist = it->first;
3921 if (!sChangelist.IsEmpty())
3923 CGit git;
3924 if (git.AddToChangeList(changelistItems, sChangelist, git_depth_empty))
3926 for (int l=0; l<changelistItems.GetCount(); ++l)
3928 int index = m_pGitStatusListCtrl->GetIndex(changelistItems[l]);
3929 if (index >= 0)
3931 auto e = m_pGitStatusListCtrl->GetListEntry(index);
3932 if (e)
3934 e->changelist = sChangelist;
3935 if (!e->IsFolder())
3937 if (m_pGitStatusListCtrl->m_changelists.find(e->changelist) != m_pGitStatusListCtrl->m_changelists.end())
3938 m_pGitStatusListCtrl->SetItemGroup(index, m_pGitStatusListCtrl->m_changelists[e->changelist]);
3939 else
3940 m_pGitStatusListCtrl->SetItemGroup(index, 0);
3944 else
3946 HWND hParentWnd = GetParent(m_hTargetWnd);
3947 if (hParentWnd)
3948 ::SendMessage(hParentWnd, CGitStatusListCtrl::GITSLNM_ADDFILE, 0, (LPARAM)changelistItems[l].GetWinPath());
3952 else
3953 CMessageBox::Show(m_pGitStatusListCtrl->m_hWnd, git.GetLastErrorMessage(), L"TortoiseGit", MB_ICONERROR);
3955 else
3957 SVN git;
3958 if (git.RemoveFromChangeList(changelistItems, CStringArray(), git_depth_empty))
3960 for (int l=0; l<changelistItems.GetCount(); ++l)
3962 int index = m_pGitStatusListCtrl->GetIndex(changelistItems[l]);
3963 if (index >= 0)
3965 auto e = m_pGitStatusListCtrl->GetListEntry(index);
3966 if (e)
3968 e->changelist = sChangelist;
3969 m_pGitStatusListCtrl->SetItemGroup(index, 0);
3972 else
3974 HWND hParentWnd = GetParent(m_hTargetWnd);
3975 if (hParentWnd)
3976 ::SendMessage(hParentWnd, CGitStatusListCtrl::GITSLNM_ADDFILE, 0, (LPARAM)changelistItems[l].GetWinPath());
3980 else
3981 CMessageBox::Show(m_pGitStatusListCtrl->m_hWnd, git.GetLastErrorMessage(), L"TortoiseGit", MB_ICONERROR);
3983 #endif
3985 else
3987 for (UINT i = 0; i < cFiles; ++i)
3989 if (!DragQueryFile(hDrop, i, szFileName, _countof(szFileName)))
3990 continue;
3992 HWND hParentWnd = GetParent(m_hTargetWnd);
3993 if (hParentWnd)
3994 ::SendMessage(hParentWnd, CGitStatusListCtrl::GITSLNM_ADDFILE, 0, (LPARAM)szFileName);
3998 GlobalUnlock(medium.hGlobal);
4000 return true; //let base free the medium
4003 HRESULT STDMETHODCALLTYPE CGitStatusListCtrlDropTarget::DragOver(DWORD grfKeyState, POINTL pt, DWORD __RPC_FAR* pdwEffect)
4005 CIDropTarget::DragOver(grfKeyState, pt, pdwEffect);
4006 *pdwEffect = DROPEFFECT_COPY;
4007 if (m_pGitStatusListCtrl)
4009 POINT clientpoint;
4010 clientpoint.x = pt.x;
4011 clientpoint.y = pt.y;
4012 ScreenToClient(m_hTargetWnd, &clientpoint);
4013 if ((m_pGitStatusListCtrl->IsGroupViewEnabled()) && (m_pGitStatusListCtrl->GetGroupFromPoint(&clientpoint) >= 0))
4014 *pdwEffect = DROPEFFECT_NONE;
4015 else if ((!m_pGitStatusListCtrl->m_bFileDropsEnabled) || (m_pGitStatusListCtrl->m_bOwnDrag))
4016 *pdwEffect = DROPEFFECT_NONE;
4018 return S_OK;
4021 void CGitStatusListCtrl::FilesExport()
4023 CAutoReadLock locker(m_guard);
4024 CString exportDir;
4025 // export all changed files to a folder
4026 CBrowseFolder browseFolder;
4027 browseFolder.m_style = BIF_EDITBOX | BIF_NEWDIALOGSTYLE | BIF_RETURNFSANCESTORS | BIF_RETURNONLYFSDIRS;
4028 if (browseFolder.Show(GetParentHWND(), exportDir) != CBrowseFolder::OK)
4029 return;
4031 POSITION pos = GetFirstSelectedItemPosition();
4032 int index;
4033 while ((index = GetNextSelectedItem(pos)) >= 0)
4035 auto fd = GetListEntry(index);
4036 // we cannot export directories or folders
4037 if ((fd->m_Action & CTGitPath::LOGACTIONS_DELETED) || fd->IsDirectory())
4038 continue;
4040 CPathUtils::MakeSureDirectoryPathExists(exportDir + L'\\' + fd->GetContainingDirectory().GetWinPathString());
4041 CString filename = exportDir + L'\\' + fd->GetWinPathString();
4042 if (m_CurrentVersion == GIT_REV_ZERO)
4044 if (!CopyFile(g_Git.CombinePath(fd), filename, false))
4046 MessageBox(CFormatMessageWrapper(), L"TortoiseGit", MB_OK | MB_ICONERROR);
4047 return;
4050 else
4052 if (g_Git.GetOneFile(m_CurrentVersion, *fd, filename))
4054 CString out;
4055 out.Format(IDS_STATUSLIST_CHECKOUTFILEFAILED, (LPCTSTR)fd->GetGitPathString(), (LPCTSTR)m_CurrentVersion, (LPCTSTR)filename);
4056 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)
4057 return;
4063 void CGitStatusListCtrl::FileSaveAs(CTGitPath *path)
4065 CAutoReadLock locker(m_guard);
4066 CString filename;
4067 filename.Format(L"%s\\%s-%s%s", (LPCTSTR)g_Git.CombinePath(path->GetContainingDirectory()), (LPCTSTR)path->GetBaseFilename(), (LPCTSTR)m_CurrentVersion.Left(g_Git.GetShortHASHLength()), (LPCTSTR)path->GetFileExtension());
4068 if (!CAppUtils::FileOpenSave(filename, nullptr, 0, 0, false, GetSafeHwnd()))
4069 return;
4070 if (m_CurrentVersion == GIT_REV_ZERO)
4072 if (!CopyFile(g_Git.CombinePath(path), filename, false))
4074 MessageBox(CFormatMessageWrapper(), L"TortoiseGit", MB_OK | MB_ICONERROR);
4075 return;
4078 else
4080 if (g_Git.GetOneFile(m_CurrentVersion, *path, filename))
4082 CString out;
4083 out.Format(IDS_STATUSLIST_CHECKOUTFILEFAILED, (LPCTSTR)path->GetGitPathString(), (LPCTSTR)m_CurrentVersion, (LPCTSTR)filename);
4084 CMessageBox::Show(GetParentHWND(), g_Git.GetGitLastErr(out, CGit::GIT_CMD_GETONEFILE), L"TortoiseGit", MB_OK);
4085 return;
4090 int CGitStatusListCtrl::RevertSelectedItemToVersion(bool parent)
4092 CAutoReadLock locker(m_guard);
4093 if(this->m_CurrentVersion.IsEmpty())
4094 return 0;
4095 if(this->m_CurrentVersion == GIT_REV_ZERO)
4096 return 0;
4098 POSITION pos = GetFirstSelectedItemPosition();
4099 int index;
4100 CString cmd,out;
4101 std::map<CString, int> versionMap;
4102 while ((index = GetNextSelectedItem(pos)) >= 0)
4104 auto fentry = GetListEntry(index);
4105 CString version;
4106 if (parent)
4108 int parentNo = fentry->m_ParentNo & PARENT_MASK;
4109 CString ref;
4110 ref.Format(L"%s^%d", (LPCTSTR)m_CurrentVersion, parentNo + 1);
4111 CGitHash hash;
4112 if (g_Git.GetHash(hash, ref))
4114 MessageBox(g_Git.GetGitLastErr(L"Could not get hash of ref \"" + ref + L"\"."), L"TortoiseGit", MB_ICONERROR);
4115 continue;
4118 version = hash.ToString();
4120 else
4121 version = m_CurrentVersion;
4123 CString filename = fentry->GetGitPathString();
4124 if (!fentry->GetGitOldPathString().IsEmpty())
4125 filename = fentry->GetGitOldPathString();
4126 cmd.Format(L"git.exe checkout %s -- \"%s\"", (LPCTSTR)version, (LPCTSTR)filename);
4127 out.Empty();
4128 if (g_Git.Run(cmd, &out, CP_UTF8))
4130 if (MessageBox(out, L"TortoiseGit", MB_ICONEXCLAMATION | MB_OKCANCEL) == IDCANCEL)
4131 continue;
4133 else
4134 versionMap[version]++;
4137 out.Empty();
4138 for (auto it = versionMap.cbegin(); it != versionMap.cend(); ++it)
4140 CString versionEntry;
4141 versionEntry.Format(IDS_STATUSLIST_FILESREVERTED, it->second, (LPCTSTR)it->first);
4142 out += versionEntry + L"\r\n";
4144 if (!out.IsEmpty())
4146 if (GetLogicalParent() && GetLogicalParent()->GetSafeHwnd())
4147 GetLogicalParent()->SendMessage(GITSLNM_NEEDSREFRESH);
4148 CMessageBox::Show(GetParentHWND(), out, L"TortoiseGit", MB_OK);
4150 return 0;
4153 void CGitStatusListCtrl::OpenFile(CTGitPath*filepath,int mode)
4155 CString file;
4156 if(this->m_CurrentVersion.IsEmpty() || m_CurrentVersion == GIT_REV_ZERO)
4157 file = g_Git.CombinePath(filepath);
4158 else
4160 file = CTempFiles::Instance().GetTempFilePath(false, *filepath, m_CurrentVersion).GetWinPathString();
4161 CString cmd,out;
4162 if(g_Git.GetOneFile(m_CurrentVersion, *filepath, file))
4164 out.Format(IDS_STATUSLIST_CHECKOUTFILEFAILED, (LPCTSTR)filepath->GetGitPathString(), (LPCTSTR)m_CurrentVersion, (LPCTSTR)file);
4165 CMessageBox::Show(GetParentHWND(), g_Git.GetGitLastErr(out, CGit::GIT_CMD_GETONEFILE), L"TortoiseGit", MB_OK);
4166 return;
4168 SetFileAttributes(file, FILE_ATTRIBUTE_READONLY);
4170 if(mode == ALTERNATIVEEDITOR)
4172 CAppUtils::LaunchAlternativeEditor(file);
4173 return;
4176 if (mode == OPEN)
4177 CAppUtils::ShellOpen(file, GetSafeHwnd());
4178 else
4179 CAppUtils::ShowOpenWithDialog(file, GetSafeHwnd());
4182 void CGitStatusListCtrl::DeleteSelectedFiles()
4184 CAutoWriteLock locker(m_guard);
4185 //Collect paths
4186 std::vector<int> selectIndex;
4188 POSITION pos = GetFirstSelectedItemPosition();
4189 int index;
4190 while ((index = GetNextSelectedItem(pos)) >= 0)
4191 selectIndex.push_back(index);
4193 CAutoRepository repo = g_Git.GetGitRepository();
4194 if (!repo)
4196 MessageBox(g_Git.GetLibGit2LastErr(L"Could not open repository."), L"TortoiseGit", MB_OK);
4197 return;
4199 CAutoIndex gitIndex;
4200 if (git_repository_index(gitIndex.GetPointer(), repo))
4202 g_Git.GetLibGit2LastErr(L"Could not open index.");
4203 return;
4205 int needWriteIndex = 0;
4207 //Create file-list ('\0' separated) for SHFileOperation
4208 CString filelist;
4209 for (size_t i = 0; i < selectIndex.size(); ++i)
4211 index = selectIndex[i];
4213 auto path = GetListEntry(index);
4214 if (path == nullptr)
4215 continue;
4217 // do not report errors as we could remove an unversioned file
4218 needWriteIndex += git_index_remove_bypath(gitIndex, CUnicodeUtils::GetUTF8(path->GetGitPathString())) == 0 ? 1 : 0;
4220 if (!path->Exists())
4221 continue;
4223 filelist += path->GetWinPathString();
4224 filelist += L'|';
4226 filelist += L'|';
4227 int len = filelist.GetLength();
4228 auto buf = std::make_unique<TCHAR[]>(len + 2);
4229 wcscpy_s(buf.get(), len + 2, filelist);
4230 CStringUtils::PipesToNulls(buf.get(), len + 2);
4231 SHFILEOPSTRUCT fileop;
4232 fileop.hwnd = this->m_hWnd;
4233 fileop.wFunc = FO_DELETE;
4234 fileop.pFrom = buf.get();
4235 fileop.pTo = nullptr;
4236 fileop.fFlags = FOF_NO_CONNECTED_ELEMENTS | ((GetAsyncKeyState(VK_SHIFT) & 0x8000) ? 0 : FOF_ALLOWUNDO);
4237 fileop.lpszProgressTitle = L"deleting file";
4238 int result = SHFileOperation(&fileop);
4240 if ((result == 0 || len == 1) && (!fileop.fAnyOperationsAborted))
4242 if (needWriteIndex && git_index_write(gitIndex))
4243 MessageBox(g_Git.GetLibGit2LastErr(L"Could not write index."), L"TortoiseGit", MB_OK);
4245 if (needWriteIndex)
4247 CWnd* pParent = GetLogicalParent();
4248 if (pParent && pParent->GetSafeHwnd())
4249 pParent->SendMessage(GITSLNM_NEEDSREFRESH);
4250 SetRedraw(TRUE);
4251 return;
4254 SetRedraw(FALSE);
4255 POSITION pos2 = nullptr;
4256 while ((pos2 = GetFirstSelectedItemPosition()) != nullptr)
4258 int index2 = GetNextSelectedItem(pos2);
4259 if (GetCheck(index2))
4260 m_nSelected--;
4261 m_nTotal--;
4263 RemoveListEntry(index2);
4265 SetRedraw(TRUE);
4269 BOOL CGitStatusListCtrl::OnWndMsg(UINT message, WPARAM wParam, LPARAM lParam, LRESULT* pResult)
4271 switch (message)
4273 case WM_MENUCHAR: // only supported by IContextMenu3
4274 if (g_IContext3)
4276 g_IContext3->HandleMenuMsg2(message, wParam, lParam, pResult);
4277 return TRUE;
4279 break;
4281 case WM_DRAWITEM:
4282 case WM_MEASUREITEM:
4283 if (wParam)
4284 break; // if wParam != 0 then the message is not menu-related
4286 case WM_INITMENU:
4287 case WM_INITMENUPOPUP:
4289 HMENU hMenu = (HMENU)wParam;
4290 if ((hMenu == m_hShellMenu) && (GetMenuItemCount(hMenu) == 0))
4292 // the shell submenu is populated only on request, i.e. right
4293 // before the submenu is shown
4294 if (g_pFolderhook)
4296 delete g_pFolderhook;
4297 g_pFolderhook = nullptr;
4299 CTGitPathList targetList;
4300 FillListOfSelectedItemPaths(targetList);
4301 if (!targetList.IsEmpty())
4303 // get IShellFolder interface of Desktop (root of shell namespace)
4304 if (g_psfDesktopFolder)
4305 g_psfDesktopFolder->Release();
4306 SHGetDesktopFolder(&g_psfDesktopFolder); // needed to obtain full qualified pidl
4308 // ParseDisplayName creates a PIDL from a file system path relative to the IShellFolder interface
4309 // but since we use the Desktop as our interface and the Desktop is the namespace root
4310 // that means that it's a fully qualified PIDL, which is what we need
4312 if (g_pidlArray)
4314 for (int i = 0; i < g_pidlArrayItems; i++)
4316 if (g_pidlArray[i])
4317 CoTaskMemFree(g_pidlArray[i]);
4319 CoTaskMemFree(g_pidlArray);
4320 g_pidlArray = nullptr;
4321 g_pidlArrayItems = 0;
4323 int nItems = targetList.GetCount();
4324 g_pidlArray = (LPITEMIDLIST *)CoTaskMemAlloc((nItems + 10) * sizeof(LPITEMIDLIST));
4325 SecureZeroMemory(g_pidlArray, (nItems + 10) * sizeof(LPITEMIDLIST));
4326 int succeededItems = 0;
4327 PIDLIST_RELATIVE pidl = nullptr;
4329 int bufsize = 1024;
4330 auto filepath = std::make_unique<WCHAR[]>(bufsize);
4331 for (int i = 0; i < nItems; i++)
4333 CString fullPath = g_Git.CombinePath(targetList[i].GetWinPath());
4334 if (bufsize < fullPath.GetLength())
4336 bufsize = fullPath.GetLength() + 3;
4337 filepath = std::make_unique<WCHAR[]>(bufsize);
4339 wcscpy_s(filepath.get(), bufsize, fullPath);
4340 if (SUCCEEDED(g_psfDesktopFolder->ParseDisplayName(nullptr, 0, filepath.get(), nullptr, &pidl, nullptr)))
4341 g_pidlArray[succeededItems++] = pidl; // copy pidl to pidlArray
4343 if (succeededItems == 0)
4345 CoTaskMemFree(g_pidlArray);
4346 g_pidlArray = nullptr;
4349 g_pidlArrayItems = succeededItems;
4351 if (g_pidlArrayItems)
4353 CString ext = targetList[0].GetFileExtension();
4355 ASSOCIATIONELEMENT const rgAssocItem[] =
4357 { ASSOCCLASS_PROGID_STR, nullptr, ext },
4358 { ASSOCCLASS_SYSTEM_STR, nullptr, ext },
4359 { ASSOCCLASS_APP_STR, nullptr, ext },
4360 { ASSOCCLASS_STAR, nullptr, nullptr },
4361 { ASSOCCLASS_FOLDER, nullptr, nullptr },
4363 IQueryAssociations* pIQueryAssociations = nullptr;
4364 if (FAILED(AssocCreateForClasses(rgAssocItem, ARRAYSIZE(rgAssocItem), IID_IQueryAssociations, (void**)&pIQueryAssociations)))
4365 pIQueryAssociations = nullptr; // not a problem, it works without this
4367 g_pFolderhook = new CIShellFolderHook(g_psfDesktopFolder, targetList);
4368 LPCONTEXTMENU icm1 = nullptr;
4370 DEFCONTEXTMENU dcm = { 0 };
4371 dcm.hwnd = m_hWnd;
4372 dcm.psf = g_pFolderhook;
4373 dcm.cidl = g_pidlArrayItems;
4374 dcm.apidl = (PCUITEMID_CHILD_ARRAY)g_pidlArray;
4375 dcm.punkAssociationInfo = pIQueryAssociations;
4376 if (SUCCEEDED(SHCreateDefaultContextMenu(&dcm, IID_IContextMenu, (void**)&icm1)))
4378 int iMenuType = 0; // to know which version of IContextMenu is supported
4379 if (icm1)
4380 { // since we got an IContextMenu interface we can now obtain the higher version interfaces via that
4381 if (icm1->QueryInterface(IID_IContextMenu3, (void**)&m_pContextMenu) == S_OK)
4382 iMenuType = 3;
4383 else if (icm1->QueryInterface(IID_IContextMenu2, (void**)&m_pContextMenu) == S_OK)
4384 iMenuType = 2;
4386 if (m_pContextMenu)
4387 icm1->Release(); // we can now release version 1 interface, cause we got a higher one
4388 else
4390 // since no higher versions were found
4391 // redirect ppContextMenu to version 1 interface
4392 iMenuType = 1;
4393 m_pContextMenu = icm1;
4396 if (m_pContextMenu)
4398 // lets fill the our popup menu
4399 UINT flags = CMF_NORMAL;
4400 flags |= (GetKeyState(VK_SHIFT) & 0x8000) != 0 ? CMF_EXTENDEDVERBS : 0;
4401 m_pContextMenu->QueryContextMenu(hMenu, 0, SHELL_MIN_CMD, SHELL_MAX_CMD, flags);
4404 // subclass window to handle menu related messages in CShellContextMenu
4405 if (iMenuType > 1) // only subclass if its version 2 or 3
4407 if (iMenuType == 2)
4408 g_IContext2 = (LPCONTEXTMENU2)m_pContextMenu;
4409 else // version 3
4410 g_IContext3 = (LPCONTEXTMENU3)m_pContextMenu;
4414 if (pIQueryAssociations)
4415 pIQueryAssociations->Release();
4418 if (g_IContext3)
4419 g_IContext3->HandleMenuMsg2(message, wParam, lParam, pResult);
4420 else if (g_IContext2)
4421 g_IContext2->HandleMenuMsg(message, wParam, lParam);
4422 return TRUE;
4426 break;
4427 default:
4428 break;
4431 return __super::OnWndMsg(message, wParam, lParam, pResult);
4434 CTGitPath* CGitStatusListCtrl::GetListEntry(int index)
4436 ATLASSERT(m_guard.GetCurrentThreadStatus());
4437 if ((size_t)index >= m_arListArray.size())
4439 ATLASSERT(FALSE);
4440 return nullptr;
4442 if (m_arListArray[index] >= m_arStatusArray.size())
4444 ATLASSERT(FALSE);
4445 return nullptr;
4447 return const_cast<CTGitPath*>(m_arStatusArray[m_arListArray[index]]);
4450 void CGitStatusListCtrl::OnSysColorChange()
4452 __super::OnSysColorChange();
4453 if (m_nBackgroundImageID)
4454 CAppUtils::SetListCtrlBackgroundImage(GetSafeHwnd(), m_nBackgroundImageID);
4457 ULONG CGitStatusListCtrl::GetGestureStatus(CPoint /*ptTouch*/)
4459 return 0;